[Pintos-KAIST] Project 2 :: System Calls - 2 (exec, wait, fork), Deny Write on Executables

2023. 5. 5. 09:06Computer Science

728x90
반응형

Pintos Project 2 관련 포스팅 목록

 

1. exec

1-1. exec 요구사항

  • 현재 프로세스를 cmd_line에 주어진 실행 파일로 변경하고, 필요한 인수를 전달합니다.
  • 이 함수는 성공한 경우 반환하지 않습니다.
  • 프로그램이 로드되거나 실행될 수 없는 경우 종료 상태 -1로 프로세스가 종료됩니다.
  • 이 함수는 exec를 호출한 스레드의 이름을 변경하지 않습니다.
  • 파일 디스크립터는 exec 호출을 통해 변경되지 않고 열린 상태를 유지합니다.
    (exec 함수를 호출하여 프로세스를 새로운 실행 파일로 대체하더라도, 이미 열려있는 파일 디스크립터들은 그대로 유지됩니다.)

 

1-2. exec 구현하기

1) syscall_handler()

  • exec을 호출하는 case문을 추가한다.
/* userprog/syscall.c */

void syscall_handler(struct intr_frame *f UNUSED)
{
...

case SYS_EXEC:
        f->R.rax = exec(f->R.rdi);
        break;
}

 

2) exec()

💡 Goal : process_exec 함수를 이용해서 인자로 전달받은 cmd_line을 실행한다.

  • cmd_line의 주소를 검증한다.
check_address(cmd_line);
  • cmd_line을 process_exec에 전달할 복사본을 만든다.
    • 🤔 Why? 그냥 넘기면 안 되나?
    • process_exec 함수 안에서 전달 받은 인자를 parsing하는 과정이 있기 때문에 복사본을 만들어서 전달해야 한다.
      cmd_line은 const char* 타입이라서 수정할 수 없음!
    • process.c 파일의 process_create_initd 함수에서 file_name의 복사본을 만들고 실행시키는 부분을 참고해서 구현할 수 있다. (단, 과제 안내에서 exec를 호출한 스레드의 이름을 변경하지 말라고 했으므로, process_create_initd에서 인자를 수정하는 부분 전까지만 참고한다.)
        char *cmd_line_copy;
        cmd_line_copy = palloc_get_page(0);
        if (cmd_line_copy == NULL)
            exit(-1);                              // 메모리 할당 실패 시 status -1로 종료한다.
        strlcpy(cmd_line_copy, cmd_line, PGSIZE); // cmd_line을 복사한다.
  • 복사본을 인자로 process_exec 함수를 호출하고, 로드되거나 실행될 수 없는 경우에는 status -1로 프로세스를 종료시킨다.
    • process_exec 함수는 실패 시 -1을 리턴하므로 -1인 경우에 에러 핸들링을 해주면 된다.
        // 스레드의 이름을 변경하지 않고 바로 실행한다.
        if (process_exec(cmd_line_copy) == -1)
            exit(-1); // 실패 시 status -1로 종료한다.
  • 이 함수는 성공한 경우 반환하지 않으므로 별도의 return은 없다!

 

🖥 exec() 전체 코드

/* userprog/syscall.c */

int exec(const char *cmd_line); // 선언

...

int exec(const char *cmd_line)
{
    check_address(cmd_line);

    // process.c 파일의 process_create_initd 함수와 유사하다.
    // 단, 스레드를 새로 생성하는 건 fork에서 수행하므로
    // 이 함수에서는 새 스레드를 생성하지 않고 process_exec을 호출한다.

    // process_exec 함수 안에서 filename을 변경해야 하므로
    // 커널 메모리 공간에 cmd_line의 복사본을 만든다.
    // (현재는 const char* 형식이기 때문에 수정할 수 없다.)
    char *cmd_line_copy;
    cmd_line_copy = palloc_get_page(0);
    if (cmd_line_copy == NULL)
        exit(-1);                              // 메모리 할당 실패 시 status -1로 종료한다.
    strlcpy(cmd_line_copy, cmd_line, PGSIZE); // cmd_line을 복사한다.

    // 스레드의 이름을 변경하지 않고 바로 실행한다.
    if (process_exec(cmd_line_copy) == -1)
        exit(-1); // 실패 시 status -1로 종료한다.
}

