sw사관학교정글/PintOS(KAIST's CS330 class)

[week09] PintOS - Project 2(User Programs) : System Calls 나머지

D cron 2022. 1. 12. 01:53

Project 2 : System call 나머지

나머지 system call들을 구현해보도록 하자!

syscall.c 헤더파일 추가 및 함수의 원형 선언

// userprog/syscall.c

#include "threads/init.h"
#include "threads/synch.h"
#include "threads/palloc.h"
#include "filesys/filesys.h"
#include "filesys/file.h"
#include "userprog/process.h"

static struct file *find_file_by_fd(int fd);
void check_address(uaddr);
void halt(void);
void exit(int status);
bool create(const char *file, unsigned initial_size);
bool remove(const char *file);
int open(const char *file);
int filesize(int fd);
int read(int fd, void *buffer, unsigned size);
int write(int fd, const void *buffer, unsigned size);
void seek(int fd, unsigned position);
unsigned tell(int fd);
void close(int fd);

system call handler

// userprog/syscall.c
void syscall_handler(struct intr_frame *f UNUSED)
{
    switch (f->R.rax) // rax는 system call number이다.
    {
    case SYS_HALT:
        halt();
        break;
    case SYS_EXIT:
        exit(f->R.rdi); //실행할 때 첫번째 인자가 R.rdi에 저장됨
        break;
    case SYS_FORK:
        f->R.rax = fork(f->R.rdi, f);
        break;
    case SYS_EXEC:
        if (exec(f->R.rdi) == -1)
        {
            exit(-1);
        }
        break;
    case SYS_WAIT:
        f->R.rax = process_wait(f->R.rdi);
        break;
    case SYS_CREATE:
        f->R.rax = create(f->R.rdi, f->R.rsi);
        break;
    case SYS_REMOVE:
        f->R.rax = remove(f->R.rdi);
        break;
    case SYS_OPEN:
        f->R.rax = open(f->R.rdi);
        break;
    case SYS_FILESIZE:
        f->R.rax = filesize(f->R.rdi);
        break;
    case SYS_READ:
        f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
        break;
    case SYS_WRITE:
        f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
        break;
    case SYS_SEEK:
        seek(f->R.rdi, f->R.rsi);
        break;
    case SYS_TELL:
        f->R.rax = tell(f->R.rdi);
        break;
    case SYS_CLOSE:
        close(f->R.rdi);
        break;
    default:
        exit(-1);
        break;
    }
}

halt()

// userprog/syscall.c
void halt(void)
{
    power_off();
}

PintOS 종료

exit()

// userprog/syscall.c
void exit(int status)
{
    struct thread *cur = thread_current();
    cur->exit_status = status;                         // 종료시 상태를 확인, 정상종료면 state = 0
    printf("%s: exit(%d)\n", thread_name(), status); // 종료 메시지 출력
    thread_exit();                                     // thread 종료
}

프로세스 종료

thread 구조체를 많이 바꿔야 하는데 지금 한번에 바꿔버리자.

// include/threads/thread.h
struct thread
{
    /* Owned by thread.c. */
    tid_t tid;                   /* Thread identifier. 정수형, 프로세스에 1부터 부여 */
    enum thread_status status; /* Thread state. */
    char name[16];               /* Name (for debugging purposes). */
    int priority;               /* Priority. 숫자 클수록 우선순위 높음*/

    /* Shared between thread.c and synch.c. */
    struct list_elem elem; /* List element. */

    // 알람시계 일어날 시간.
    int64_t wakeup;

    // priority donation
    int init_priority; // 최초 스레드 우선순위 저장.

    struct lock *wait_on_lock;        // 현재 thread가 요청했는데 받지못한 lock. 기다리는중
    struct list donations;            // 자신에게 priority를 나누어준 'thread'의 리스트
    struct list_elem donation_elem; // 위의 thread 리스트를 관리하기위한 element. thread 구조체의 elem과 구분.

    // project 2 : system call
    int exit_status;        // 자식 프로세스의 exit 상태를 부모에게 전달
    struct file **fd_table; // file descriptor table의 시작주소 가리키게 초기화
    int fd_idx;                // fd table에 open spot의 index

    struct intr_frame parent_if;
    struct semaphore fork_sema; // fork한 child의 load를 기다리는 용도

    struct list child_list; // parent가 가진 child_list
    struct list_elem child_elem;

    struct semaphore wait_sema;
    struct semaphore free_sema;

    struct file *running;

    int stdin_count; 
    int stdout_count;
#ifdef USERPROG
    /* Owned by userprog/process.c. */
    uint64_t *pml4; /* Page map level 4 */
#endif
#ifdef VM
    /* Table for whole virtual memory owned by thread. */
    struct supplemental_page_table spt;
#endif

    /* Owned by thread.c. */
    struct intr_frame tf; /* Information for switching */
    unsigned magic;          /* Detects stack overflow. */
};

create()

// userprog/syscall.c
bool create(const char *file, unsigned initial_size)
{
    check_address(file);
    return filesys_create(file, initial_size);
}

파일을 생성한다. 파일 생성에 성공하면 true, 실패하면 false를 반환한다.

remove()

// userprog/syscall.c
bool remove(const char *file)
{
    check_address(file);
    return filesys_remove(file);
}

파일을 삭제한다. 파일 제거에 성공하면 true, 실패하면 false를 반환한다.

exec()

잠깐!!!

exec() 함수를 구현하기 전에 process_create_initd() 함수를 조금 수정해줘야 한다. 지금 현재는 전체를 넣어주는데, file_name을 분리해서 넣어줘야 한다.

// userprog/process.c
tid_t process_create_initd(const char *file_name)
{
    char *fn_copy;
    tid_t tid;

    /* Make a copy of FILE_NAME.
     * Otherwise there's a race between the caller and load(). */
    fn_copy = palloc_get_page(0); // page를 할당받고 해당 page에 file_name을 저장해줌

    if (fn_copy == NULL)
        return TID_ERROR;
    strlcpy(fn_copy, file_name, PGSIZE); // page의 크기 즉, PGSIZE는 2^12 (4KB)

    // project 2 : system call
    // file_name을 분리해서 넣어줘야함
    char *save_ptr;
    strtok_r(file_name, " ", &save_ptr);
    /* Create a new thread to execute FILE_NAME. */
    // PRI_DEFAULT : 기본 우선순위 31
    // file_name을 이름으로 하고 PRI_DEFAULT를 우선순위로 갖는 새로운 thread 생성, tid에 저장
    // thread는 fn_copy를 인자로 받는 initd라는 함수를 실행시킴
    tid = thread_create(file_name, PRI_DEFAULT, initd, fn_copy);
    if (tid == TID_ERROR)
        palloc_free_page(fn_copy);
    return tid;
}

이제 exec()을 구현해보자.

// userprog/syscall.c
int exec(char *file_name)
{
    check_address(file_name);
    int file_size = strlen(file_name) + 1;
    char *fn_copy = palloc_get_page(PAL_ZERO);
    if (fn_copy == NULL)
    {
        exit(-1);
    }
    strlcpy(fn_copy, file_name, file_size); // file 이름만 복사
    if (process_exec(fn_copy) == -1)
    {
        return -1;
    }
    NOT_REACHED();
    return 0;
}

현재 프로세스를 cmd_line에 이름이 지정된 실행 파일로 변경하고 argument를 전달한다.

이전 포스팅에서 exec() 을 예시로 system call을 어떻게 불러오는지 설명했었다. 기억이 안나면

여기로 https://d-cron.tistory.com/61

다시 review해보자면 큰 그림은 다음과 같다.

여기까지 완료하면 syscall handler가 전달받은 함수와 argument를 가지고 exec() 를 실행시킨다.

그리고 이 때는 kernel thread의 stack은 다 pop되어서 비워져 있다.

아까와 동일한 kernel thread에서 exec()가 실행되고, 그 안에있는 process_exec()가 실행되고, 그 안에있는 load()가 실행되면서 메모리에 실행할 file의 argv,argc를 넣을 user stack을 할당하고 쌓고 , load()가 끝나면 do_iret()을 실행시켜서 새로운 user_process를 실행한다!

(test 통과를 위해서는 process_exec()안에 있는 hex_dump()는 주석처리를 해주던지 지워야 한다)

fork()

시스템 콜을 호출한 부모 프로세스로부터 자식 프로세스를 생성하는 함수이다.

// userprog/syscall.c
tid_t fork(const char *thread_name, struct intr_frame *f)
{
    return process_fork(thread_name, f);
}

process_fork() 함수로 가보자.

// userprog/process.c
tid_t process_fork(const char *name, struct intr_frame *if_ UNUSED)
{
    // project 2 : system call
    /* Clone current thread to new thread.*/
    struct thread *cur = thread_current();
    // 현재 thread의 parent_if에 if_를 저장
    memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));

    tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, cur);
    if (tid == TID_ERROR)
    {
        return TID_ERROR;
    }

    struct thread *child = get_child_with_pid(tid); // child_list안에서 만들어진 child thread를 찾음
    sema_down(&child->fork_sema);                    // 자식이 메모리에 load 될때까지 기다림(blocked)
    if (child->exit_status == -1)
    {
        return TID_ERROR;
    }
    return tid;
}

