Pintos Project 2 분업 기록
fork 파트는 어디까지 혼자 만들 수 있고,
어디부터 팀 코드와 맞물릴까
팀 브랜치를 합친 뒤 내 담당은 fork 쪽으로 정리됐다. 처음에는 fork 말고 다른 작업을 먼저 할 수 있을지 봤지만, fd table, wait, deny write가 서로 물려 있어서 임시 코드로 밀어붙이면 나중에 더 꼬일 가능성이 컸다. 그래서 이번 글은 fork 엔진 자체를 코드 흐름으로 다시 읽고, 다음 연결 지점을 표시하는 데 집중한다.
이 글의 완성 기준
| 읽는 목표 | 이 글만 보고 fork 관련 코드 블록을 기존 브랜치에 같은 구조로 옮길 수 있어야 한다. |
|---|---|
| 핵심 흐름 | SYS_FORK -> process_fork() -> thread_create() -> __do_fork() -> duplicate_pte() -> do_iret() |
| 중심 테스트 | fork-once를 기준으로 fork의 기본 분기와 주소공간 복사를 이해한다. |
| 범위 밖 | fork-read, fork-close, fork-recursive는 fd 상속과 정식 wait 구조까지 연결되어야 한다. 이 글은 그 연결 지점을 표시한다. |
왜 새 코드를 더 넣기보다 fork를 다시 읽었나
fork 말고 독립적으로 할 수 있는 작업을 찾으면 deny write가 먼저 떠오른다. 하지만 rox-simple은 file syscall이 필요하고, rox-child, rox-multichild는 exec/wait 흐름과도 엮인다. 즉 deny write만 따로 넣어도 바로 의미 있는 테스트를 보기 어렵다.
fork 쪽도 마찬가지다. 기본 fork 엔진은 만들 수 있지만, fd 상속은 fd table이 필요하고, recursive fork는 child_info 기반 wait가 필요하다. 그래서 지금 필요한 것은 “임시로 더 붙일 코드”가 아니라, fork 엔진이 어디까지 담당하고 어디서 팀원 코드와 만나는지 구분하는 것이었다.
0. 회의 후 다시 세운 기준
이전 구현은 Argument Passing을 통과시키는 데 초점이 있었다. 명령어를 자르고, user stack을 만들고, 임시 대기 구조로 출력 테스트를 확인하는 흐름이었다. 하지만 팀 브랜치를 합치고 나니 기준이 달라졌다. 이제는 “args 테스트가 돈다”보다 “fork, wait, fd table, exit status가 서로 연결될 수 있는 구조인가”가 더 중요해졌다.
| 이전 흐름에서 보인 한계 | 왜 문제가 되는가 | fork 브랜치에서 잡은 기준 |
|---|---|---|
process_exec() 중심으로 실행만 맞춤 |
새 프로세스가 아니라 기존 프로세스를 갈아끼우는 흐름이어서 fork의 복제 문제를 설명하지 못한다. | process_fork()와 __do_fork()를 나누어 부모/자식 역할을 분리한다. |
| 부모가 자식 준비 완료 전에 먼저 진행할 수 있음 | 자식 주소공간 복사가 실패했는데도 부모가 성공처럼 tid를 받을 수 있다. | fork_sema와 success로 자식 복제 완료 여부를 부모에게 전달한다. |
| 부모의 레지스터 상태를 자식에게 넘기는 기준이 없음 | 자식이 fork 다음 줄부터 실행되는 것처럼 보일 수 없다. | intr_frame을 복사하고 자식의 R.rax만 0으로 바꾼다. |
| fd와 wait는 필드만 있다고 자동으로 연결되지 않음 | fork-read, fork-close, fork-recursive는 fd 상속과 child_info 기반 wait가 필요하다. |
현재 글에서는 fork 엔진을 정리하고, 다음 연결 지점을 명확히 표시한다. |
1. 먼저 보는 조립 지도
아래 순서대로 넣어야 fork 코드가 한 덩어리로 맞물린다. 중간 블록 하나만 가져가면 컴파일은 되더라도 fork 흐름이 완성되지 않는다.
process.h에 process_fork(const char *, struct intr_frame *) 선언process.c 상단에 fork_args 구조체 추가process_fork()에서 부모 문맥을 담고 자식 thread 생성duplicate_pte()에서 부모 유저 페이지를 자식 페이지로 복사__do_fork()에서 자식의 실행 문맥과 주소공간 준비syscall.c의 SYS_FORK에서 process_fork() 호출2. process.h: fork 함수 원형부터 맞춘다
syscall.c에서 process_fork()를 호출하려면 헤더에 함수 원형이 있어야 한다. 두 번째 인자로 struct intr_frame *를 받는 이유는 fork 순간의 부모 레지스터 상태를 자식에게 넘기기 위해서다.
tid_t process_create_initd (const char *file_name);
tid_t process_fork (const char *name, struct intr_frame *if_);
int process_exec (void *f_name);
int process_wait (tid_t);
void process_exit (void);
void process_activate (struct thread *next);
3. syscall.c: fork의 입구는 SYS_FORK 분기다
유저 프로그램이 fork()를 호출하면 syscall 번호가 SYS_FORK로 들어온다. 여기서는 이름 인자와 현재 intr_frame을 그대로 process_fork()에 넘긴다.
case SYS_FORK:
f->R.rax = process_fork ((const char *) f->R.rdi, f);
break;
f는 단순한 인자 묶음이 아니다. fork syscall을 호출한 순간의 부모 레지스터 상태다. 자식이 부모와 같은 코드 위치에서 이어지는 것처럼 보이려면 이 값을 복사해야 한다.
4. fork_args: 부모와 자식을 이어주는 상자
자식 thread는 thread_create()로 시작된다. 그런데 자식이 부모의 pml4와 fork 순간의 intr_frame을 알아야 하므로, 이 정보를 하나의 구조체에 묶어 aux로 넘긴다.
struct fork_args {
struct thread *parent; /* fork를 호출한 부모 thread */
struct intr_frame parent_if; /* fork 순간 부모의 레지스터 상태 */
struct semaphore fork_sema; /* 자식 복사가 끝날 때까지 부모를 재우는 신호 */
bool success; /* 자식 복사가 성공했는지 부모에게 알려주는 값 */
};
| 필드 | 없으면 생기는 문제 |
|---|---|
parent |
자식이 부모의 주소공간을 어디서 복사해야 하는지 모른다. |
parent_if |
자식이 fork 다음 줄부터 이어서 실행되는 것처럼 만들 수 없다. |
fork_sema |
부모가 자식 복사 완료 전에 먼저 반환할 수 있다. |
success |
자식 복사 실패를 부모에게 전달할 방법이 없다. |
5. process_fork(): 부모 쪽 fork 흐름
process_fork()는 부모가 실행하는 함수다. 여기서 부모는 자식 thread를 만들고, 자식이 주소공간 복사를 마칠 때까지 sema_down()으로 기다린다.
tid_t
process_fork (const char *name, struct intr_frame *if_) {
struct fork_args *args = palloc_get_page (PAL_ZERO);
if (args == NULL)
return TID_ERROR;
args->parent = thread_current ();
memcpy (&args->parent_if, if_, sizeof args->parent_if);
sema_init (&args->fork_sema, 0);
args->success = false;
tid_t tid = thread_create (name, PRI_DEFAULT, __do_fork, args);
if (tid == TID_ERROR) {
palloc_free_page (args);
return TID_ERROR;
}
sema_down (&args->fork_sema);
bool success = args->success;
palloc_free_page (args);
return success ? tid : TID_ERROR;
}
이 블록이 해결하는 것
부모는 자식이 복제에 성공했는지 확인한 뒤에만 fork 결과를 반환한다.
조심할 것
부모가 args를 해제하므로, 자식은 sema_up() 이후에 args를 다시 참조하면 안 된다.
6. __do_fork(): 자식 쪽 fork 흐름
__do_fork()는 새로 만들어진 자식 thread에서 실행된다. 부모의 레지스터 상태를 복사하고, 자식 전용 pml4를 만든 뒤, 부모의 유저 페이지를 복사한다.
static void
__do_fork (void *aux) {
struct intr_frame if_;
struct fork_args *args = aux;
struct thread *parent = args->parent;
struct thread *current = thread_current ();
memcpy (&if_, &args->parent_if, sizeof (struct intr_frame));
if_.R.rax = 0;
current->pml4 = pml4_create ();
if (current->pml4 == NULL)
goto error;
process_activate (current);
#ifdef VM
supplemental_page_table_init (¤t->spt);
if (!supplemental_page_table_copy (¤t->spt, &parent->spt))
goto error;
#else
if (!pml4_for_each (parent->pml4, duplicate_pte, parent))
goto error;
#endif
process_init ();
args->success = true;
sema_up (&args->fork_sema);
do_iret (&if_);
error:
args->success = false;
sema_up (&args->fork_sema);
thread_exit ();
}
자식의 R.rax를 0으로 바꾸는 이유
fork는 한 번 호출했는데 부모와 자식에게 서로 다른 값을 돌려주는 함수다. 부모는 자식 tid를 받아야 하고, 자식은 0을 받아야 한다. 부모의 intr_frame을 그대로 복사하면 자식도 부모와 같은 반환값을 갖게 되므로, 자식 쪽에서는 if_.R.rax = 0으로 반환값을 직접 바꾼다.
부모
process_fork()의 최종 반환값으로 자식 tid를 받는다.
자식
if_.R.rax = 0 덕분에 fork가 0을 반환한 것처럼 보인다.
7. duplicate_pte(): 부모의 유저 페이지를 자식에게 복사한다
부모와 자식은 같은 가상주소를 볼 수 있어야 한다. 하지만 실제 페이지까지 공유하면 한쪽의 쓰기가 다른 쪽에 영향을 준다. 그래서 자식 전용 페이지를 새로 만들고, 부모 페이지 내용을 복사한 뒤, 같은 가상주소에 연결한다.
static bool
duplicate_pte (uint64_t *pte, void *va, void *aux) {
struct thread *current = thread_current ();
struct thread *parent = (struct thread *) aux;
if (is_kernel_vaddr (va))
return true;
void *parent_page = pml4_get_page (parent->pml4, va);
if (parent_page == NULL)
return false;
void *newpage = palloc_get_page (PAL_USER);
if (newpage == NULL)
return false;
memcpy (newpage, parent_page, PGSIZE);
bool writable = is_writable (pte);
if (!pml4_set_page (current->pml4, va, newpage, writable)) {
palloc_free_page (newpage);
return false;
}
return true;
}
주소공간 복사 그림
va = 0x8048000
parent_page = A
va = 0x8048000
newpage = B
가상주소는 같게 보이지만 실제 페이지는 다르다. 이것이 fork 이후 부모와 자식이 독립적으로 실행될 수 있는 기본 조건이다.
8. thread 쪽 필드: fork가 앞으로 연결해야 할 대상
통합 코드에는 이미 fd_table, children, child_info 필드가 들어와 있다. 이 필드들은 fork 엔진의 다음 연결 지점이다. 단, 필드가 존재한다고 해서 fork가 자동으로 fd와 wait 관계를 상속하는 것은 아니다.
#ifdef USERPROG
uint64_t *pml4;
int exit_status;
struct file **fd_table;
int fd_table_size;
int fd_next;
struct list children;
struct child_info *child_info;
#endif
struct child_info {
tid_t pid;
int exit_status;
bool exited;
bool waited;
struct semaphore wait_sema;
struct list_elem elem;
};
thread 초기화도 같이 맞아야 한다
#ifdef USERPROG
t->pml4 = NULL;
t->exit_status = -1;
t->fd_table = NULL;
t->fd_table_size = 0;
t->fd_next = 2;
list_init (&t->children);
t->child_info = NULL;
#endif
fork 엔진의 다음 작업은 부모의 fd_table을 자식에게 복제하는 것이다. 이때 단순히 포인터 주소만 복사하면 부모와 자식의 close 동작이 꼬일 수 있다. file_duplicate()를 사용해 file object를 복제하는 방향으로 연결해야 한다.
9. 테스트별로 어디까지 내 코드인가
| 테스트 | fork 엔진과의 관계 | 추가로 필요한 것 |
|---|---|---|
fork-once |
부모/자식 분기, R.rax = 0, 주소공간 복사 확인 |
현재 fork 엔진의 핵심 확인 대상 |
fork-read |
자식이 부모의 열린 파일을 읽을 수 있어야 함 | fd table 상속, file duplicate |
fork-close |
부모/자식의 fd close가 서로 잘 분리되어야 함 | fd 복제와 close 정책 |
fork-recursive |
여러 세대의 fork/wait 관계가 이어져야 함 | 정식 child_info, wait(pid), exit status 관리 |
10. 내가 이 파트에서 답할 수 있어야 하는 질문
intr_frame을 복사해야 하는가?R.rax를 0으로 바꾸는가?fork_sema에서 기다려야 하는가?duplicate_pte()에서 건너뛰는가?11. 다음 작업 체크리스트
| 순서 | 작업 | 목표 테스트 |
|---|---|---|
| 1 | fork-once 기준으로 현재 흐름 재검증 |
fork-once |
| 2 | 부모 fd table을 자식 fd table로 복제 | fork-read, fork-close |
| 3 | child_info 기반 wait 구조와 fork 연결 | fork-recursive, wait-* |
| 4 | deny write와 process_exit 정리 흐름 충돌 확인 | rox-* |
마무리하며
이번에 얻은 결론은 “fork 말고 할 게 없다”가 아니었다. fork는 메모리 복사, 실행 문맥, fd 상속, wait 관계가 만나는 중심에 있었다. 그래서 지금 단계에서 필요한 일은 임시 코드를 더 붙이는 것이 아니라, fork 엔진을 정확히 읽고 팀원 코드와 만나는 지점을 표시하는 것이었다. 다음 작업은 이 흐름 위에 fd 상속과 wait 구조를 실제로 연결하는 것이다.
'개발 > 프로젝트' 카테고리의 다른 글
| Pintos Project 2 최종 구현 정리: main branch 코드가 만들어지는 흐름 (1) | 2026.05.07 |
|---|---|
| Pintos Project 2 발표 회고: 핀토스 제국은 어떻게 신민을 통제했는가 (1) | 2026.05.07 |
| Pintos Project 2 중간 과정 기록 (2) | 2026.05.04 |
| Pintos · Project 2 User Programs · 1편 (1) | 2026.05.02 |
| 운영체제 · Pintos · Project 1 Threads · 3편 (1) | 2026.04.29 |