exec 끝~!


2. fork

2-1. fork 요구사항

  • 현재 프로세스를 복제하여 THREAD_NAME이라는 이름을 가진 새로운 프로세스를 생성해야 합니다.
  • 레지스터 값은 %RBX, %RSP, %RBP, %R12부터 %R15까지인 callee-saved register만 복제해야 합니다.
    → 🚨 레지스터 값을 쟤네만 복제하라는 뜻이 아님!
    general purpose register 중에서는 callee-saved register 제외하고는 백업 안해도 된다.
    즉, %rip, %ds, %ss, %es, %cs 등 특수한 역할을 하는 레지스터들도 복사를 해줘야한다!
  • 함수는 자식 프로세스의 pid를 반환해야 하며, 그렇지 않은 경우 유효한 pid가 될 수 없습니다.
  • 자식 프로세스에서는 반환 값이 0이어야 합니다.
  • 자식 프로세스는 파일 디스크립터와 가상 메모리 공간을 포함한 리소스를 복제해야 합니다.
  • 부모 프로세스는 자식 프로세스가 성공적으로 복제되었는지 알 때까지 fork() 호출에서 반환되지 않아야 합니다.
    • 즉, 자식 프로세스가 리소스를 복제하지 못한 경우 부모의 fork() 호출은 TID_ERROR를 반환해야 합니다.
  • 해당 템플릿은 threads/mmu.c 파일의 pml4_for_each() 함수를 사용하여 사용자 메모리 공간 전체와 해당 페이지 테이블 구조를 복사합니다.
    • pte_for_each_func의 누락된 부분을 채워주어야 합니다.

 

2-2. fork 구현하기 (+ Deny Write on Executables)

1) syscall_handler()

  • fork를 호출하는 case문을 추가한다.
/* userprog/syscall.c */

void syscall_handler(struct intr_frame *f UNUSED)
{
...

case SYS_FORK:
        f->R.rax = fork(f->R.rdi, f);
        break;
}

 

2) fork()

  • process_fork 함수를 호출한다.
/* userprog/syscall.c */

int fork(const char *thread_name, struct intr_frame *f); // 선언

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

 

3) process_fork()

💡 Goal : 현재 프로세스를 복제하여 name이라는 이름의 프로세스를 만들고, 새로 만든 프로세스의 스레드 ID를 리턴한다.

  • 현재 구현 상태👇🏻 (이 함수의 내용을 수정해야 한다.)
    /* Clones the current process as `name`. Returns the new process's thread id, or
     * TID_ERROR if the thread cannot be created. */
    tid_t process_fork(const char *name, struct intr_frame *if_ UNUSED)
    {
        /* Clone current thread to new thread.*/
        return thread_create(name,
                             PRI_DEFAULT, __do_fork, thread_current());
    }

 

