
2025.05.23
유저 모드 컨텍스트 복제
struct intr_frame if_를 페이지 단위로 복사 (palloc_get_page)R.rax = 0 으로 설정 → 자식 fork()에서 0 반환자식 상태 관리 구조체 준비
struct child_status {
tid_t tid; /* 자식 TID */
int exit_status; /* 자식이 exit()에서 넘긴 상태 코드 */
bool has_exited; /* exit() 호출 여부 */
struct semaphore sema;/* 부모 대기를 위한 세마포어 */
struct list_elem elem;/* 부모의 children 리스트 연결자 */
};
sema_init(&c->sema, 0)으로 부모가 sema_down하면 블록자식 스레드 생성
thread_create(name, PRI_DEFAULT, __do_fork, child_if) 호출name 을 그대로 쓰레드 이름으로 사용부모–자식 관계 설정
c->tid = child_tid;
thread_by_tid(child_tid)->parent_tid = parent->tid;
thread_by_tid()로 자식 스레드 구조체를 찾아 parent_tid 설정process_exit()에서 부모를 깨울 수 있음부모 동기화 (semaphore)
sema_down(&c->sema)로 자식 복제 완료(__do_fork() 안에서 sema_up) 시점까지 블록리턴
process_fork()는 자식 TID를 반환__do_fork() 안의 do_iret(&child_if)를 통해 사용자 모드로 복귀하며 fork()에서는 0을 반환자식 스레드 본체: __do_fork()
current->pml4 = pml4_create();
process_activate(current);pml4_for_each(parent->pml4, duplicate_pte, parent);sema_up(&c->sema);do_iret(&child_if);페이지 복제 핵심: duplicate_pte()
duplicate_pte()가 부모 PML4를 한 페이지씩 순회하며 fork() 시 주소 공간이 부모와 완전 복제
pml4_get_page(parent->pml4, va)로 부모 페이지 찾고palloc_get_page(PAL_USER)로 새 페이지 할당memcpy(newpage, parent_page, PGSIZE)pte & PTE_W)를 확인해서 writable 세팅pml4_set_page(child->pml4, va, newpage, writable)로 자식에게 매핑child_status 할당 및 초기화children 리스트에 push_backthread_create() 로 initd 생성c->tid = tid; parent_tid 설정sema_down(&c->sema) 으로 부모 블록fn_copy에 "프로그램 [인자...]" 형태의 문자열 저장prog_name에 복사 → 스레드 이름으로 사용child_status 생성: 자식 TID, exit 상태, 세마포어, 리스트 엘리먼트 등children 리스트에 c 추가thread_create(prog_name, ..., initd, fn_copy) 호출c->tid = tid;parent_tid = parent->tid;sema_down(&c->sema)로 부모가 initd의 process_exit()까지 블록이제 run 'foo [args]' 경로에서 부모는 initd가 완전히 종료(exit)돼 process_exit()에서 sema_up(&c->sema) 호출될 때까지 올바르게 기다리고, 그 후에 "Execution of 'foo ...' complete." 메시지와 함께 종료.
process_create_initd() – 커널이 처음 한 번 “initd”라는 사용자 프로세스를 띄우기 위해 쓰는 함수. – 내부적으로는 thread_create(initd…) → process_exec() → ELF 로딩 경로로 들어가기 때문에, fork가 아니라 exec 기반으로 새 프로세스를 시작. – 이걸로 “메인 커널”과는 완전히 다른 첫 사용자 프로세스 컨텍스트를 만들고, 부모(커널 스레드)는 그 프로세스가 끝날 때까지 기다림.process_fork() – 그 이후 userland 에서 fork() 시스템 콜을 호출할 때마다, 이미 실행 중인 프로세스를 복제(clone) 하기 위해 쓰는 함수. – SYS_FORK → syscall_handler → process_fork(name, &if_) 로 진입해서, 부모의 메모리·파일 디스크립터·실행 컨텍스트를 그대로 복사한 “자식” 프로세스를 만들어 줌. – fork()가 리턴한 뒤 부모에는 자식 TID, 자식 코드 경로에서는 0이 돌아가도록 레지스터를 세팅해 주는 전형적인 Unix‐style fork 구현.
| fork | exec | |
|---|---|---|
| 생성 시점 | 이미 실행 중인 프로세스를 복제 | 커널(메인 스레드)이 디스크에서 새 프로그램을 로드 |
| 주소 공간 | 부모와 똑같은 가상 메모리(코드·데이터)를 공유·복제 | 완전히 새로운 ELF 바이너리를 읽어서 덮어씀 |
| 스레드 이름 | 원래 프로세스 이름(또는 인자가 넘긴 이름) 유지 | process_create_initd()에서 짠 이름(prog_name) |
| 반환값 | 부모: 자식 TID, 자식: 0 반환 | 호출한 곳으로 돌아가지 않음(성공 시 곧바로 사용자 코드 진입) |
| 내부 동기화 | 세마포어로 부모·자식간 sema_down/sema_up | 부모는 process_create_initd()에서 대기, exec 자체에는 wait 없음 |
main() (threads/init.c) 진입bss_init)palloc_init, paging_init)thread_init) → all_list, ready_list 등 세팅console_init, timer_init, kbd_init …)exception_init, syscall_init)thread_start() 호출 → idle 스레드 생성 → 인터럽트 허용 → 스케줄러 시작run_actions() → run_task()run '프로그램 [인자]' 옵션을 해석process_create_initd(cmdline) 호출process_create_initd()cmdline 전체를 복사해 두고(palloc_get_page + strlcpy)prog_name(첫 번째 토큰)만 뽑아 스레드 이름으로 사용children 리스트에 child_status 추가thread_create(prog_name, initd, fn_copy)sema_down으로 블록initd() (커널 스레드)process_init() 호출(빈 껍데기)process_exec(fn_copy) → 유저 ELF 로드 루틴(load()) 진입do_iret()로 사용자 모드로 전환 → 유저 프로그램의 main(int, char**) 진입exit()할 때까지 sema_down에서 블록process_exit() → sema_up → 부모 깨어남process_create_initd() 리턴 → run_task()에서 “Execution complete.” 출력 → VM 종료thread_create)API 호출
tid_t tid = thread_create("name", priority, function, aux);
새 커널 스레드 구조체 할당
palloc_get_page(PAL_ZERO)init_thread(t, name, priority)초기 컨텍스트 설정
t->tf.rip = (uintptr_t)kernel_thread;
t->tf.R.rdi = (uint64_t)function;
t->tf.R.rsi = (uint64_t)aux;
/* cs, ds, ss, eflags 등 설정 */
READY 상태에 추가
ready_list에 삽입thread_preempt()로 즉시 선점스케줄러
schedule() → next_thread_to_run() → thread_launch() → do_iret()kernel_thread(function, aux)로 진입 → intr_enable() → function(aux) 실행fork() 와 exec() 흐름 비교fork() | exec() | |
|---|---|---|
| 호출 위치 | 유저 모드 C 코드 안에서 | 유저 모드 C 코드 안에서 |
| 진입 경로 | SYS_FORK → syscall_handler → process_fork(name, &if_) | SYS_EXEC → syscall_handler → process_exec(fn_copy) |
| 동작 | 부모 컨텍스트(주소 공간, 파일 등) 복제 → 자식 생성 | 현재 스레드 주소 공간 버리고 ELF 로드 |
| 반환값 | 부모: 자식 TID, 자식: 0 | 호출 지점으로 복귀하지 않음 |
process_fork()intr_frame 복사 + RAX=0 세팅child_status 생성 → 부모 리스트 추가 → 부모 sema_down 블록thread_create(name, __do_fork, child_if) → 복제 쓰레드 생성__do_fork()에서 페이지 테이블·파일 디스크립터·메모리 복제 후 sema_up → 부모 깨어남process_exec()process_cleanup() → 기존 주소 공간 해제load() → ELF 로드(프로그램 헤더별 페이지 매핑, 스택/argv 구성)do_iret() → 새 프로그램 진입(main)sema_down(&c->sema)sema_up(&c->sema) (__do_fork() 안)sema_down(&c->sema) (process_wait())sema_up(&c->sema) (process_exit() 안)process_exec)를 통해process_fork() → __do_fork() 안에서pml4_for_each(parent->pml4, duplicate_pte, parent) 호출duplicate_pte() 가상 주소 va 하나하나마다parent_page = pml4_get_page(parent->pml4, va) 로 부모의 물리 페이지 찾아오고newpage = palloc_get_page(PAL_USER) 로 자식 쪽 새 페이지 할당memcpy(newpage, parent_page, PGSIZE) 로 내용(기계어 코드+데이터)을 복사pml4_set_page(child->pml4, va, newpage, writable) 로 자식 PML4 에 매핑결국 “fork” 를 호출하면
모두 새로 할당한 페이지에 동일하게 복사되기 때문에, 자식은 부모와 메모리 컨텐츠가 완전히 똑같은 프로세스가 됨.
중요한 차이: “소스코드(.c)” 자체가 복사되는 게 아니라, 메모리에 올라가서 실행되는 컴파일된 기계어 코드와 그 외 데이터 구조가 통째로 복제된다고 이해하면 된다.