크래프톤 정글/TIL

PintOS 프로젝트 2주차 [User Programs / System Call] (2)

양선규 2024. 6. 1. 21:17
728x90
반응형

95개의 테스트 케이스 중 15개를 실패한 코드입니다. 주의해서 참고하세요.

pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
FAIL tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
FAIL tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
pass tests/userprog/fork-once
pass tests/userprog/fork-multiple
pass tests/userprog/fork-recursive
pass tests/userprog/fork-read
pass tests/userprog/fork-close
pass tests/userprog/fork-boundary
pass tests/userprog/exec-once
pass tests/userprog/exec-arg
pass tests/userprog/exec-boundary
pass tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
pass tests/userprog/exec-read
pass tests/userprog/wait-simple
pass tests/userprog/wait-twice
pass tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
pass tests/userprog/multi-recurse
pass tests/userprog/multi-child-fd
pass tests/userprog/rox-simple
FAIL tests/userprog/rox-child
FAIL tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
pass tests/filesys/base/lg-create
FAIL tests/filesys/base/lg-full
FAIL tests/filesys/base/lg-random
FAIL tests/filesys/base/lg-seq-block
FAIL tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
FAIL tests/filesys/base/sm-full
FAIL tests/filesys/base/sm-random
FAIL tests/filesys/base/sm-seq-block
FAIL tests/filesys/base/sm-seq-random
FAIL tests/filesys/base/syn-read
FAIL tests/filesys/base/syn-remove
FAIL tests/filesys/base/syn-write
pass tests/userprog/no-vm/multi-oom
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
15 of 95 tests failed.

 

https://yskisking.tistory.com/244

 

PintOS 프로젝트 2주차 [User Programs / System Call] (1)

95개의 테스트 케이스 중 15개를 실패한 코드입니다. 주의해서 참고하세요.pass tests/userprog/args-nonepass tests/userprog/args-singlepass tests/userprog/args-multiplepass tests/userprog/args-manypass tests/userprog/args-dbl-sp

yskisking.tistory.com

 

저번 글과 이어지는 글이다. 이번 글에선 fork()부터 구현해 보도록 하겠다.

 

 

fork() 시스템 콜은, 현재 프로세스를 복제하여 새로운 프로세스를 생성한다.

아래는 GitBook의 fork에 대한 설명이다.

 

%RBX, %RSP, %RBP,  %R12 - %R15(얘네들은 callee-saved register(피호출자 함수에 저장하는 레지스터)) 를 제외한 나머지 레지스터값은 복제할 필요가 없다. 반드시 자식 프로세스의 process id를 반환해라. 그렇지 않으면 유효한 pid가 아니다. 자식 프로세스에서, 리턴값은 반드시 0이어야 한다. 자식 프로세스는 반드시 부모 프로세스로부터 파일 디스크립터, 가상 메모리 공간 등을 포함한 자원을 복사해와야 한다. 부모 프로세스는 자식 프로세스가 성공적으로 복제된 것을 알고 나서 fork로부터 리턴해야 한다. 즉, 만약 자식 프로세스가 자원을 복제하는데 실패하면 부모 프로세스의 fork() 호출은 반드시 TID_ERROR를 반환해야 한다. 

이 템플릿은 threads/mmu.c 내에 있는 pml4_for_each() 를 사용해 전체 user memory 공간을 복제하는데, 대응하는 페이지 테이블 구조체를 포함한다. 하지만 빈 부분(pte_for_each_func에서)을 채워야 한다.

 

fork()가 이루어지기 위해선 레지스터, 파일 디스크립터, 메모리 값 등등 다양한 데이터를 부모 프로세스로부터 복제해야 한다.

또한, 부모 프로세스는 자식 프로세스의 복제 과정이 끝나기를 기다려야 하는데, 이유는 다음과 같다.

 

1. 데이터 일관성과 동기화

- 부모 프로세스의 메모리와 자원을 복제하는 과정에서 부모 프로세스의 상태가 변하지 않아야 한다.

- 복제가 완전히 완료된 후 자식 프로세스가 실행되어야 한다.

- 복제 과정 중에 부모 프로세스가 계속 실행된다면, 복제되어야 하는 데이터가 변경될 수 있으므로 데이터 일관성을 해칠 수 있다

2. 자식 프로세스 상태 확인

