지난 시스템 콜 기능 구현 포스트에서는 exec, fork, wait을 제외한 기능들을 구현했습니다. 관련 내용은 아래 포스트 참고하시길 바랍니다.
https://yunchan97.tistory.com/73
이번에는 exec, fork, wait에 대해서 구현을 시작하겠습니다. 우선 이전 내용들도 구현하기 어려웠지만 이번 포스팅에서 다룰 세 개의 함수들이 이번 시스템콜과제에서 제일 어려웠던 부분이었던 것 같습니다.
1️⃣ int exec(const char *file_name)
- 주어진 인수를 전달하여 현재 프로세스를 cmd_line에 지정된 이름의 실행 파일로 변경합니다.
- 성공하면 반환값은 없고 실패하면 exit(-1)로 종료됩니다.
- 파일 디스크립터는 exec 호출을 통해 변경되지 않고 열린 상태를 유지합니다.
int exec(const char *file_name)
{
check_address(file_name); // file_name 포인터가 유효한지 확인합니다.
char *file_name_copy = palloc_get_page(PAL_ZERO); // 페이지 할당을 통해 파일 이름을 복사할 메모리 공간을 얻습니다.
if (file_name_copy == NULL)
exit(-1); // 메모리 할당에 실패한 경우, -1을 반환하고 현재 프로세스를 종료합니다.
strlcpy(file_name_copy, file_name, PGSIZE); // file_name을 file_name_copy로 복사합니다. PGSIZE는 복사할 최대 크기를 나타냅니다.
if (process_exec(file_name_copy) == -1)
exit(-1); // process_exec 함수를 호출하여 파일을 실행합니다. 실행에 실패한 경우, -1을 반환하고 현재 프로세스를 종료합니다.
}
- 여기서 인자로 받은 file_name을 copy 해서 사용하는 이유는 file_name을 process_exec함수 안에서 parsing 해야 하는데 전달받은 file_name의 형태가 const char* 형태이므로 수정할 수 없습니다. 따라서 수정가능하게 copy 해서 사용하는 것입니다.
- 복사본을 인자로 process_exec 함수를 호출하고, 로드되거나 실행될 수 없는 경우에는 status -1로 프로세스를 종료시킵니다.
2️⃣ tid_t fork(const char *thread_name, struct intr_frame *f)
- 현재 프로세스의 복제본인 새 프로세스를 THREAD_NAME이라는 이름으로 생성합니다.
- 호출 저장 레지스터인 % RBX, % RSP, % RBP, % R12 - % R15를 제외한 레지스터의 값은 복제할 필요가 없습니다.
- 자식 프로세스의 pid를 반환해야 하며, 그렇지 않으면 유효한 pid가 아닐 수 있습니다
- 자식 프로세스에서 반환 값은 0이어야 합니다.
- 자식 프로세스에는 파일 기술자 및 가상 메모리 공간을 포함한 중복된 리소스가 있어야 합니다.
- 부모 프로세스는 자식 프로세스가 성공적으로 복제되었는지 여부를 알기 전까지는 포크에서 반환해서는 안 됩니다.
- 즉, 자식 프로세스가 리소스를 복제하는 데 실패하면 부모의 fork () 호출은 TID_ERROR를 반환해야 합니다.
2-1) fork() 코드
- syscall.c 에 있는 fork() 함수가 실행되면 process_fork()를 호출합니다.
tid_t fork(const char *thread_name, struct intr_frame *f)
{
// 현재 프로세스를 포크하여 새로운 자식 프로세스를 생성하는 process_fork 함수를 호출하고 그 결과를 반환합니다.
return process_fork(thread_name, f);
}
2-2) process_fork() 코드
- 스레드 구조체에 parent_if 구조체를 추가합니다.
struct thread
{
...
struct intr_frame parent_if;
...
}
- 현재 스레드의 parent_if에 if를 복사합니다.
// 현재 스레드의 parent_if에 복제해야 하는 if를 복사한다.
struct thread *cur = thread_current();
memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));
- 현재 스레드를 fork 한 new 스레드를 생성합니다.
// 현재 스레드를 fork한 new 스레드를 생성한다.
tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, cur);
if (pid == TID_ERROR)
return TID_ERROR;
- 자식 관계 설정을 위한 구조체를 추가하고 새로 생긴 list를 init_thread에서 초기화해 줍니다.
struct thread
{
...
struct intr_frame parent_if;
struct list child_list;
struct list_elem child_elem;
...
}
- 이제 스레드는 thread_create로 생성되고 ready_list에 들어간 상태다. 이 스레드가 스케줄링되어서 실행되면 thread_create 함수에 넣어준 __do_fork 함수가 호출되어 load가 진행된다.
- 부모가 이 load가 완료될 때까지 대기하기 위해 semaphore를 활용한다.
- 우선 semaphore관련 부분을 thread 구조체에 추가해 줍니다.
struct thread
{
...
struct semaphore load_sema;
struct semaphore exit_sema;
struct semaphore wait_sema;
...
}
- 자식이 로드될 때까지 대기하기 위해서 방금 생성한 자식 스레드를 찾는다.
struct thread *child = get_child_process(pid);
// 현재 스레드는 생성만 완료된 상태이다. 생성되어서 ready_list에 들어가고 실행될 때 __do_fork 함수가 실행된다.
// __do_fork 함수가 실행되어 로드가 완료될 때까지 부모는 대기한다.
sema_down(&child->load_sema);
- 자식을 찾는 get_child_process함수.
// 자식 리스트에서 원하는 프로세스를 검색하는 함수
struct thread *get_child_process(int pid)
{
struct thread *cur = thread_current(); // 현재 스레드를 가져옵니다.
struct list *child_list = &cur->child_list; // 자식 스레드들을 저장하는 리스트를 가져옵니다.
// 자식 리스트를 순회하며 원하는 PID를 가진 자식 스레드를 검색합니다.
for (struct list_elem *e = list_begin(child_list); e != list_end(child_list); e = list_next(e))
{
struct thread *t = list_entry(e, struct thread, child_elem);
if (t->tid == pid) // 자식 스레드의 TID가 찾고자 하는 PID와 일치하는 경우
return t; // 해당 자식 스레드를 반환합니다.
}
return NULL; // 자식 스레드를 찾지 못한 경우 NULL을 반환합니다.
}
- process_fork 전체 코드
tid_t process_fork(const char *name, struct intr_frame *if_ UNUSED)
{
struct thread *cur = thread_current(); // 현재 스레드를 가져옵니다.
// 현재 스레드의 parent_if에 if_가 가리키는 데이터를 복사합니다.
memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));
tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, cur); // 새로운 스레드를 생성하여 fork 작업을 수행합니다.
if (pid == TID_ERROR)
return TID_ERROR; // 새로운 스레드 생성에 실패한 경우, TID_ERROR를 반환합니다.
struct thread *child = get_child_process(pid); // 생성한 자식 스레드를 가져옵니다.
sema_down(&child->load_sema); // 자식 스레드가 load_sema를 획득할 때까지 대기합니다.
return pid; // 생성한 자식 스레드의 TID를 반환합니다.
}
2-3) __do_fork() 코드
- 부모 스레드의 parent_if값을 parent_if에 저장한다.
/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
struct intr_frame *parent_if = &parent->parent_if;
- memcpy를 사용하여 parent_if가 가리키는 메모리 블록의 데이터를 if_가 가리키는 메모리 블록으로 복사하는 작업을 수행합니다.
- rax가 리턴값을 의미하므로 자식 프로세스의 리턴값을 0으로 지정합니다.
/* 1. Read the cpu context to local stack. */
memcpy (&if_, parent_if, sizeof (struct intr_frame));
if_.R.rax = 0;
- 파일 디스크립터 테이블의 파일을 복제합니다.
/* TODO: Your code goes here.
* TODO: Hint) To duplicate the file object, use `file_duplicate`
* TODO: in include/filesys/file.h. Note that parent should not return
* TODO: from the fork() until this function successfully duplicates
* TODO: the resources of parent.*/
// FDT 복사
for (int i = 0; i < FDCOUNT_LIMIT; i++)
{
struct file *file = parent->fdt[i];
if (file == NULL)
continue;
if (file > 2)
file = file_duplicate(file); // 파일 객체를 복제하여 자식 프로세스의 FDT에 저장합니다.
current->fdt[i] = file; // 복제한 파일 객체를 자식 프로세스의 FDT에 할당합니다.
}
current->fdidx = parent->fdidx; // 자식 프로세스의 fdidx 값을 부모 프로세스의 fdidx 값으로 설정합니다.
- 로드가 완료되면 기다리고 있던 부모를 sema_up 시켜서 대기를 해제시켜 줍니다.
// 로드가 완료될 때까지 기다리고 있던 부모 대기 해제
sema_up(¤t->load_sema);
- __do_fork() 전체 코드
static void
__do_fork (void *aux) {
struct intr_frame if_;
struct thread *parent = (struct thread *) aux;
struct thread *current = thread_current ();
/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
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));
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
/* TODO: Your code goes here.
* TODO: Hint) To duplicate the file object, use `file_duplicate`
* TODO: in include/filesys/file.h. Note that parent should not return
* TODO: from the fork() until this function successfully duplicates
* TODO: the resources of parent.*/
// FDT 복사
for (int i = 0; i < FDCOUNT_LIMIT; i++)
{
struct file *file = parent->fdt[i];
if (file == NULL)
continue;
if (file > 2)
file = file_duplicate(file); // 파일 객체를 복제하여 자식 프로세스의 FDT에 저장합니다.
current->fdt[i] = file; // 복제한 파일 객체를 자식 프로세스의 FDT에 할당합니다.
}
current->fdidx = parent->fdidx; // 자식 프로세스의 fdidx 값을 부모 프로세스의 fdidx 값으로 설정합니다.
// 로드가 완료될 때까지 기다리고 있던 부모 대기 해제
sema_up(¤t->load_sema);
process_init();
/* Finally, switch to the newly created process. */
if (succ)
do_iret (&if_);
error:
sema_up(¤t->load_sema);
exit(TID_ERROR);
}
2-4) 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;
/* 1. TODO: If the parent_page is kernel page, then return immediately. */
if (is_kernel_vaddr(va))
return true;
/* 2. Resolve VA from the parent's page map level 4. */
parent_page = pml4_get_page(parent->pml4, va);
if (parent_page == NULL)
return false;
/* 3. TODO: Allocate new PAL_USER page for the child and set result to
* TODO: NEWPAGE. */
newpage = palloc_get_page(PAL_USER | PAL_ZERO);
if (newpage == NULL)
return false;
/* 4. TODO: Duplicate parent's page to the new page and
* TODO: check whether parent's page is writable or not (set WRITABLE
* TODO: according to the result). */
memcpy(newpage, parent_page, PGSIZE);
writable = is_writable(pte);
/* 5. Add new page to child's page table at address VA with WRITABLE
* permission. */
if (!pml4_set_page(current->pml4, va, newpage, writable))
{
/* 6. TODO: if fail to insert page, do error handling. */
return false;
}
return true;
}
2-5) load() 코드
- 현재 실행 중인 파일을 수정하는 일을 막기 위한 작업을 추가해줘야 합니다.
- 실행 중인 파일에 대한 쓰기 작업을 거부하는 코드를 추가합니다.
static bool
load(const char *file_name, struct intr_frame *if_)
{
// ...
t->running = file; // 현재 스레드의 실행 중인 파일을 설정합니다.
file_deny_write(file); // 파일에 대한 쓰기 권한을 거부합니다.
/* Set up stack. */
if (!setup_stack (if_)) // 스택을 설정합니다.
goto done;
/* Start address. */
if_->rip = ehdr.e_entry; // 프로그램의 시작 주소를 설정합니다.
/* TODO: Your code goes here.
* TODO: Implement argument passing (see project2/argument_passing.html). */
success = true;
done:
/* We arrive here whether the load is successful or not. */
// file_close (file);
return success;
}
2-6) process_exit() 코드
- 프로세스가 종료될 때 실행 중인 파일을 닫습니다.
void
process_exit (void) {
struct thread *t = thread_current ();
/* TODO: Your code goes here.
* TODO: Implement process termination message (see
* TODO: project2/process_termination.html).
* TODO: We recommend you to implement process resource cleanup here. */
for (int i = 2; i < FDCOUNT_LIMIT; i++)
close(i);
palloc_free_multiple(t->fdt, FDT_PAGES);
file_close(t->running); // 2) 현재 실행 중인 파일도 닫는다.
process_cleanup();
}
3️⃣ int wait(int pid)
- 자식 프로세스 pid를 기다렸다가 자식의 종료 상태를 검색합니다.
- pid가 아직 살아있다면 종료될 때까지 기다립니다. 그런 다음 pid가 종료하기 위해 전달한 상태를 반환합니다.
- pid가 exit()를 호출하지 않았지만 커널에 의해 종료된 경우(예: 예외로 인해 종료된 경우) wait(pid)는 -1을 반환해야 합니다.
- 부모 프로세스가 wait를 호출할 때 이미 종료된 자식 프로세스를 기다리는 것은 완전히 합법적이지만, 커널은 여전히 부모가 자식의 종료 상태를 검색하거나 자식이 커널에 의해 종료되었음을 알 수 있도록 허용해야 합니다.
3-1) process_wait() 코드
- get_child_process 함수를 통해 인자로 받은 tid를 갖는 자식이 있으면 실행하고 없으면 -1을 반환하고 종료합니다.
- 자식이 종료될 때까지 대기합니다.
- 자식이 종료된 signal을 받으면 현재 스레드의 자식 리스트에서 제거합니다.
- 자식이 완전히 종료되고 자식에게 signal을 보내줍니다.
int
process_wait(tid_t child_tid UNUSED) {
/* XXX: Hint) The pintos exit if process_wait (initd), we recommend you
* XXX: to add infinite loop here before
* XXX: implementing the process_wait. */
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); // 자식 스레드의 종료를 알리기 위해 exit_sema를 올립니다.
return child->exit_status; // 자식 스레드의 종료 상태를 반환합니다.
}
3-2) process_exit() 코드
void
process_exit(void) {
struct thread *t = thread_current();
/* TODO: Your code goes here.
* TODO: Implement process termination message (see
* TODO: project2/process_termination.html).
* TODO: We recommend you to implement process resource cleanup here. */
// 파일 디스크립터 닫기
for (int i = 2; i < FDCOUNT_LIMIT; i++)
close(i);
// 파일 디스크립터 테이블 해제
palloc_free_multiple(t->fdt, FDT_PAGES);
// 실행 중인 파일 닫기
file_close(t->running);
// 프로세스 정리
process_cleanup();
// 부모 스레드가 자식 스레드의 종료를 확인할 수 있도록 wait_sema를 올립니다.
sema_up(&t->wait_sema);
// 자식 스레드가 종료될 때까지 대기합니다.
sema_down(&t->exit_sema);
}