Piintos Internals - 커널 스레드 동작 전체적 과정

기타/[Pintos]

2019. 4. 13. 17:09

[핀토스 1장. pintos internals]



#핀토스 커널 실행 코드의 메인 함수

int main (void)

   {

     ram_init ();

     argv = read_command_line ();

     argv = parse_options (argv);

   

     /* 스스로를 스레드화 */  #1

     thread_init ();  코드

     console_init ();  

   

     printf ("Pintos booting with %'"PRIu" kB RAM...\n",

             init_ram_pages * PGSIZE / );

   

     /* 메모리 시스템 초기화 */  #2

     palloc_init (user_page_limit);    코드

     malloc_init ();

     paging_init ();

  

    /* Segmentation. */

  #ifdef USERPROG

    tss_init ();

    gdt_init ();

  #endif

  

    /* 초기화(인터룹트, 장치들) */  #3

    intr_init ();    코드

    timer_init ();    코드

    kbd_init ();    코드

    input_init ();

 

  #ifdef USERPROG

    exception_init ();

    syscall_init ();

  #endif

  

    /* 스레드 스케줄러 시작 */  #4

    thread_start ();     코드

    serial_init_queue ();

    timer_calibrate ();

  

  #ifdef FILESYS

    /* Initialize file system. */

    ide_init ();

    locate_block_devices ();

    filesys_init (format_filesys);

  #endif

  

    printf ("Boot complete.\n");

    

    /* 테스트 혹은 사용자 프로그램 실행 */  #5

    run_actions (argv);

  

    /* Finish up. */

    shutdown ();

    thread_exit ();

  }







1. The Initial Thread (스레드를 스레드화)


# thread_init 함수

 void thread_init (void)

{

  ASSERT (intr_get_level () == INTR_OFF);

  

  lock_init (&tid_lock);

  list_init (&ready_list);

  

  /* 실행 스레드를 위해 스레드 구조 셋업 */

  initial_thread = running_thread ();

  init_thread (initial_thread, "main", PRI_DEFAULT);

  initial_thread->status = THREAD_RUNNING;

  initial_thread->tid = allocate_tid 

 


# ASSERT 함수 : 값이 False면 시스템 Shutdown을 실시 ( = kernel panic 상태)

# inter_get_level 함수 : interrupt 꺼져 있는지 확인(현재 부팅단계라 꺼져있어야 정상)



# 레디 큐 초기화



# running_thread 함수 : PCB 초기화 값의 시작 주소를 리턴 = tid값

# 우선순위 부여 (0~63, 초기값 : 32)

# status : PCB 상태를 의미, running 상태로 전환

# tid 값 +1




# 핀토스의 PCB

 struct thread

{

  /* 고유번호 */

  tid_t tid;

  /* 스레드 상태 */

  enum thread_status status;

  /* 스레드 명 */

  char name[16];

  /* 스레드 문맥교환 시에 문맥 저장하는 곳 */

  unit8_t *stack;

  /* 우선순위 저장(0~63) */

  int priority;

  /* 이 PCB를 큐에 넣을때 사용 */

#ifdef USERPROC

  /* 페이지 디렉토리 */

  uint32_t *pagedir;

#endif

  /* 스택 오버플로우 감지 */

  unsigned magic;

};

 

- thread 하나 생성시 구조체(4kilo 공간) 하나씩 생성.


- tid ~ magic 구간이 스레드의 PCB에 해당된다.


- 4kilo 공간중 PCB 공간을 뺀 나머지는 

  kernel stack(해당 스레드 실행하며 사용할 스택공간)이다.


- 스택 공간에 데이터 쌓이다가 size 초과되면 

  magic으로 넘어가서 오버플로우 감시( = magic 역할)











2. INITIALIZING MEMORY ( 메모리 초기화 )


# 부팅 직후 최초 메모리

 


 - 핀토스는 가상메모리를 사용한다

  => 모든 응용프로그램은 가상메모리를 사용시 4GB를 할당한다(실제메모리 아님)

  => 4GB중 3은 사용자 프로그램이 사용하고, 1을 커널(핀토스)이 사용한다.



<1GB 커널공간 배치>


 # .bss : 초기화값이 지정되지 않은 전역변수들 위치

 # .data : 초기화된 전역변수들 위치

 # .rodata : 초기화된 전역변수중 값이 바뀌지 않는 값들(=읽기전용)

 # .text : main함수 컴파일해서 나오는 값들

 ㄴ 여기까지가 우리가 보고 있는 코드들이 위치해 있는 위치.


 # LOADER_PhYS_BASE(값) : 값 = 커널의 시작지점.



# 동적 메모리 할당 영역 초기화 (palloc_init 함수)

 

 # palloc_init 함수 : 동적 할당 메모리 부분 초기화




# paging_init 함수 : 페이징 시스템 초기화  (코드생략)




# 가상 메모리를 실제메모리로 매핑하는 과정

 


+) 우리가 흔히 아는 포인터 변수(주소) 값은 물리메모리(RAM등)의 주소가 아니라 가상 메모리 주소이다. ( 물리 주소는 알수 없음 )