- process_fork 함수를 통해 부모를 복제하여 자식을 만들게 되는데, 복제 성공 여부에 따라서 적절한 리턴값을 확인하여 필요할 경우 오류 처리를 해야 한다. ( 성공 시 부모에게 자식의 pid 리턴, 실패 시 ERROR 리턴 )

3. POSIX 표준 준수

- POSIX 표준에 따르면 fork 시스템 콜은 성공 시 부모에게 자식의 pid를, 자식에게 0을 반환해야 한다. 

- 이를 위해서는 부모가 자식의 복제 과정(thread_create + __do_fork)이 끝나기를 기다렸다가 자식의 pid를 받아가야 한다.

 

syscall.c

tid_t fork(const char *thread_name, struct intr_frame *f) {
   
    return process_fork(thread_name, f);
}

 

fork 시스템 콜이다.

우리는 process_fork를 파고 들어가며 다양한 함수들을 수정해야 한다.

 

thread_name은 자식 프로세스의 이름이 되고, 인터럽트 프레임 f에는 복제해야 할 부모 프로세스가 실행중이던 다양한 정보들이 담겨있다. 

 

 

process.c

tid_t
process_fork (const char *name, struct intr_frame *if_ UNUSED) {
    /* Clone current thread to new thread.*/

    /* --- Project 2: system call --- */
    // if에 담긴 현재 CPU 상태, 즉 실행중이던 부모 프로세스 context들을 복사한다.
    // if를 직접 넘기면 race condition이나 동기화 측면의 문제가 발생할 수 있기 때문에,
    // 부모 스레드의 parent_if 필드에 if를 복사하여 부모 스레드를 __do_fork의 인자로 넘겨준다.
    struct thread *parent = thread_current();
    memcpy(&parent->parent_if, if_, sizeof(struct intr_frame));

    // 전달받은 thread_name으로 스레드 생성
    // 복제된 if를 가지고 있는 parent를 인자로 넣어 __do_fork() 진행
    // 자식 프로세스가 running되면 즉시 __do_fork를 실행하게 된다.
    tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, parent);

    // 스레드 생성에 실패했을 경우 TID_ERROR 리턴
    if (pid == TID_ERROR) {
        return TID_ERROR;
    }

    // 부모 스레드의 child_list에서, pid를 이용해 방금 생성한 자식 스레드 가져오기
    struct thread *child = get_child(pid);

    // 자식 프로세스가 실행하는 __do_fork가 끝나기를 기다린다.
    // if 복제가 완료되면, __do_fork 함수에서 자식 프로세스가 sema_up을 해준다.
    sema_down(&child->fork_sema);

    // 여기부터는 자식 프로세스의 __do_fork가 완료된 후 진행된다
    if (child->exit_status == -1)
    {
        return TID_ERROR;
    }
    // fork할 때 부모에게는 자식의 PID를, 자식에게는 0을 리턴하는 것이 POSIX 표준이다.
    return pid;
}

 

실질적으로 fork를 진행하는 process_fork 함수이다.

 

if 정보를 직접 넘기면, 부모 프로세스가 계속 실행되면서 값이 변할 수 있기 때문에 부모 스레드의 parent_if 필드에 if 값을 복사하여 부모 스레드 자체를 __do_fork의 인자로 넘긴다. __do_fork 에서는 parent->parent_if 형식으로 if를 가져와 복제할 것이다.

 

thread_create를 통해 자식 프로세스가 생성되면 ready queue에 들어가게 되고 곧 실행될 것이다. 부모 프로세스는 sema_down을 통해서 자식 프로세스가 running된 후 __do_fork 작업을 마칠 때 까지 기다리게 된다.

__do_fork가 완료되면 자식 프로세스가 sema_up을 해 주고, process_fork 함수는 자식 프로세스의 pid를 리턴한다. 만약 복제가 실패했다면 TID_ERROR를 리턴하게 된다.

 

 

thread.c

/* ----------- added for Project.2 ----------- */
  t->exit_status = 0;
  t->running = NULL;
  list_init(&t->child_list);
  sema_init(&t->fork_sema, 0);
  sema_init(&t->wait_sema, 0);
  sema_init(&t->free_sema, 0);
  /* ------------------------------------------- */

 

init_thread 함수이다. fork에서 쓸 semaphore를 미리 초기화해 두어야 한다.

나중에 쓸 wait_sema와 free_sema도 함께 초기화해 주자.

 

 