3-1) if_를 parent_if에 복사

  • 현재 스레드에 인자로 전달 받은 if_를 저장할 수 있도록 필드를 새로 만들어 저장한다.
  • Why?
    • 스레드를 생성할 때 __do_fork 함수를 이용하는데
      __do_fork의 주석 Hint 부분을 보면, parent->tf에는 userland context가 들어있지 않으니 process_fork의 두번째 인자를 이 함수에 전달할 수 있게 해야 한다고 말하고 있다.
      (Userland context는 유저 모드에서 실행 중인 프로세스의 상태와 관련된 정보를 의미한다.)
        __do_fork 주석 내용

        /* A thread function that copies parent's execution context.
         * Hint) parent->tf does not hold the userland context of the process.
         *       That is, you are required to pass second argument of process_fork to
         *       this function. */
        - 부모의 실행 컨텍스트를 복사하는 스레드 함수입니다.
        - 힌트) parent->tf는 프로세스의 사용자 및 컨텍스트를 보유하지 않습니다.
        - 즉, process_fork의 두 번째 인수를 이 함수에 전달해야 합니다.
  • 🤔 왜 parent->tf를 사용할 수 없는 걸까?
    • fork를 하면 자식은 부모의 tf를 물려받아야 하는데, 지금은 fork()를 수행하면서 context switch가 일어난 상태로, 현재 부모의 tf에는 커널이 작업하던 정보가 저장되어 있다.
    • 자식에게 물려줘야 하는 tf는 커널이 작업하던 정보가 아닌 user-level에서 부모 프로세스가 작업하던 정보를 물려줘야 한다.
    • user_level에서의 부모 프로세스가 실행되던 정보는 시스템콜이 호출될 때 syscall_handler에 f로 들어온다.
    • 따라서, f를 fork 함수에 전달해서 활용해야 한다.
    • 👇🏻 부모의 tf를 나타내는 cur→tf와 fork 인자로 전달 받은 if를 출력해본 결과이다. 주소를 변환해보면 부모의 tf는 커널 영역의 값이고 fork 인자로 받은 if는 유저 영역의 값을 가지고 있다.

struct thread

  • 그래서, 일단 스레드 구조체에 f를 넣기 위해 필드를 하나 새로 만든다.
    /* threads/thread.h */

    struct thread
    {
    ...
    struct file **fdt;
    int next_fd;

    struct intr_frame parent_if; // 추가
    ...

    }

 

process_fork

  • parent_if에 인자로 전달 받은 if를 복사한다.
    struct thread *cur = thread_current();
    memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));
  • 전달받은 if와 cur→parent_if에 if값을 복사한 상태 👇🏻 (이렇게 만들어줘야 한다!)

 

3-2) 새 스레드 생성

process_fork

  • 스레드가 생성되고 나서 실행할 함수로 __do_fork를 지정하고, 인자로 부모가 될 스레드(위에서 if_를 복사해서 넣은 스레드)를 넣어준다.
    // 현재 스레드를 fork한 new 스레드를 생성한다.
        tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, cur);
        if (tid == TID_ERROR)
            return TID_ERROR;

 

3-3) 자식 관계 설정

  • 새로 생성되는 스레드를 현재 실행중인 스레드의 자식으로 지정해준다.

 

struct thread

  • 자식 리스트를 설정하기 위한 필드 child_listchild_elem을 추가한다.
    /* threads/thread.h */

    struct thread
    {
    ...
    struct file **fdt;
    int next_fd;

    struct intr_frame parent_if;
    struct list child_list; // 추가
    struct list_elem child_elem; // 추가
    ...

    }

 

init_thread

  • 새로 만든 child_list를 초기화해준다.
/* threads/thread.c */
static void
init_thread(struct thread *t, const char *name, int priority)
{
...

    list_init(&(t->child_list));
}

 

thread_create

  • 현재 스레드의 child_list에 새로 만든 스레드를 추가한다.
    // 현재 스레드의 자식으로 추가
        list_push_back(&thread_current()->child_list, &t->child_elem);

 

3-4) 로드가 완료될 때까지 대기

  • 이제 스레드는 thread_create로 생성되고 ready_list에 들어간 상태다. 이 스레드가 스케줄링되어서 실행되면 thread_create 함수에 넣어준 __do_fork 함수가 호출되어 load가 진행된다.
  • 부모는 이 load가 완료될 때까지 대기해야 한다.
  • semaphore를 활용해 load가 완료될 때까지 부모를 재우자~!

 

struct thread

  • load_sema 필드를 추가한다.