3, INITIALIZING INTERRUPT HANDLING ( 인터룹트 초기화 )



[내부 인터럽트와 외부 인터럽트]

- 내부 인터럽트 ( = 트랩, SW 인터럽트 )

  : CPU에 의해 발생하는 인터럽트 (시스템 호출, 비유효 메모리 접근, 0으로 나누기등)

  : 동기(Synchronous) = CPU동작과 인터럽트 발생이 서로 인과관계가 있음을 의미.


- 외부 인터럽트 ( I/O 인터럽트 )

  : 오류가 아닌, 시스템 제어를 커널로 넘기기 위한 수단으로서 사용.

  : 비동기(asynchronous) : CPU 동작과 인터럽트 발생의 인과관계가 없음을 의미

  : 외부 인터럽트 처리중에 중간에 멈췄다가 나중에 다시시작(sleep/yield) 불가능. => 가능한 빠르게 처리






[Interrupt Descriptor Table(IDT)]

: 인터럽트 256개에 관한 Descriptor(설명,처리방법) 모아둔거

- idtr : 위치값



# IDT 초기화 코드

void intr_init ( void)

{

  pic_init ();


  /* IDT 초기화 */

  for (i = 0;  i  < INTR_CNT; i++)

    idt[i] = make_intr_gate (intr_stubs[i], 0);


  /* IDT 레지스터 로드 */

  idtr_operand = make_idtr_operand (sizeof idt-1, idt);

  asm volatile ("lidt %0" :  :  "m" (idtr_operand)); 

 

 # intr_stubs[i] : 값 초기화 되어있는 배열

 # intr숫자_strub() : 처리함수의 함수 포인터

 # idt[i] : 초기화 하려는 배열  



# 인터럽트 초기화(타이머, 키보드)

void timer_init (void)

{

  uint16_t count = (1193180 + TIMER_FREQ / 2) / TIMER_FREQ;

  outb (0x43, 0x34);

  outb (0x40, count & 0xff);

  outb (0x40, count >> 8);

  intr_register_ext (0x20, timer_interrupt, "8254 Timer");

}


void kbd_init (void)

{

  intr_register_ext (0x21, keyboard-interrupt, "8042 Keyboard");

} 

  # 각각 타이머, 키보드 초기화 함수

  # count : 타이머 주기

  # intr_register_ext 함수 : 인터럽트 본격적 처리함수



  - intr_handlers : 각 인터럽트 처리함수들이 저장되어 있음  코드









[Interrupt Handling(외부에서 인터럽트 발생시, CPU가 해당 인털럽트 처리기를 실행하는 작업)]  -  약간 독립적

<개요>

1. CPU에 외부 interrupt 발생.

2. CPU에서 실행중이던 레지스터 push로 저장 - +) 현재 실행중인 thread(running 상태의 thread)의 stack에 저장.

3. 들어온 인터럽트의 종류를 idt[]에서 찾고, intNN_stub로 이동.


# 인터럽트 발생시 레지스터 저장함수

intr_entry:

  push ...

  call intr_handler   코드

intr_exit:

  pop ...

  iret


intnn_stub:

  push ...

  jmp intr_entry 

 - push 여러개로 레지스터가 나누어서 저장된다.

 - pop : 상태 복원 (위에서 인터럽트 처리 완료후 실행된다)

 - iret : 인터럽트 return 명령.


# intnn_Stub : idt[]를 통해 제일 먼저 실행된다.

# call intr_handler : 레지스터 처리함수를 호출



# 레지스터 처리함수 밑밑에 자세히 코드

/* 레지스터처리함수 */ 

void intr_handler (struct intr_frame *frame)