thread.c

  struct thread *cur = thread_current();
  list_push_back(&cur->child_list, &new_t->child_elem);

 

thread_create 함수이다.

프로세스를 생성할 때 현재 프로세스의 child list에 자식 프로세스를 넣어주는 코드를 추가해 주자.

 

 

process.c

// pid에 해당하는 현재 스레드의 자식 스레드 반환
struct thread
*get_child(int pid) {

    // 현재 스레드의 child list 가져오기
    struct thread *cur = thread_current ();
    struct list *child_list = &cur->child_list;
   
    // 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) {
            return t;
        }
    }
   
    // child list에 pid에 해당하는 스레드가 없다면 NULL을 반환한다
    return NULL;
}

 

process_fork에서 사용하는 get_child 함수이다.

pid를 받아서, 현재 스레드의 child list에서 pid에 해당하는 자식 스레드를 리턴한다.

 

 

process.c

static void
__do_fork(void *aux)
{
    /* 자식 프로세스는 running 되자마자 바로 __do_fork를 실행한다!!*/

    struct intr_frame if_;
    // 인터럽트 프레임이 aux의 parent_if 필드에 복사되어 있다.
    struct thread *parent = (struct thread *)aux;
    struct thread *current = thread_current();
   
    struct intr_frame *parent_if;
    bool succ = true;
   
    // 복사해서 받아온 if를 parent_if 포인터 변수에 담는다.
    parent_if = &parent->parent_if;

#ifdef DEBUG
    printf("[Fork] Forking from %s to %s\n", parent->name, current->name);
#endif

    // 부모 인터럽트 프레임을 자식 프로세스로 복사한다
    memcpy(&if_, parent_if, sizeof(struct intr_frame));

    // 자식 프로세스 리턴값을 0으로 설정한다
    // fork할 때 부모에게는 자식의 PID를, 자식에게는 0을 리턴하는 것이 POSIX 표준이다.
    if_.R.rax = 0;

    /* 2. Duplicate PT */
    // 자식 프로세스를 위한 페이지 테이블 생성 및 할당
    current->pml4 = pml4_create();
    if (current->pml4 == NULL)
        goto error;

    // 페이지 테이블을 CPU의 테이블 레지스터에 로드하고 TSS를 업데이트한다
    // TSS : 태스크 상태 세그먼트
    // 태스크 : 프로세스, 스레드 같은 실행 단위를 포괄적으로 지칭
    // 이 부분에서는 스택 포인터 관련 정보를 업데이트한다
    process_activate(current);

#ifdef VM
    supplemental_page_table_init(&current->spt);
    if (!supplemental_page_table_copy(&current->spt, &parent->spt))
        goto error;
#else
    // 부모 페이지 테이블의 PTE를 순회하며, 현재 자식 스레드 페이지 테이블로 복제한다
    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.*/

   
    if (parent->fd_idx == FDT_COUNT_LIMIT)
        goto error;

    // fd table 복제
    for (int i = 0; i < FDT_COUNT_LIMIT; i++)
    {
        // 부모의 fd table에서 파일을 가져온다
        struct file *file = parent->fd_table[i];
        if (file == NULL)
            continue;
        // if 'file' is already duplicated in child don't duplicate again but share it
        bool found = false;
        if (!found)
        {
            // 부모 fd table의 file을 new_file에 복제한다
            struct file *new_file;
            if (file > 2)
                new_file = file_duplicate(file);
            else
                new_file = file;
           
            // 복제한 file을 자식 프로세스 fd table에 그대로 할당한다
            current->fd_table[i] = new_file;
        }
    }

    // 부모의 fd_idx도 복제한다
    current->fd_idx = parent->fd_idx;

#ifdef DEBUG
    printf("[do_fork] %s Ready to switch!\n", current->name);
#endif

    // 부모는 자식 스레드를 생성해 놓고 sema_down으로 if 복제, 즉 __do_fork가 끝나기를 기다리고 있었다.
    // 복제가 완료되었으니 sema_up을 해서 부모의 process_fork 함수가 이어서 진행되도록 한다.
    sema_up(&current->fork_sema);

    /* Finally, switch to the newly created process. */
    if (succ)
        do_iret(&if_);
error:
    // thread_exit();
    // project 2 : system call

    // 에러가 났을 경우 exit_status를 ERROR로 설정하고 sema_up 해준 후 프로세스를 종료한다.
    current->exit_status = TID_ERROR;
    sema_up(&current->fork_sema);
    exit(TID_ERROR);
}

 