여기 __do_fork()함수를 이용해서 자식 thread를 생성한다. __do_fork() 함수를 들어가보자.

// userprog/process.c
static void
__do_fork(void *aux) // load로 볼 수도 있다(부모의 것들을 자식에게 다 복사해서 메모리에 올리는 과정)
{
    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;
    bool succ = true;
    // project 2 : system call
    parent_if = &parent->parent_if;

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

    /* 1. Read the cpu context to local stack. */
    memcpy(&if_, parent_if, sizeof(struct intr_frame));
    // project 2 : system call
    if_.R.rax = 0;

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

    process_activate(current); //tss를 업데이트 해준다.

#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.*/

    // process_init();
    // project 2 : system call
    if (parent->fd_idx == FDCOUNT_LIMIT)
        goto error;

    for (int i = 0; i < FDCOUNT_LIMIT; i++)
    {
        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)
        {
            struct file *new_file;
            if (file > 2)
                new_file = file_duplicate(file);
            else
                new_file = file;
            current->fd_table[i] = new_file;
        }
    }
    current->fd_idx = parent->fd_idx;

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

    sema_up(&current->fork_sema);

    /* Finally, switch to the newly created process. */
    if (succ)
        do_iret(&if_);
error:
    // thread_exit();
    // project 2 : system call
    current->exit_status = TID_ERROR;
    sema_up(&current->fork_sema);
    exit(TID_ERROR);
}