/* threads/thread.h */

struct thread
{
...
struct file **fdt;
int next_fd;

struct intr_frame parent_if;
struct list child_list;
struct list_elem child_elem;

struct semaphore load_sema; // 추가
...

}

 

init_thread

  • load_sema를 초기화한다.
/* threads/thread.c */
static void
init_thread(struct thread *t, const char *name, int priority)
{
...

    sema_init(&t->load_sema, 0);
}

 

process_fork

  • 생성하면서 반환 받은 pid를 이용해서 방금 생성한 자식 스레드를 찾는다.
    • pid로 자식을 찾는 함수 get_child_process는 아래에서 새로 선언한다.
  • 자식 스레드의 load_sema를 이용해 sema_down을 호출한다.
    // 자식이 로드될 때까지 대기하기 위해서 방금 생성한 자식 스레드를 찾는다.
    struct thread *child = get_child_process(pid);

    // 현재 스레드는 생성만 완료된 상태이다. 생성되어서 ready_list에 들어가고 실행될 때 __do_fork 함수가 실행된다.
    // __do_fork 함수가 실행되어 로드가 완료될 때까지 부모는 대기한다.
    sema_down(&child->load_sema);

 

get_child_process

  • pid를 인자로 받아 자식 스레드를 반환하는 함수를 새로 선언한다.
    (process_fork랑 process_wait에서 쓰인다.)
/* userprog/process.h */

struct thread *get_child_process(int pid);
/* userprog/process.c */

// 자식 리스트에서 원하는 프로세스를 검색하는 함수
struct thread *get_child_process(int pid)
{
    /* 자식 리스트에 접근하여 프로세스 디스크립터 검색 */
    struct thread *cur = thread_current();
    struct list *child_list = &cur->child_list;
    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);
        /* 해당 pid가 존재하면 프로세스 디스크립터 반환 */
        if (t->tid == pid)
            return t;
    }
    /* 리스트에 존재하지 않으면 NULL 리턴 */
    return NULL;
}

 

3-5) 자식 프로세스의 pid 반환

  • 과제에서 요구한대로 자식 프로세스의 pid를 반환하고 종료한다.

process_fork

// 자식 프로세스의 pid를 반환한다.
    return pid;

 

🖥 process_fork 전체 코드

/* Clones the current process as `name`. Returns the new process's thread id, or
 * TID_ERROR if the thread cannot be created. */
tid_t process_fork(const char *name, struct intr_frame *if_ UNUSED)
{
    /* Clone current thread to new thread.*/
    // 현재 스레드의 parent_if에 복제해야 하는 if를 복사한다.
    struct thread *cur = thread_current();
    memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));

    // 현재 스레드를 fork한 new 스레드를 생성한다.
    tid_t pid = thread_create(name, PRI_DEFAULT, __do_fork, cur);
    if (pid == TID_ERROR)
        return TID_ERROR;

    // 자식이 로드될 때까지 대기하기 위해서 방금 생성한 자식 스레드를 찾는다.
    struct thread *child = get_child_process(pid);

    // 현재 스레드는 생성만 완료된 상태이다. 생성되어서 ready_list에 들어가고 실행될 때 __do_fork 함수가 실행된다.
    // __do_fork 함수가 실행되어 로드가 완료될 때까지 부모는 대기한다.
    sema_down(&child->load_sema);

    // 자식 프로세스의 pid를 반환한다.
    return pid;
}

 

4) __do_fork()

4-1) parent_if 할당

  • 인자로 전달 받은(process_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;

 

4-2) 자식 프로세스의 리턴값 지정

  • 자식 프로세스의 리턴값은 0으로 지정한다.
/* 1. Read the cpu context to local stack. */
    memcpy(&if_, parent_if, sizeof(struct intr_frame));
    if_.R.rax = 0; // 자식 프로세스의 리턴값은 0

 

