Adventure Time - Finn 3

새소식

운영체제

[Pintos-Kaist] Project2 - System Call(4) - (exec, fork, wait)

  • -

지난 시스템 콜 기능 구현 포스트에서는 exec, fork, wait을 제외한 기능들을 구현했습니다. 관련 내용은 아래 포스트 참고하시길 바랍니다.

https://yunchan97.tistory.com/73

 

[Pintos-Kaist] Project2 - System Call(3) -(write, putbuf, open, process_add_file, process_get_file, read, filesize, seek, tell,

지난 포스트에서는 전체적인 코드의 흐름을 파악하는 과정을 봤습니다. 아래 포스트에 이어서 다음 기능을 구현해보겠습니다. https://yunchan97.tistory.com/71 [Pintos-Kaist] Project2 - System Call(1) - (halt, exit

yunchan97.tistory.com

 

 

이번에는 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(&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;

    /* 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 < 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(&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);
}

 

 

 

 

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);
}
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.