{

  ...

  handler = intr_handler[frame->vec_no]; 코드

  if (handler != NULL)

    handler (frame);

  ...

 - 모든 인터럽트 처리 시작 지점. (여기들어오기전에는 실행중이던 reg값 저장하는 작업)


# intr_handler : interrupt 종류마다 처리방법 다름

  : 이거 전까지는 공통 사항. 여기부터 달라진다.

 






[인터럽트된 스레드의 스택의 인터럽트 프레임]

: 위에서 말했던 reg의 상태가 저장되는 running thread의 stack의 모습.


# 스택의 모습


- 3번에 나누어 push한 내용 

  1) CPU로 인한 push : 4칸 저장

  2) intrNN_stub()로 인한 push : 3칸 저장

  3) intr_entry()로 인한 push : 나머지 저장.






[intr_handler()]

: 256개의 인터럽트중 번호를 받아 해당 인터럽트 처리함수. (위에 레지스터처리함수에서 실행됨)

void intr_handler (struct intr_frame *frame)

{

  bool external;

  intr_handler_func *handler;


  external = frame->vec_no >= 0x20 && frame->vec_no < 0x30;


  if (external){

    ASSERT(intr_get_level()==INTR_OFF);

    ASSERT (!intr_context());

    in_external_intr = true;

    yield_on_return = false;

  }


  /* 인터럽트 처리문장 */

  handler = intr_handlers[frame->vec_no];

  if (handler != NULL) handler(frame);      코드

  else if (frame->vec_no == 0x27 || frame->vec_no == 0x2f) {}

  else {

    intr_dump_frame(frame);

    PANIC ("Unexpected interrupt")


  if (external) {

    ASSERT(intr_get_level() == INTR_OFF);

    ASSERT (intr_context());

    in_external_intr = false;

    pic_end_of_interrupt(frame->vec_no);

    if (yield_on_return)

      thread_yield();  코드

  }

}

# intr_frame :  stack 포인터





# 해당 범위 내에 있으면 외부 인터럽트에 해당.





# in_external_intr : 현재 인터런트가 외부 인터럽트인지 여부.

# yield_on_return : 현재 실행 프로세스가 CPU 뺏을지 여부

  (여기서는 외부 인터럽트 처리중이기엔 false)



# 여기서 해당되는 인터럽트 함수호출형태로 처리

# handler() : 타이머 인터럽트 함수 실행 (아래 case1에 자세히)






# 외부 인터럽트일때 동작



# in_external_intr = false : 외부 인터럽트 동작끝났기에 변경

# pic_end_of_interrupt() : 외부 인터럽트 넣은 장치에 처리 끝남을 알림

# 위 handler()함수를 통해 실행된 타이머 인터럽트 함수가 끝날때 yield_on_return <- 1을 리턴한다. =  T로 바뀌면 실행 프로세스 CPU를 뺏는다. 

# 스레드 교환(실행->준비)













4. STARTING THREAD SCHEDULER(스레드 스케쥴러 시작)



[Starting Thread Scheduling]


# 스레드 시작 함수

void thread_start (void)

{

  /* idle 스레드 생성 */

  struct semaphore idle_started;


  sema_init 97idle-started, 0);

  thread_create("idle",            코드

                     PRI_MIN,

                     idle,

                    &idle_started);


  /* 선점형(preemptive) 스레드 스케쥴링 시작 */

  intr_enable();


  /* idle 스레드 초기화를 위한 idle 스레드 대기 */

  sema_down (&idle_started);     코드

 






# thread_create() : 새 스레드 생성시 꼭 사용.

# PRI_MIn : 제일 낮은 우선순위

# idle : 지금 만드는 새 스레드가 수행할 함수

# &idle_started : 위 함수의 파라미터



# intr_enable() : 인터럽트 시작함수( = 타이머시작)



# 스레드 교환(실행->대기)


# idle 함수 생략


# 스레드 생성 함수

tid_t thread_create (

      const char *name,

      int priority,

      thread_func *function,

      void *aux)

{

  struct thread *t;

  struct kernel_thread_frame *kf;

  struct switch_entry_frame *ef;

  struct switch_threads_frame *sf;

  tid_t tid;


  ASSERT (function != NULL);

  

  /* 스레드 할당(allocate) */  1)

  t = palloc_get_page(PAL_ZERO);

  if (t == NULL) return TID_ERROR;


  /* 스레드 초기화 */

  init_thread (t,name,priority);

  tid = t->tid = allocate_tid ();


  /* 커널 스레드를 위한 스택 프레임 */ 2)

  kf = alloc_frame(t, sizeif *kf);

  kf->eip = NULL;

  kf->function = function;

  kf->aux = aux;


  /* switch_entry() 를 위한 스택 프레임 */ 

  sf =alloc_frame(t, sizeof *sf);

  sf->eip = switch_entry;


  /* 실행 큐에 추가 */

  thread_unblock (t);


  return tid;

}

 



