PintOS에서 process_wait 올바르게 구현하기

0. 서론

2주차에서 구현한 코드 중 process_waitprocess_exit가 심각한 오류를 내포하고 있음을 발제를 통해 확인할 수 있었다. 이를 해소하기 위한 방법을 알아보자.

1. 기존 코드의 문제점

기존 process_wait()process_exit()은 아래와 같이 구현되어 있었다.

/* process_wait() */
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* curr = thread_current();
    struct thread* child = get_child(child_tid);
    if (child == NULL)
    {
        return -1;
    }
    sema_down(&child->sema_for_wait);
    int child_exit_status = child->exit_status;
    list_remove(&child->child_elem);
    sema_up(&child->sema_for_free);
    return child_exit_status;
}

/* process_exit() */
void process_exit(void)
{
    struct thread* cur = 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. */

    // 프로세스의 모든 열린 파일 close
    for (int fd = 0; fd < OPEN_MAX, fd++;)
    {
        close(fd);
    }
    
    /* Deallocate File Descriptor Table */
    palloc_free_multiple(cur->fdt, FDT_PAGES);
    file_close(cur->running_f);
    
    sema_up(&cur->sema_for_wait);
    sema_down(&cur->sema_for_free);
    
    process_cleanup();
}

여기서 문제가 되는 코드는 process_exit()

sema_down(&cur->sema_for_free);

이다.

위 코드에 의해 exit되는 process들은 전부 세마포의 값을 내리고 스스로 block상태가 되게 되는데, 만약 해당 프로세스의 부모 프로세스가 wait() 시스템 콜을 호출하지 않는다면 이들은 영원히 깨어나지 못하게 된다.

즉, sema_down()은 항상 호출되는데 sema_up()은 조건부로 호출되므로 문제가 발생한다.

따라서 이를 위해선 문제가 되는 sema_down()sema_up()을 호출하지 않아야 하므로 아래와 같은 코드가 되어야 한다.

/* process_wait() */
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* curr = thread_current();
    struct thread* child = get_child(child_tid);
    if (child == NULL)
    {
        return -1;
    }
    sema_down(&child->sema_for_wait);
    int child_exit_status = child->exit_status;
    list_remove(&child->child_elem);
    return child_exit_status;
}

/* process_exit() */
void process_exit(void)
{
    struct thread* cur = 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. */

    // 프로세스의 모든 열린 파일 close
    for (int fd = 0; fd < OPEN_MAX, fd++;)
    {
        close(fd);
    }
    
    /* Deallocate File Descriptor Table */
    palloc_free_multiple(cur->fdt, FDT_PAGES);
    file_close(cur->running_f);
    
    sema_up(&cur->sema_for_wait);
    
    process_cleanup();
}

그런데 이렇게 두 번째 세마포를 사용하지 않으면 발생하는 또 다른 문제가 있다(사실 이 문제 때문에 두 번째 세마포sema_for_free를 사용했다).

/* process_wait() */
{
	//...
    sema_down(&child->sema_for_wait);
    int child_exit_status = child->exit_status;
    list_remove(&child->child_elem); // Warning!
    return child_exit_status;
}

바로 child thread가 process_exit()이후 제거되게 되면 list_remove(&child->child_elem)의 child 포인터가 댕글링 포인터가 된다는 점이다.

2. 해결 방안

따라서 이를 해소하기 위해서 댕글링 포인터가 발생할 가능성을 완전히 배제하기 위해 부모가 child pointer를 관리하지 않도록 수정했고, 대신 child의 tid와 exit_status를 리스트 형태로 가지도록 하였다.

struct child_info_t
{
    tid_t tid;
    int exit_status;
    struct semaphore sema_for_wait;
    struct list_elem elem;
};

또, sync_readsync_write등의 테스트를 통과하기 위해 각 자식 정보마다 고유의 sema_for_wait을 가지도록 했다.

그리고 해당 구조체를 위한 인터페이스들을 정의해 주었다.

/////////////// declaration ///////////////
void add_child(struct thread* parent, tid_t child_tid);
void set_exit_status(struct thread* parent, tid_t child_tid, int exit_status);
int get_exit_status(struct thread* parent, tid_t child_tid);
struct child_info_t* get_child_info(struct thread* parent, tid_t child_tid);
void delete_child(struct thread* parent, tid_t child_tid);

/////////////// definition ///////////////
void add_child(struct thread* parent, tid_t child_tid)
{
    struct child_info_t* info = (struct child_info_t*)malloc(sizeof(struct child_info_t));
    info->tid = child_tid;
    sema_init(&info->sema_for_wait, 0);
    list_push_back(&parent->child_info_list, &info->elem);
}

void set_exit_status(struct thread* parent, tid_t child_tid, int exit_status)
{
    struct list_elem* e;
    for (e = list_begin(&parent->child_info_list); e != list_end(&parent->child_info_list); e = list_next(e))
    {
        struct child_info_t* info = list_entry(e, struct child_info_t, elem);
        if (info->tid == child_tid)
        {
            info->exit_status = exit_status;
        }
    }
}

int get_exit_status(struct thread* parent, tid_t child_tid)
{
    struct list_elem* e;
    for (e = list_begin(&parent->child_info_list); e != list_end(&parent->child_info_list); e = list_next(e))
    {
        struct child_info_t* info = list_entry(e, struct child_info_t, elem);
        if (info->tid == child_tid)
        {
            return info->exit_status;
        }
    }

    return 0;
}

struct child_info_t* get_child_info(struct thread* parent, tid_t child_tid)
{
    struct list_elem* e;
    for (e = list_begin(&parent->child_info_list); e != list_end(&parent->child_info_list); e = list_next(e))
    {
        struct child_info_t* info = list_entry(e, struct child_info_t, elem);
        if (info->tid == child_tid)
        {
            return info;
        }
    }

    return NULL;
}

void delete_child(struct thread* parent, tid_t child_tid)
{
    struct list_elem* e;
    for (e = list_begin(&parent->child_info_list); e != list_end(&parent->child_info_list); e = list_next(e))
    {
        struct child_info_t* info = list_entry(e, struct child_info_t, elem);
        if (info->tid == child_tid)
        {
            list_remove(e);
        }
    }
}

이후 process_wait()process_exit()을 아래와 같이 수정해준다.

int process_wait(tid_t child_tid UNUSED)
{
    struct thread* curr = thread_current();
    struct child_info_t* info = NULL;
    if (!(info = get_child_info(curr, child_tid)))
    {
        return -1;
    }
    sema_down(&info->sema_for_wait);
    int child_exit_status = get_exit_status(curr, child_tid);
    delete_child(curr, child_tid);
    return child_exit_status;
}

void process_exit(void)
{
    struct thread* cur = thread_current();

    // 프로세스의 모든 열린 파일 close
    for (int fd = 0; fd < OPEN_MAX, fd++;)
    {
        close(fd);
    }
    
    /* Deallocate File Descriptor Table */
    palloc_free_multiple(cur->fdt, FDT_PAGES);
    file_close(cur->running_f);
    struct child_info_t* info = get_child_info(cur->parent, cur->tid);
    sema_up(&info->sema_for_wait);
    
    process_cleanup();
}

이외에도 syscall.c등에서 시스템 콜 함수들을 조금씩 바꾸어야 하는데, 그 부분은 간단하므로 생략한다.