__do_fork 함수이다. 자식 프로세스가 running되면 가장 먼저 이 함수를 실행하게 되며, 여기서 부모 프로세스 정보를 실제로 복제하는 작업을 진행한다.

 

부모 프로세스 if에 있는 메모리 값, 레지스터 정보들을 복제한 후 자식 프로세스에게 페이지 테이블을 할당한다. 이후 process_activate를 통해 복제한 값들을 실제로 CPU에 올리고 자식 프로세스가 실행될 수 있는 준비를 한다.

마지막으로 파일 디스크립터 테이블까지 복제하며, fd_idx 값도 복제한다.

 

복제 작업이 완료되면, sema_up을 통해 부모 프로세스가 진행하던 process_fork 함수가 이어서 진행되도록 한다.

주석을 상세히 달았으니 이해하기 어렵지 않을 거라고 생각한다.

 

 

process.c

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;

    // 주어진 가상 주소(va)가 커널 영역에 있다면 즉시 true를 리턴한다.
    // 커널 페이지는 복사할 필요가 없기 때문 -> 커널 영역은 모든 프로세스에 의해 공유되는 메모리 영역이다
    if (is_kernel_vaddr(va))
    {
        return true;
    }

    // 부모의 페이지 테이블에서 va와 매핑되는 실제 페이지를 찾는다. 실제 물리 메모리 주소.
    // 만약 없다면 false를 리턴한다
    parent_page = pml4_get_page(parent->pml4, va);
    if (parent_page == NULL)
    {
        printf("[fork-duplicate] failed to fetch page for user vaddr 'va'\n");
        return false;
    }

#ifdef DEBUG
    // pte: address pointing to one page table entry
    // *pte: page table entry = address of the physical frame
    void *test = ptov(PTE_ADDR(*pte)) + pg_ofs(va); // should be same as parent_page -> Yes!
    uint64_t va_offset = pg_ofs(va);                // should be 0; va comes from PTE, so there must be no 12bit physical offset
#endif
   
    // 자식 프로세스(지금 running 중이다)를 위한 페이지를 할당한다
    // PAL_USER는 유저 영역에 페이지를 할당하도록 하는 플래그이다
    newpage = palloc_get_page(PAL_USER);
    if (newpage == NULL)
    {
        printf("[fork-duplicate] failed to palloc new page\n");
        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). */

    // 부모 페이지를(PTE, 4096byte) newpage에 복사한다
    memcpy(newpage, parent_page, PGSIZE);

    // 부모 페이지 쓰기 가능 여부를 검사하여 writable에 결과를 저장한다 (boolean)
    writable = is_writable(pte);


    /* 5. Add new page to child's page table at address VA with WRITABLE
     *    permission. */
    // 자식 프로세스 페이지 테이블에 새로운 페이지를(PTE) 추가한다
    // va, writable을 설정하여 새로운 페이지 추가
    if (!pml4_set_page(current->pml4, va, newpage, writable))
    {
        /* 6. TODO: if fail to insert page, do error handling. */
        // 실패하면 false 반환
        printf("Failed to map user virtual page to given physical frame\n");
        return false;
    }

#ifdef DEBUG
    // TEST) is 'va' correctly mapped to newpage?
    if (pml4_get_page(current->pml4, va) != newpage)
        printf("Not mapped!"); // never called

    printf("--Completed copy--\n");
#endif

    return true;
}

 

 

 

duplicate_pte 함수이다. __do_fork 페이지 복제 부분의 pml4_for_each()의 3번째 인자로 들어가는데, 실제로 페이지를 복제하는 것은 여기서 이루어진다. 부모 프로세스의 페이지 테이블 엔트리(PTE)마다 duplicate_pte가 실행되어 부모의 PTE를 자식 프로세스로 복제한다.

 

커널 영역은 모든 프로세스가 공유하기 때문에 커널 영역일 경우 즉시 true를 리턴한다.

부모 페이지 테이블에서 va와 매핑되는 실제 물리 메모리 주소를 찾되, 없으면 false를 리턴한다.

이후 부모 PTE를 복사할 newpage 변수에 페이지를 할당받은 후 복사한다. writable 여부(쓰기가능여부)도 확인하여 저장해 둔다.