- 스레드 생헝 함수가 하는 일 두가지.

 1) 스레드 할당과 초기화

 2) switch_threads()와 kernel_thread()를 위한 가짜 스택 프레임 생성







- 스레드 최초 실행시, 프로세스 대기(ready)상태에서 시작.

  : 레디 큐에 넣어야 됨.





- 새로운 스레드의 PCB의 stack(4kilo)에 초기값 삽입(=스택 초기화)





- 이후 스레드 교환에서 필요성 설명예정.



















[실행중인 스레드 상태 변화 종류]

: 실행 상태에서 상태변화가 일어남. = running 스레드의 CPU 반납 과정(정해진 시간)






[스레드 교환(thread switching) - 케이스 1]

: 실행(running) 상태 -> 준비(ready) 상태


# intr_handler 함수를 통해 thread_yield 함수 실행 과정 보기(위에 함수 코드 있고 간략히 써놨지만 여기서 한번 더씀)

 intr_handler()

{

  ...

  handler();

  ...

  if(external) {

    ...

    if(yield_on_return)

      thread_yield();    코드

  }

}

 



# handler 함수를 통해 timer_interrupt 함수가 실행된다.




# 타이머 인터럽트 함수에서 yield_on_return 값을 1로 변경해서 리턴






# timer_interrupt 함수

 timer_interrupt()

{

  ticks++;

  ...

  if (++thread_ticks >= TIME_SLICE)

    intr_yield_on_return();

}

 


# ticks++ : 타이머 인터럽트 발생시 +1 = 시간의 흐름, 발생횟수



# intr_yield_on_return 함수 내에서 yield_on_return 값을 1로 변경해줌.




# thread_yield 함수

thread_yield (void)

{

  struct thread *cur = thread_current ();

  enum intr_level old_level;


  ASSERT (!intr_context ());

 

  old_level = intr_disable ());

  if (cur != idle_thread)

    list_push_back (&ready_list, &cur->elem);

  cur->status = THREAD_READY;

  schedule();  

  intr_set_level (old_level);

}

 


# thread_current() : 현재 실행중인 스레드의 PCB






# 현재 실행중인 스레드가 idle_thread가 아니면,

# 준비 연결리스트 맨 끝에 실행중인 스레드의 PCB를 삽입해라

# 상태도 준비상태로 변경.

# schedule() : 호출시, 다음번 실행 스레드 선택, 그 스레드로 문맥교환 작업 발생












[스레드 교환(Thread Switching) - 케이스 2]

: 실행(running)상태 -> 대기(waiting)상태


# sema_down 함수 : 아직 몰라도 됨.

void sema_down (struct semaphore *sema)