4-3) 파일 디스크립터 테이블의 파일 복제

  • 주석 내용대로 file_duplicate 함수를 활용하면 파일을 복제할 수 있다.
/* 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 < 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;
    }

    // next_fd도 복제
    current->next_fd = parent->next_fd;

 

4-4) 부모 프로세스의 대기 해제

  • 로드가 완료되었으니 sema_up을 호출해 부모 프로세스의 대기를 해제시킨다.
    // 로드가 완료될 때까지 기다리고 있던 부모 대기 해제
    sema_up(&current->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; // 자식 프로세스의 리턴값은 0

    /* 2. Duplicate PT */
    current->pml4 = pml4_create();
    if (current->pml4 == NULL)
        goto error;

    process_activate(current);
#ifdef VM
    supplemental_page_table_init(&current->spt);
    if (!supplemental_page_table_copy(&current->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 < 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(&current->load_sema);
    process_init();

    /* Finally, switch to the newly created process. */
    if (succ)
        do_iret(&if_);
error:
    sema_up(&current->load_sema);
    exit(TID_ERROR);
}

 

5) duplicate_pte

  • 페이지 테이블을 복제하는 데 사용되는 함수 duplicate_pte의 빈 부분을 채운다.
  • why? 과제에서 애초에 채우라고 비워서 줬음ㅎㅋ
  • 주석을 보고 시키는대로 하면 된당
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;
}

 

6) load

  • 현재 실행 중인 파일을 수정하는 일이 발생하면 안되겠지!
  • 실행 중인 파일에 대한 쓰기 작업을 거부하는 코드를 추가한다.
    • file_deny_write() 함수를 사용
    • User Programs 5번째 과제임 (Deny Write on Executables)
    • 이 부분을 구현하면 rox 관련 테스트들을 통과할 수 있다. (rox: Read Only for eXecutable)
  • 파일을 닫으면 쓰기 작업이 다시 허용된다ㅠㅠ 따라서 여기서 파일을 닫지 않고 스레드가 종료될 때 파일을 닫을 수 있게 스레드 구조체에 로드한 파일을 저장할 수 있게 필드를 추가한다.

 

struct thread

  • 현재 스레드의 실행중인 파일을 저장할 running 필드를 추가한다.
/* threads/thread.h */

struct thread
{
...
struct file **fdt;
int next_fd;

struct intr_frame parent_if;
struct list child_list;
struct list_elem child_elem;

struct semaphore load_sema;

struct file *running; // 추가
...

}

 

load

  • 현재 load 함수에서는 로드가 완료되면 파일을 바로 close하는데, 여기서 close하지 않고 스레드가 삭제될 때 파일을 닫도록 변경해야 한다. —> file_close(file); 부분을 제거한다.
    • Why? 파일을 닫으면 쓰기 작업이 다시 허용되기 때문이다. 따라서 프로세스의 실행 파일에 대한 쓰기를 거부하려면 해당 파일을 프로세스가 실행 중인 동안 계속 열어두어야 한다.
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_)) // user stack 초기화
        goto done;

    /* Start address. */
    if_->rip = ehdr.e_entry; // entry point 초기화
    // rip: 프로그램 카운터(실행할 다음 인스트럭션의 메모리  주소)

    /* 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. */
    // 파일을 여기서 닫지 않고 스레드가 삭제될 때 process_exit에서 닫는다.
    // file_close(file);
    return success;
}

 

process_exit

  • 프로세스가 종료될 때 현재 실행 중인 파일을 닫는다.
void process_exit(void)
{

...

    file_close(cur->running); // 현재 실행 중인 파일을 닫는다.
    process_cleanup();

}

 


3. wait