마지막으로 pml4_set_page 함수를 이용해 자식 프로세스의 페이지 테이블(pml4)에 PTE를 추가한다.

// 부모 페이지 테이블의 PTE를 순회하며, 현재 자식 스레드 페이지 테이블로 복제한다
    if (!pml4_for_each(parent->pml4, duplicate_pte, parent))
        goto error;

 

__do_fork의 이 부분에서 부모의 PTE를 전부 순회하며 duplicate_pte 함수로 PTE를 전부 복제하는 것이다.

이렇게 fork는 끝났다.

 

 

syscall.c

int exec(char *file_name)
{
    check_address(file_name);

    // file_name의 길이를 구한다.
    // strlen은 널 문자를 포함하지 않기 때문에 널 문자 포함을 위해 1을 더해준다.
    int file_name_size = strlen(file_name) + 1;

    // 새로운 페이지를 할당받고 0으로 초기화한다.(PAL_ZERO)
    // 여기에 file_name을 복사할 것이다
    char *fn_copy = palloc_get_page(PAL_ZERO);
    if (fn_copy == NULL)
    {
        exit(-1);
    }

    // file_name 문자열을 file_name_size만큼 fn_copy에 복사한다
    strlcpy(fn_copy, file_name, file_name_size);

    // process_exec 호출, 여기서 인자 파싱 및 file load 등등이 일어난다.
    // file 실행이 실패했다면 -1을 리턴한다.
    if (process_exec(fn_copy) == -1)
    {
        return -1;
    }

    NOT_REACHED();
    return 0;
}

 

exec 시스템 콜이다. command line으로 받은 명령어를 인자로 받아서 파일을 실행하는 역할을 한다.

입력값으로 들어오는 file_name은 "/bin/ls -l foo bar" 이런 식의 명령어(파일명)와 인자로 이루어져 있다.

 

 process_exec가 정상적으로 실행되면, 즉 파일이 열리면 다시 exec로 리턴하지 않는다. 그래서 성공 시 리턴값이 필요가 없다. 그 이유는 파일 로딩이 성공하면 CPU의 실행 컨텍스트(레지스터 상태, 프로그램 카운터 등)가 새로운 프로그램 시작 지점으로 변경되고, 코드의 실행 흐름을 새로운 프로그램으로 전환시키기 때문이다.

 

근데 마지막에 return 0이 있긴 한데, 어차피 그 위에 NOT_REACHED()가 있어서 사실상 없는 거라고 봐도 무방하다. NOT_REACHED는 정상적인 프로그램 흐름이었을 경우 절대 도달해서는 안될 곳을 의미하니까.

 

 

syscall.c

 
int wait (tid_t pid)
{
    // pid에 해당하는 자식 프로세스가 종료되기를 기다린다.
    process_wait(pid);
}

 

wait() 시스템 콜은 process_wait()를 호출하며, pid에 해당하는 자식 프로세스가 종료되기를 기다리게 된다.

process_wait와 process_exit는 밀접하게 관련되어 있어서, 코드를 한번에 올리겠다.

 

 

process.c

int process_wait(tid_t child_tid UNUSED)
{  
    // tid에 해당하는 자식 스레드를 가져온다.
    struct thread *child = get_child(child_tid);

    // 자식 스레드가 아닌 경우 return -1
    if (child == NULL)
        return -1;

    // 자식 프로세스가 끝날 때 까지 잠든다.(BLOCKED 되어 있는다)
    sema_down(&child->wait_sema);

    /// 자는 중 ///
    /// 자는 중 ///

    // 여기서부터는 깨어났다.
    // 자식 프로세스 측에서 끝낼 준비를 다 했다는 의미로, process_exit 함수에서 sema_up을 해 주었다.
    // 자식은 부모가 자신을 child_list에서 지우기고 exit status를 return 하는 것을 기다리기 위해 sema_down 하여 자고 있다.
    // 자식 프로세스를 child_list에서 지우기
    list_remove(&child->child_elem);

    // child_list에서 지웠으므로, 이제 자식이 종료될 수 있도록 sema_up을 해 준다.
    sema_up(&child->free_sema);

    // 자식 프로세스 exit status를 return 한다.
    return child->exit_status;
}