이 함수는 부모의 context를 복사하는 함수다.

여기서 parent_if를 if_에 memcpy하기 때문에(context가 다 저장되기 때문에) 자식프로세스는 parent가 실행되는 그 시점부터 실행된다.

복사가 다 끝나면 sema_up(&current->fork_sema)를 통해 sema를 up 시켜준다(기다리고 있는 부모를 위해)

중간에 duplicate_pte 함수가 있는데 이 함수도 채워줘야 한다. 부모의 page table을 복사해서 자신의 pml4를 만드는 함수다

// userprog/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;

    /* 1. TODO: If the parent_page is kernel page, then return immediately. */
    if (is_kernel_vaddr(va))
    {
        // return false ends pml4_for_each. which is undesirable - just return true to pass this kernel 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)
    {
        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
    /* 3. TODO: Allocate new PAL_USER page for the child and set result to
     *    TODO: NEWPAGE. */
    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). */
    memcpy(newpage, parent_page, PGSIZE);
    writable = is_writable(pte); // pte는 parent_page를 가리키는 주소
    /* 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. */
        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;
}

fork의 이해를 돕기 위한 그림자료와 설명.

parent kernel thread가 process_fork() 안에 thread_create()를 통해 child kernel thread를 생성한다. child kernel thread는 __do_fork() 함수를 들고 들어가서 실행시킨다.

parent kernel thread의 thread_create()는 return되고, process_fork() 안에 tid에는 child kernel thread의 tid가 저장된 상태가 되며 sema_down(&child->forksema)를 통해 자식이 sema_up을 해줄 때까지 기다린다(blocked) 자식이 부모를 다 복사했으면 sema_up을 해주고 do_iret()을 통해 user process를 시작한다.

wait()

현재 상태는 test확인을 위해 for문을 돌려놓은 상태이다.

//userprog/process.c
int process_wait(tid_t child_tid UNUSED)
{
    for simple tests
    for (int i = 0; i < 100000000; i++)
    {
    }
    return -1;
}

이제 제대로 wait()을 구현해보자.

//userprog/process.c
int process_wait(tid_t child_tid UNUSED)
{
    struct thread *child = get_child_with_pid(child_tid);
    // 본인의 자식이 아닌경우(호출 프로세스의 하위 항목이 아닌 경우)
    if (child == NULL)
        return -1;

    sema_down(&child->wait_sema); // 여기서는 parent가 잠드는 거고

    // 여기서부터는 깨어났다.
    // 깨어나면 child의 exit_status를 얻는다.
    int exit_status = child->exit_status;
    // child를 부모 list에서 지운다.
    list_remove(&child->child_elem);
    // 내가 받았음을 전달하는 sema
    sema_up(&child->free_sema);
    return exit_status;
}

wait()을 더 잘 이해하기 위해서는 대응되는 함수인 process_exit() 과 함께 살펴보면 좋다.

//userprog/process.c
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. */
    // project 2-4
    for (int i = 0; i < FDCOUNT_LIMIT; i++)
    {
        close(i);
    }
    // for multi-oom(메모리 누수)
    palloc_free_multiple(cur->fd_table, FDT_PAGES);
    // for rox- (실행중에 수정 못하도록)
    file_close(cur->running);

    sema_up(&cur->wait_sema);    // 종료되었다고 기다리고 있는 부모 thread에게 signal 보냄-> sema_up에서 val을 올려줌
    sema_down(&cur->free_sema); // 부모의 exit_Status가 정확히 전달되었는지 확인(wait)
    process_cleanup();            // pml4를 날림(이 함수를 call 한 thread의 pml4)
}

load()

static bool
load(const char *file_name, struct intr_frame *if_)
{
    ...

    // project 2 : system call
    // 현재 실행중인 파일의 경우 write 할 수 없도록 설정
    t->running = file;
    file_deny_write(file);

    ...
done:
    /* We arrive here whether the load is successful or not. */
    // file_close(file);
    return success;
}

현재 실행중인 파일의 경우 write 할 수 없도록 설정한다(이걸 해야지 rox-관련된 test들 통과 가능)

참고자료

How does thread/process switching work in Pintos? - MPCS 52030 - Operating Systems