3-1. wait 요구사항

  • wait() 함수는 자식 프로세스 pid를 기다리고 자식의 종료 상태(exit status)를 검색합니다.
    • pid가 아직 살아있는 경우 종료될 때까지 기다립니다.
    • 자식이 종료되면 자식의 종료 상태를 반환합니다.
  • pid가 exit()를 호출하지 않고 커널에 의해 종료된 경우(예: exception으로 인해 종료) wait(pid)는 -1을 반환해야 합니다.
  • 부모 프로세스가 호출을 기다리는 동안에 이미 종료된 자식 프로세스를 기다리고 있을 수 있지만, 커널은 여전히 부모가 자식의 종료 상태를 검색하거나 자식이 커널에 의해 종료되었음을 알 수 있도록 허용해야 합니다.
  • 다음 중 어느 하나의 조건이 참이면 wait()은 즉시 실패하고 -1을 반환해야 합니다:
    1. pid가 호출하는 프로세스의 직접적인 자식을 참조하지 않는 경우
      • 호출 프로세스가 성공적인 fork() 호출의 반환값으로 받은 pid만 호출 프로세스의 직접적인 자식에 해당한다.
      • 자식은 상속되지 않습니다.
        A가 자식 B를 생성하고 B가 자식 프로세스 C를 생성하는 경우 A는 C를 기다릴 수 없습니다.(B가 죽더라도)
        따라서 A가 프로세스 C를 기다리는 wait(C) 호출은 실패해야 합니다.
        마찬가지로, 고아 프로세스는 새로운 부모를 할당받지 않습니다.
    2. wait를 호출한 프로세스가 이미 pid에 대해 wait를 호출한 경우
      즉, 한 프로세스는 특정 자식에 대해 최대 한 번까지만 기다릴 수 있습니다.
  • 프로세스는 임의의 수의 자식을 생성하고, 임의의 순서로 그들을 기다릴 수 있으며, 자식의 일부 또는 전체에 대해 기다리지 않고 종료할 수도 있습니다.
  • 모든 wait이 발생할 수 있는 방법을 고려해야 합니다.
  • struct thread를 포함한 프로세스의 모든 리소스는 프로세스의 부모가 프로세스를 기다리는지 여부와
    자식이 부모보다 먼저 종료되었는지 여부에 관계없이 해제되어야 합니다.
  • initial process가 종료될 때까지 Pintos가 종료되지 않도록 보장해야 합니다.
  • 제공된 Pintos 코드는 main() (threads/init.c)에서 process_wait() (userprog/process.c에 있는)를 호출하여 이를 수행하려고 시도합니다.
  • 구현 방법: process_wait()를 함수 맨 위에 있는 주석에 따라 구현한 다음, process_wait()을 기반으로 wait 시스템 호출을 구현하는 것을 제안합니다.

 

3-2. wait 구현하기

1) syscall_handler()

  • wait를 호출하는 case문을 추가한다.
/* userprog/syscall.c */

void syscall_handler(struct intr_frame *f UNUSED)
{
...

case SYS_WAIT:
        f->R.rax = wait(f->R.rdi);
        break;
}

 

2) wait()

  • process_wait 함수를 호출한다.
/* userprog/syscall.c */

int wait(int pid); // 선언

int wait(int pid)
{
    return process_wait(pid);
}

 

3) process_wait()

  • 과제 Docs에서 process_wait()를 주석에 따라 구현하고 활용하라고 안내하고 있으니, 이 함수를 완성해서 활용하자!
    process_wait 주석 내용

    /* Waits for thread TID to die and returns its exit status.  If
     * it was terminated by the kernel (i.e. killed due to an
     * exception), returns -1.  If TID is invalid or if it was not a
     * child of the calling process, or if process_wait() has already
     * been successfully called for the given TID, returns -1
     * immediately, without waiting.
     *
     * This function will be implemented in problem 2-2.  For now, it
     * does nothing. */

    - process_wait 함수는 스레드 식별자 **TID가 종료될 때까지 기다리고, exit status를 반환**합니다. 
    - 스레드가 커널에 의해 종료되었을 경우(즉, 예외로 인해 종료된 경우) -1을 반환합니다. 
    - TID가 유효하지 않거나 호출하는 프로세스의 자식 스레드가 아니거나, 
        이미 해당 TID에 대해 process_wait()가 호출되었다면 즉시 -1을 반환합니다.

 