{

  ...

  while (sema->value == 0)

  { 

    list_push_back (&sema->waiters,

                        &thread_current () ->elem);

    thread_block();   코드

    }

  ...

 # thread_block 함수 : 스레드 교환 작업 실행.



# thread_block 함수

thread_block (void)

{

  ASSERT (!intr_context ());

  ASSERT (intr_get_level () == INTR_OFF);


  thread_current ()->status = THREAD_BLOCKED;

  schedule (); 

 



# 교환중에 외부 인터럽트 들어오면 곤란. 그래서 잠깐 받는거 꺼둠.


# 실행중이던 스레드 상태 변경 : BLOCKING 상태로. (wait,block,sleep 같은 상태의미)

# 스케쥴 함수 호출되는 두번째 경우(다음 실행 스레드 선정, 문맥교환)








[스레드 교환(Thread Switching) - 케이스 3]

: 실행(running)상태 -> 종료(terminate)상태


# kernel_thread 함수 : 모든 스레드의 시작점이자 끝

kernel_thread (thread_func *function, void *aux)

{

  ASSERT (function != NULL);


  intr_enable ();

  function (aux);

  thread_exit ();   코드





# 인터럽트 꺼진상태에서 스케쥴러 실행

# 스레드 기능 실행

# thread_exit () : 위 스레드 기능 실행 함수가 리턴되면, 스레드 kill 함수.



# thread_exit 함수

 void thread_exit (void)

{

  ASSERT (!intr_context ());


#ifdef USERPROG

  process_exit();

#endif


  intr_disable();

  thread_current ()->status = THREAD_DYING;

  schedule ();

  NOT_REACHED ();

}

 









# 스레드 상태 변경

# 스케쥴 함수 실행 3번째 경우




# 정리

- running -> ready      : intr_handler() - thread_yield() - schedule()

- running -> waiting    : sema_down() - thread_block() - schedule()

- running -> terminate : kernel_thread() - thread_exit() - schedule()








[schedule 함수]

 : 다음 실행 스레드 선정, 그 스레드와 문맥 교환.


# schedule 함수

 schedule (void)

{

  struct thread *cur = running_thread ();

  struct thread *next = next_thread_to_run ();

  struct thread *prev = NULL;


  ASSERT (intr_get_level () == INTR_OFF);

  ASSERT (cur->status != THREAD_RUNNING);

  ASSERT (is_thread (next));


  if (cur != next)

    prev = switch_threads (cur -> next)   코드

  schedule_tail (prev);   코드

}

 


# 기존 실행 스레드 PCB 상태 저장

# 다음 실행 스레드 선택(레디 큐에서)



# 인터럽트 꺼져 있는지 확인 : 스케쥴 함수 실행되기전에 지정

# 기존 스레드 상태 바뀌어 있는지 확인 : 스케쥴 함수 실행전에 상태 변화 해줬어야 함(status 값)

# 다음 실행 스레드( = next)가 올바른 PCB인지 확인


# 기존 스레드와 다음 스레드가 동일시, 문맥교환 필요 없다.

# switch_threads () : 문맥 교환 함수






# switch_threads 함수 : 문맥교환 함수

 switch_threads:


  # push 문으로 레지스터값 stack에 저장(위에서 배움)

  pushl %ebx

  pushl %ebp

  pushl %esi

  pushl %edi


  .globl thread_stack_ofs

    mov thread stack ofs, %edx


  # 현재 스택 포인터를 old thread의 스택에 저장.

  movl SWITCH_CUR(%esp), %eax

  movl %esp, (%eax, %edx, 1)


  # 새로운 스레드의 스택으로부터 스택 포인터 복원.

  mov1 SWITCH_NEXT(%esp), %ecx

  movl (%ecx, %dbx, 1), %esp


  # 레지스터(문맥) 복원

  popl %edi

  popl %esi

  popl %ebp

  popl %ebx

         ret


.endfunc

 



# schedule_tail 함수 : 새로운 실행 스레드의 상태 변화, 이전 종료 스레드의 완벽 제거 

void schedule_tail (struct thread *prev)

{

  struct thread *cur = running_thread ();

  ASSERT (intr_get_level () == INTR_OFF);

  cur->status = THREAD_RUNNING;

  thread_ticks = 0;

#ifdef USERPROC

  process_activate ();

#endif


  if (prev!=NULL && 

     prev->status == THREAD_DYING &&

     prev!=initial_thread) {

    ASSERT (prev != cur);

    palloc_free_page (prev)

  }

 # prev : 조금전까지 실행 상태였다 변화된 스레드.


# 새로운 실행 스레드


# 새로운 실행 스레드의 상태를 실행으로 변환

# 새로운 타임 슬라이스 시작(이 실행 스레드에 할당된 시간









# palloc_free_page (prev) : 이전 실행 스레드가 종료중이였다면, 

 걔가 사용중이던 가상 메모리 공간(4kilo) 할당 해제 = 완벽 제거.







[특이 케이스 - 다음 실행 스레드가 새로 생성된 스레드(이전에 한번도 실행된적없는 스레드) 인경우]

: 이 경우엔 문맥교환이 일어날때, 스택포인터와 같은 정보가 없기때문에 문맥 복원 코드 적용하는데 문제...

 => 스레드 생성시, 가짜 코드지만 문맥교환 작업 수행하는데 적잘한 코드를 해당 스레드의 스택에 넣어준다.


# switch_entry 함수 : 생략..시간날때..












5. RUNNING TEST(실행 테스트)


[테스트 프로그램 실행 - 핀토스의 알람기능 동작 테스트]

문제) test_sleep(5,7) : 5개의 스레드가 각각 7번 알람.


# 문제




: 각 스레드 별로 주기(duration) : 10, 20, ...50 (총 5개)



# 결과

: 화살표 : 출력 발생 지점





















'기타 > [Pintos]' 카테고리의 다른 글

0. 핀토스 설치하기  (0) 2019.04.13