[핀토스 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 |
---|