process_wait

1) get_child_process 함수를 만들어서 사용한다. 인자로 받은 tid를 갖는 자식이 없는 경우에는 -1을 반환하고 종료한다.

2) 찾은 자식이 sema_up 해줄때까지 (종료될 때까지) 대기한다.

3) 자식에게서 종료 signal이 도착하면 자식 리스트에서 해당 자식을 제거한다.

4) 자식이 완전히 종료되어도 괜찮은지 대기하고 있으므로, sema_up으로 signal을 보내 완전히 종료되게 해주고,

5) 자식의 exit_status를 반환하고 함수를 종료한다.

int process_wait(tid_t child_tid UNUSED)
{
    struct thread *child = get_child_process(child_tid);
    if (child == NULL) // 1) 자식이 아니면 -1을 반환한다.
        return -1;

    // 2) 자식이 종료될 때까지 대기한다. (process_exit에서 자식이 종료될 때 sema_up 해줄 것이다.)
    sema_down(&child->wait_sema);
    // 3) 자식이 종료됨을 알리는 `wait_sema` signal을 받으면 현재 스레드(부모)의 자식 리스트에서 제거한다.
    list_remove(&child->child_elem);
    // 4) 자식이 완전히 종료되고 스케줄링이 이어질 수 있도록 자식에게 signal을 보낸다.
    sema_up(&child->exit_sema);

    return child->exit_status; // 5) 자식의 exit_status를 반환한다.
}

 

struct thread

  • 스레드 구조체에 대기를 위한 필드 exit_sema와 wait_sema를 추가한다.
/* threads/thread.h */

struct thread
{
...
struct file **fdt;
int next_fd;

struct intr_frame parent_if;
struct list child_list;
struct list_elem child_elem;

struct semaphore load_sema;
struct semaphore exit_sema; // 추가
struct semaphore wait_sema; // 추가

struct file *running; // 현재 실행중인 파일
...

}

 

init_thread

  • exit_sema와 wait_sema를 초기화한다.
/* threads/thread.c */
static void
init_thread(struct thread *t, const char *name, int priority)
{
...

    sema_init(&t->exit_sema, 0);
    sema_init(&t->wait_sema, 0);
}

 

4) process_exit()

process_exit

1) FDT의 모든 파일을 닫고 메모리도 반환한다.

2) 현재 실행 중인 파일도 닫는다.

3) 자식이 종료되기를 기다리고 있는 (wait) 부모에게 sema_up으로 signal을 보낸다.

4) 부모가 wait을 마무리하고 나서 signal을 보내줄 때까지 대기한다.

  • 이 대기가 풀리고 나면 스케줄링이 이어진다.
  • thread_exit 함수에서 process_exit 이후에 do_schedule(THREADY_DYING)이 진행되는 것을 볼 수 있음
/* Exit the process. This function is called by thread_exit (). */
void process_exit(void)
{
    struct thread *cur = thread_current();

    // 1) FDT의 모든 파일을 닫고 메모리를 반환한다.
    for (int i = 2; i < FDT_COUNT_LIMIT; i++)
        close(i);
    palloc_free_page(cur->fdt);
    file_close(cur->running); // 2) 현재 실행 중인 파일도 닫는다.

    process_cleanup();

    // 3) 자식이 종료될 때까지 대기하고 있는 부모에게 signal을 보낸다.
    sema_up(&cur->wait_sema);
    // 4) 부모의 signal을 기다린다. 대기가 풀리고 나서 do_schedule(THREAD_DYING)이 이어져 다른 스레드가 실행된다.
    sema_down(&cur->exit_sema);
}
728x90
반응형