x86-64 기반 Pint OS 프로젝트 2의 System Calls를 구현하기 위해 공부한 개념을 정리한다.
fork
정의
pid_t fork (const char *thread_name);
thread_name을 name으로 가지는, 현재 실행 중인 프로세스의 클론(자식) 프로세스를 만든다. fork 함수는 자식 프로세스의 PID(프로세스 식별자)를 반환해야 한다. 반환 값이 0인 경우는 자식 프로세스에서 실행 중임을 나타낸다. 자식 프로세스는 부모 프로세스의 자원(file descriptor, 가상 메모리 공간 등)을 복제해야 한다. 부모 프로세스는 자식 프로세스가 성공적으로 복제되었는지 알기 전까지 fork 호출에서 반환되지 않는다. 즉, 자식 프로세스가 자원을 복제하는 데 실패한 경우, 부모 프로세스의 fork 호출은 TID_ERROR를 반환해야 한다!
자식 프로세스(스레드)를 만드는 것은 thread_create ()를 써서 바로 만들면 되고, 자원을 복제하는 것이 fork의 주 과제이다. 페이지(메모리 영역)와 fd(file descriptor)를 따로 복사해주어야 한다!
어려웠던 점
fork 로직을 이해하는 것과 자원을 복제하는 것이 가장 어려웠다. Pint OS에서 fork 하기 위한 로직이 준비되어 있는데 꽤나 복잡하게 파고 들어가야 한다. fork 로직은 다음과 같다.
fork() -> process_fork() -> thread_create(__do_fork) -> __do_fork() -> duplicate_pte() -> do_iret()
이 중에서 duplicate_pte ()에서 부모 프로세스의 자원에 대한 복사가 이루어진다. duplicate_pte ()는 말 그대로 pte(page table entry)를 복사하는 함수이다. 페이지 테이블에 관한 단위가 복잡해서 어려웠는데, 하고보니 그냥 페이지만 복사하면 끝이다.
또한 이전 시스템 콜과 달리, fork는 semaphore를 사용해서 상태를 잠글 필요가 있다. 부모 프로세스는 자식 프로세스가 load되거나 wait하는 것을 대기해야 하기 때문이다.
fork ()
pid_t
fork (char *thread_name, struct intr_frame *f) {
return process_fork (thread_name, f);
}
process_fork ()
tid_t
process_fork (const char *name, struct intr_frame *if_) {
struct thread *curr = thread_current ();
memcpy (&curr->parent_if, if_, sizeof (struct intr_frame));
tid_t tid = thread_create (name, PRI_DEFAULT, __do_fork, curr);
if (tid == TID_ERROR)
return TID_ERROR;
struct thread *child = get_child_process (tid);
sema_down (&child->load_sema);
if (child->exit_status == TID_ERROR) {
return TID_ERROR;
}
return tid;
}
- 이 함수는 부모 프로세스에서만 실행된다.
- 자식 프로세스(스레드)를 만들 때, 자원을 복사하기 위해 __do_fork () 함수를 넣어준다.
- 부모 프로세스는 자식을 만든 후, 자식 프로세스의 load_sema를 대기한다.
- 만약, 자식의 load가 실패하면, TID_ERROR를 리턴한다.
__do_fork ()
static void
__do_fork (void *aux) {
struct intr_frame if_;
struct thread *parent = (struct thread *)aux;
struct thread *current = thread_current ();
/* Project 2. */
struct intr_frame *parent_if = &parent->parent_if;
bool succ = true;
/* 1. Read the cpu context to local stack. */
memcpy (&if_, parent_if, sizeof (struct intr_frame));
/* Project 2. */
if_.R.rax = 0;
/* 2. Duplicate PT */
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
/* Project 2. */
for (int i = 0; i < FDT_COUNT_LIMIT; i++) {
struct file *file = parent->fdt[i];
if (file == NULL)
continue;
if (file > 2)
file = file_duplicate (file);
current->fdt[i] = file;
}
current->next_fd = parent->next_fd;
sema_up (¤t->load_sema);
/* Project 2. */
process_init ();
if (succ)
do_iret (&if_);
error:
sema_up (¤t->load_sema);
exit (TID_ERROR);
}
- Project 2. 라고 주석으로 감싸진 코드가 구현한 결과물이다.
- 이 함수는 자식 프로세스의 최초 실행 시, 부모 프로세스의 자원을 복제하기 위해서 실행된다.
- 복제가 끝나고 do_iret (&if_)를 실행하면, 부모 프로세스의 실행 흐름과 같은 상태에서 시작한다.
- 자식 프로세스에서 fork ()의 리턴값은 0이므로,
if_.R.rax = 0;
으로 레지스터에 설정한다. - 페이지(메모리 영역)에 대한 복제는 duplicate_pte ()에서 이루어진다.
- 그 아래에서 fd(file descriptor)에 대한 복제가 이루어진다.
- 복제가 무사히 완료되었으면, load_sema를 풀어줌으로써 부모 프로세스에게 알린다(signal).
duplicate_pte ()
static bool
duplicate_pte (uint64_t *pte, void *va, void *aux) {
struct thread *current = thread_current ();
struct thread *parent = (struct thread *)aux;
void *parent_page;
void *newpage;
bool writable;
if (is_kernel_vaddr (va))
return true;
parent_page = pml4_get_page (parent->pml4, va);
if (parent_page == NULL)
return false;
newpage = palloc_get_page (PAL_USER | PAL_ZERO);
if (newpage == NULL)
return false;
memcpy (newpage, parent_page, PGSIZE);
writable = is_writable (pte);
if (!pml4_set_page (current->pml4, va, newpage, writable)) {
return false;
}
return true;
}
로직은 간단하다.
- 부모 프로세스의 가상 주소에 대한 페이지를 가져온다(pml4_get_page).
- 자식 프로세스의 페이지를 생성한다(palloc_get_page).
- 페이지의 데이터를 복사한다(memcpy).
- 자식 프로세스의 가상 주소에 페이지를 등록한다(pml4_set_page).
wait
정의
int wait (pid_t pid);
pid를 가지는 자식 프로세스를 wait하고, 자식 프로세스의 exit status를 리턴한다. 자식 프로세스가 종료될 때까지 대기(wait)한다.
이전까지는 단순히 반복문을 아주 많이 돌면서 대기했었다면, fork를 구현한 지금은 자식 프로세스의 세마포어를 대기한다. 부모 프로세스와 자식 프로세스 간에 세마포어 시그널을 주고 받으면서 서로의 상태를 확인한다!
wait ()
int
process_wait (tid_t child_tid) {
struct thread *child = get_child_process (child_tid);
if (child == NULL)
return -1;
sema_down (&child->wait_sema);
list_remove (&child->child_elem);
sema_up (&child->exit_sema);
return child->exit_status;
}
- 이 함수는 부모 프로세스에서만 실행된다.
- tid를 가지는 자식 프로세스를 가져온다(get_child_process).
- 자식 프로세스의 wait_sema 시그널을 대기한다.
- 시그널을 받은 후 자식 리스트에서 제거한다.
- 자식 프로세스의 exit_sema를 풀어줌으로써 시그널을 보낸다.
exit 수정
이전에 exit 시스템 콜을 구현했었다. 단순히 thread_exit() 함수를 호출하면서 종료했었는데, thread_exit() 내부에서는 process_exit() 함수를 호출한다. 자식 프로세스가 종료할 때는 자신의 종료 상태를 부모 프로세스에게 알려야 한다. 이후 부모 프로세스에게서 exit_sema 시그널을 받으면 그제서야 종료할 수 있다.이를 추가적으로 구현해주어야 한다. 또한, 할당받은 페이지와 열었던 fd(file descriptor)를 전부 해제해야 한다!
exit ()
void
process_exit (void) {
struct thread *cur = thread_current ();
for (int i = 2; i < FDT_COUNT_LIMIT; i++) {
if (cur->fdt[i] != NULL)
close (i);
}
palloc_free_multiple (cur->fdt, FDT_PAGES);
file_close (cur->running);
process_cleanup ();
sema_up (&cur->wait_sema);
sema_down (&cur->exit_sema);
}
- 이 함수는 자식 프로세스에서 실행된다.
- 열었던 fd와 할당받은 페이지를 전부 해제한다(close, palloc_free_multiple).
- 부모 프로세스에게 wait_sema를 풀었음을 알린다.
- 부모 프로세스에게서 exit_sema가 풀렸음을 받고, 종료한다.
출처
'프로젝트 > Pint OS' 카테고리의 다른 글
[Pint OS] 에러: multi-oom (0) | 2023.06.17 |
---|---|
[Pint OS] System Calls (5) (1) | 2023.06.14 |
[Pint OS] System Calls (3) (0) | 2023.06.11 |
[Pint OS] System Calls (2) (0) | 2023.06.11 |
[Pint OS] 에러: missing "begin" message (0) | 2023.06.09 |