/* Exit the process. This function is called by thread_exit (). */
void process_exit(void)
{
    // 프로세스 종료를 위한 정리 작업을 하는 함수
    // exit 시스템 콜에서 이미 exit status는 설정되었다.
    // 설정된 이후 thread_exit()를 통해 process_exit()가 호출된 것이다.

    // fd table에 할당되어 있는 열린 파일들을 모두 닫는다.
    struct thread *cur = thread_current();
    for (int i = 2; i < FDT_COUNT_LIMIT; i++)
    {
        // close 시스템 콜
        close(i);
    }
   
    // 메모리 누수 방지를 위해 fd table을 할당 해제한다.
    palloc_free_multiple(cur->fd_table, FDT_PAGES);

    // 이제 끝낼 준비가 되었다.
    // 따라서 process_wait()에서 자식이 끝날 때 까지 자고 있는 부모를 깨워준다.
    sema_up(&cur->wait_sema);

    // process_wait()에서 부모가 child_list에서 자식을 제거한 후 exit status를 return할 수 있도록 sema_down으로 자고 있는다.
    sema_down(&cur->free_sema);

    /// 자는 중 ///
    /// 자는 중 ///

    // 부모는 작업을 마쳤다. 스레드의 페이지 테이블을 할당 해제한다.
    // 나머지 세부적인 종료 절차들은 운영체제가 수행한 후 종료된다.
    process_cleanup();
}

 

process_wait() 함수를 통해 부모 프로세스는 자식 프로세스가 종료되기를 기다리며, 자식 프로세스가 종료될 준비를 마칠 때 까지 sema_down 을 통해서 BLOCKED된 상태로 기다린다.

 

자식 프로세스가 process_exit() 함수에서 파일 디스크럽터 및 메모리 반환 같은 종료 준비 작업을 마치면 sema_up을 통해서 부모를 깨운다. 그리고 다시 부모가 child_list에서 자신을 제거하고 exit status를 리턴하는 작업을 마칠 수 있도록 sema_down을 통해 기다리고 있는다.

 

부모는 일어나서 process_wait()을 이어서 실행하며, 자신의 child_list에서 자식을 제거하고 sema_up으로 자고 있는 자식을 깨운 후 자식의 exit status를 리턴한다. 

 

마지막으로 자식은 process_cleanup() 함수로 자신의 페이지 테이블을 반환하고 process_exit() 함수는 종료된다. 이후 상세한 종료 절차는 운영체제에 의해 진행된 후 프로세스는 종료된다.

 

 

========================

 

이번 Project 2 User Program은 정말 매우 많이 어려웠다. 도저히 구현에 대한 감이 안 잡혀서 다른 사람이 정리해 둔 정답 코드들을 엄청 많이 참고했다. 하지만 분명 정답 코드를 보고 따라 쳤는데도 테스트가 전부 통과되지 않았다. 디버깅도 너무 어렵고, 어디가 문제인 지 알아도 뭘 어떻게 고쳐야 할 지도 모르겠고. User Program은 10일동안 진행되었는데, 정말 아무것도 안 남고 시간만 낭비한 기분이 들었었다. 

 

이렇게 하는 게 무슨 의미인가 싶었고... 뭐라도 남기기 위해, 확실하게 이해하기 위해 Project 3이 시작된지 3일차인 지금까지 User Program 내용을 하나하나 이해하며 정리했다. 그랬더니 이제서야 흐름이 보이고, 내가 지난 10일 간 무엇을 했는지를 깨달을 수 있었다. 동료들은 대부분 Project 3를 서로 열심히 토론해가며 진행하고 있는데, 지금까지 저번주 프로젝트를 정리만 한 나는 무슨 소리인지 하나도 모르겠다.

 

하지만 이게 나에게 맞는 방향인 것 같다. 남들이 먼저 앞서나가는 것 같아 조금 불안하더라도... 진도를 조금 못 나가더라도... 공부했던 걸 복습하고 기록해서 확실히 내 것으로 만드는 것이 장기적으로 나에게 훨씬 이득이라고 생각했다.

 

Project 3 Virtual Memory는 굉장히 어렵다고 하던데, Project 2도 제대로 못 했던 내가 잘 할 수 있을지 모르겠다. 저번주엔 급하게 쫓기듯이 구현한 느낌이 있는데, 이번엔 그냥 내 방식대로 확실히 이해해가며 천천히 진행하려고 한다. 

 

728x90
반응형