ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • PintOS Project3 - Virtual Memory 2편
    정글 크래프톤 5기 회고 및 정리/PintOS 2024. 6. 4. 19:33

    3. Stack Growth

    구현1

    bool vm_try_handle_fault (struct intr_frame *f, void *addr,
        bool user, bool write, bool not_present);

    - 이 함수는 userprog/excpetion.c 의 page_fault() 에서 페이지 폴트 예외를 처리하는 동안 호출된다.

    - 페이지 폴트가 stack growth 유효를 확인

     

    bool vm_try_handle_fault (struct intr_frame *f UNUSED, void *addr UNUSED, bool user UNUSED, bool write UNUSED, bool not_present UNUSED) {
        ...
        if (not_present) {
             void *rsp = f->rsp;
             if (!user) rsp = thread_current()->rsp_stack;
             
             if ((USER_STACK - (1 << 20) <= rsp - 8 && rsp - 8 == addr && addr <= USER_STACK) || (USER_STACK - (1 << 20) <= rsp && rsp <= addr && addr <= USER_STACK))
                vm_stack_growth(addr);
                
             ...
        }
    	return false;
    }
    void *rsp = f->rsp; - user access의 경우 rsp는 유저 stack을 가리킨다.
    if (!user) rsp = thread_current()->rsp; - kernel access인 경우 thread에서 rsp를 가져와야 한다.
    vm_stack_growth(addr); - 스택 확장으로 처리할 수 있는 폴트인 경우, vm_stack_growth를 호출한다.

    구현2

    void vm_stack_growth (void *addr);

    - 대부분의 운영체제는 스택 크기에 절대적인 제한을 둔다. ( 이 프로젝트에서는 스택 크기를 최대 1MB로 제한해야 한다. )

    - addr에 하나 이상의 이명 페이지를 할당하여 스택 크기를 늘리는 함수

     

    static void vm_stack_growth (void *addr UNUSED) {
        vm_alloc_page(VM_ANON | VM_MARKER_0, pg_round_down(addr), 1);
    }
    설명 - 스택 크기를 증가시키기 위해 anon page를 하나 할당하여 주어진 주소(addr)를 유효주소로 해준다.
    pg_round_down - 페이지의 크기만큼 stack을 늘려줘야하기 때문에 주소(addr)와 제일 가까운 해시의 시작점을 찾아준다.
    - PGSIZE 만큼 늘리기 위해 사용!

     

     

    * pt-grow-stk-sc 테스트가 fail 뜬다. 밑의 트러블 슈팅에서 좀 더 자세히 보겠다.


    4. Memory Mapped Files

    - 익명 페이지와 달리, 메모리 매핑된 페이지는 파일 기반 매핑이다.

    - 페이지의 내용은 기존 파일의 데이터를 반영한다.

    - 페이지 폴트가 발생하면, 물리적 프레임이 즉시 할당되고 파일에서 메모리로 내용이 복사된다.

    - 메모리 매핑된 페이지가 언매핑되거나 스왑 아웃될 때, 내용의 변경 사항이 파일에 반영된다.

     

    구현1 (Complete)

    void *mmap (void *addr, size_t length, int writable, int fd, off_t offset);
    - fd로 열린 파일을 offset 바이트로부터 프로세스의 가상 주소 공간의 addr에 length 바이트만큼 매핑한다.
    - 파일 전체가 addr에서 시작하여 연속적인 가상 페이지에 매핑된다.
    - 만약 파일 길이가 페이지 크기(PGSIZE)의 배수가 아니라면, 마지막으로 매핑된 페이지의 일부 바이트가 파일의 끝을 넘어 "튀어" 나온다.
        · 이 바이트들은 페이지가 페이징될 때 0으로 설정하고
        · 페이지가 디스크에 쓰여질 때는 버린다.
    - 이 함수가 성공하면 파일이 매핑된 가상 주소를 반환한다.
    - 실패하면 파일을 매핑할 유효한 주소가 아닌 NULL을 반환해야 한다.
    - mmap 호출은 fd로 열린 파일의 길이가 0 바이트 경우 실패할 수 있다.
    - addr이 페이지 정렬되어 있지 않았거나 매핑된 페이지 범위가 기존에 매핑된 페이지 집합(스택 또는 실행 파일 로드 시 매핑된 페이지 포함)과 겹치는 경우는 반드시 실패해야 한다.
    - Linux에서 addr이 NULL인 경우 커널이 매핑을 생성할 적절한 주소를 찾는다.
    - 간단히 하기 위해, 주어진 addr에서 mmap을 시도할 수 있다.
    - 따라서, addr이 0인 경우 반드시 실패해야 한다.
    - 이는 일부 PintOS 코드가 가상 페이지 0이 매핑되지 않았다고 가정하기 때문이다.
    - 또한, length가 0인 경우에도 mmap은 실패해야 한다.
    - 마지막으로, 콘솔 입력 및  출력을 나타내는 파일 디스크립터는 매핑할 수 없다.
    - 메모리 맵핑된 페이지는 익명 페이지와 마찬가지로 지연 할당 방식으로 할당되어야 한다.
    - 페이지 객체를 만들기 위해 vm_alloc_page_with_initializer 또는 vm_alloc_page를 사용할 수 있다.

     

    // syscall.c
    void syscall_handler (struct intr_frame *f UNUSED) {
        uint64_t system_call_num = f->R.rax;
        switch (system_call_num) {
            ...
            case SYS_MMAP:
                f->R.rax = mmap(f->R.rdi, f->R.rsi, f->R.rdx, f->R.r10, f->R.r8);
                break;
            case SYS_MUNMAP:
                munmap(f->R.rdi);
                break;
            ...
        }
    }
    
    void *mmap (void *addr, size_t length, int writable, int fd, off_t offset) {
        if (addr == NULL ||  || addr != pg_round_down(addr)) return NULL;
        if (is_kernel_vaddr(addr) || is_kernel_vaddr(addr + length)) return NULL;
        if (offset != pg_round_down(offset)) return NULL;
    
        if (spt_find_page(&thread_current()->spt, addr)) return NULL;
        if (fd == 0 || fd == 1) return NULL; 
    
        struct file *found_file = find_file_by_fd(fd);
        if (found_file == NULL) return NULL;
        if (file_length(found_file) == 0 || (int)length <= 0) return NULL;
    
        return do_mmap(addr, length, writable, f, offset);
    }
    
    void munmap (void *addr) {
        do_munmap(addr);
    }
    if (addr == NULL ||  || addr != pg_round_down(addr)) 
        return NULL;
    - addr 이 0(NULL) 일 경우 반드시 실패
    - 파일 전체가 addr에서 시작하여 연속적인 가상 페이지에 매핑된다.
    if (is_kernel_vaddr(addr) || is_kernel_vaddr(addr + length)) 
        return NULL;
    - 페이지를 매핑해야 하므로 유저 영역의 주소이어야 한다.
    - 페이지의 일부 바이트가 파일의 끝을 넘어 튀어나오면 안된다.
    if (offset != pg_round_down(offset)) 
        return NULL;
    - fd로 열린 파일은 offset 바이트로부터 프로세스의 가상 주소 공간의 addr에 length 바이트만큼 매핑한다.
    if (spt_find_page(&thread_current()->spt, addr))
        return NULL;
    - 매핑된 페이지 범위가 기존에 매핑된 페이지 집합과 겹치는 경우 반드시 실패해야 한다.
    if (fd == 0 || fd == 1) return NULL;  - 콘솔 입력 및 출력을 나타내는 파일 디스크립터는 매핑할 수 없다.
    if (found_file == NULL) return NULL;
    if (file_length(found_file) == 0 || (int)length <= 0) 
        return NULL;
    - fd로 열린 파일의 길이가 0 바이트인 경우 실패할 수 있다.
    - length가 0인 경우에도 mmap은 실패해야 한다.

    void * do_mmap (void *addr, size_t length, int writable, struct file *file, off_t offset) {
        struct file *reopened_file = file_reopen(file);
        void *start_addr = addr;
    
        int total_page_count = length % PGSIZE ? length / PGSIZE + 1 : length / PGSIZE;
    	
        size_t read_bytes = file_length(reopened_file) < length ? file_length(reopened_file) : length;
        size_t zero_bytes = PGSIZE - read_bytes % PGSIZE;
    
        ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
        ASSERT(pg_ofs(addr) == 0);
        ASSERT(offset % PGSIZE == 0);
    
        while (read_bytes > 0 || zero_bytes > 0) {
            size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
            size_t page_zero_bytes = PGSIZE - page_read_bytes;
    
            struct container *container = (struct container *)malloc(sizeof(struct container));
            container->file = reopened_file;
            container->offset = ofs;
            container->read_bytes = page_read_bytes;
    
            if (!vm_alloc_page_with_initializer(VM_FILE, addr, writable, lazy_load_segment, container))
                return NULL;
            
            struct page *p = spt_find_page(&thread_current()->spt, start_addr);
            p->mapped_page_count = total_page_count;
    
            read_bytes -= page_read_bytes;
            zero_bytes -= page_zero_bytes;
            addr += PGSIZE;
            offset += page_read_bytes;
        }
    
        return start_addr;
    }
    - fd로 열린 파일을 offset 바이트로부터 프로세스의 가상 주소 공간의 addr에 length 바이트만큼 매핑하는 것이 목표이기에 load_segment의 로직과 매우 흡사하다.
    - 다른 부분만 알아보도록 하겠다.
    file_reopen(file); - 이 파일에 대한 새로운 파일 디스크립터를 얻기에 다른 매핑에 영향을 주거나 영향을 받지 않는 독립적인 매핑을 가질 수 있다.
    start_addr - 이 함수가 성공하면 파일이 매핑된 가상 주소를 반환한다.
    total_page_count & mapped_page_count - total_page_count : 이 매핑을 위해 사용한 총 페이지 수
    - mapped_page_count : 매핑된 페이지 갯수, 해제 시 사용한다.

    구현2 (Complete)

    void munmap (void *addr);
    - 지정된 주소 범위 addr에 대한 맵핑을 해제한다.
    - 이 주소는 동일한 프로세스에서 이전에 mmap 호출에 의해 반환된 가상 주소여야 하며, 아직 해제되지 않은 상태여야 한다.
    - 프로세스가 종료될 때, exit을 통하든 다른 방법으로든, 모든 맵핑은 암묵적으로 해제된다.
    - 맵핑이 암묵적으로 또는 명시적으로 해제될 때, 프로세스에 의해 작성된 모든 페이지는 파일로 다시 쓰여지며, 작성되지 않은 페이지는 그렇지 않아야 한다.
    - 그런 다음 페이지들은 프로세스의 가상 페이지 목록에서 제거된다.
    - 파일을 닫거나 제거해도 그 파일의 맵핑이 해제되지는 않는다.
    - 맵핑은 생성된 후 munmap이 호출되거나 프로세스가 종료될 때까지 유효하며, 이는 Unix 관례를 따른다.
    - 자세한 내용은 Open File 제거하기를 참조하시오.
        · 각 맵핑에 대해 파일의 별도이고 독립적인 참조를 얻기 위해서는 file_reopen 함수를 사용해야 한다.
    - 두개 이상의 프로세스가 동일한 파일을 맵핑하는 경우, 일관된 데이터를 보는 것이 요구되지 않는다.
    - Unix는 두 맵핑이 동일한 물리 페이지를 공유하도록 하며, mmap 시스템 호출은 페이지가 공유도히는지 아니면 개인적인지(즉, 쓰기 시 복사)를 지정할 수 있는 인자도 가지고 있다.

     

    void do_munmap (void *addr) {
        struct supplemental_page_table *spt = &thread_current()->spt;
        struct page *p = spt_find_page(spt, addr);
        int count = p->mapped_page_count;
        for (int i = 0; i < count; i++) {
            if (p) destroy(p);
            addr += PGSIZE;
            p = spt_find_page(spt, addr);
        }
    }

    구현3 (No touch)

    void vm_file_init (void);

    - 파일 기반 페이지 서브시스템을 초기화한다.

    - 이 함수에서는 파일 기반 페이지와 관련된 모든 것을 설정할 수 있다.

     


    구현4 (No touch)

    bool file_backed_initializer (struct page *page, enum vm_type type, void *kva);

    - 파일 기반 페이지를 초기화한다.

    - 이 함수는 먼저 page->operations에 파일 기반 페이지를 위한 핸들러를 설정한다.

    - 메모리를 지원하는 파일과 같이 페이지 구조체에 일부 정보를 업데이트하고 싶을 수 있다.

     


    구현5 (Complete)

    static void file_backed_destroy (struct page *page);

    - 파일 기반 페이지를 관련 파일을 닫음으로써 파괴한다.

    - 내용이 변경되었다면, 변경 사항을 파일에 다시 써줘야 한다.

    - 이 함수에서 페이지 구조체를 해제할 필요는 없다.

        · file-backed_destroy의 호출자가 처리해야 한다.

     

    static void file_backed_destroy (struct page *page) {
        // page struct를 해제할 필요는 없습니다. (file_backed_destroy의 호출자가 해야 함)
        struct file_page *file_page UNUSED = &page->file;
    
        if (pml4_is_dirty(thread_current()->pml4, page->va)) {
            file_write_at(file_page->file, page->va, file_page->read_bytes, file_page->ofs);
            pml4_set_dirty(thread_current()->pml4, page->va, 0);
        }
        pml4_clear_page(thread_current()->pml4, page->va);
    }
    pml4_is_dirty - 가상 페이지 vpage에 대한 pml4의 PTE(Page Table Entry, 페이지 테이블 항목)가 더티(dirty)인 경우, 즉 PTE가 설치된 이후 페이지가 수정되었다면 true를 반환한다.
    - pml4에 vpage에 대한 PTE가 없는 경우 false를 반환한다.
    file_write_at 1. buffer로부터 size바이트를 file에 쓴다.
    2. 파일 내의 오프셋 file_ofs부터 시작한다.
    3. 실제로 쓰인 바이트 수를 반환하며, 이는 파일의 끝에 도달하면 size보다 적을 수 있다.
    - (보통 이런 경우 파일을 확장하지만, 파일 확장은 아직 구현되지 않았습니다.)
    4. 파일의 현재 위치는 영향을 받지 않는다.
    pml4_set_dirty - pml4에서 가상 페이지 vpage에 대한 PTE의 더티 비트를 0로 설정한다.
    pml4_clear_page - 사용자 가상 페이지 upage를 페이지 디렉토리 PD에서 "존재하지 않음(not present)"으로 표시한다.
    - 이후 해당 페이지에 대한 접근은 오류(fault)를 발생시킬 것이다.
    - 페이지 테이블 항목의 다른 비트는 보존된다. upage는 매핑될 필요가 없다.

    ETC. pintOS 트러블 슈팅

    1. Sync 관련 (project2)

    - 먼저 우리가 구현한 lock_acquire와 lock_release를 재검토할 필요가 있다.
    - 나는 위의 두 함수가 깔끔하게 구현이 안되어있어서 그런지 계속 lock_release 부분에서 에러가 발생했다.
    - 그래서 semaphore를 이용해서 lock처럼 사용해서 이 문제를 해결했다.
        · open, read, write, process_exec의 load 수행 시 동기화를 제어해줘야하기에  이 4부분에 semaphore를 걸어줬다.

    - 그래도 lock 관련 함수는 수정해야 한다.
        · file 관련 함수들 안에서 lock 코드를 사용하기 때문에 lock 관련 코드들을 정상 작동하게 만들어야 한다.
        · 아니면 file_read 함수에서 lock_release 에러가 계속 발생하더라...

     

     

    2. pt-write-code2

    - 테스트 코드를 읽어보면 쉽게 해결할 수 있다.
    int read (int fd, void *buffer, unsigned size) {
        ...
        // project3 : pt-write-code2 test
        struct page *page = spt_find_page(&thread_current()->spt, buffer);
        if (page && !page->writable) {
            sema_up(&filesys_sema);
            exit(-1);
        }
        ...
    }

     

     

    3. pt-grow-stk-sc

    - gitbook의 해당 파트를 읽어보자!

    - 페이지 폴트에 의존하는 겨애우 커널에서 페이지 폴트가 발생하는 다른 경우를 처리해야 한다.
    - 프로세서는 예외로 인해 사용자 모드에서 커널 모드로 전환될 때만 스택 포인터를 저장하므로 전달된 값을 읽으면 사용자는 스택 포인터가 아닌 정의되지 않은 값이 생성된다.
    - 사용자 모드에서 커널 모드로 처음 전환할 때 rsp를 저장하는 것과 다른 방법을 준비해야 한다.

     

    // syscall.c
    void syscall_handler (struct intr_frame *f UNUSED) {
        thread_current()->rsp_stack = f->rsp;
        ...
    }
    // vm.c
    bool vm_try_handle_fault (struct intr_frame *f UNUSED, void *addr UNUSED, bool user UNUSED, bool write UNUSED, bool not_present UNUSED) {
        ...
        if (not_present) {
             void *rsp = f->rsp;
             if (!user) rsp = thread_current()->rsp_stack;
    
         ...
    
    }

     

     

    4. page-merge-...

    - 해당 테스트는 복잡하지만 한번 읽어보자!
        · create - open - write - fork의 순으로 실행되는 것을 볼 수 있는데 create의 동기화도 필요하다.

    * 그래도 page-merge-mm 안될 경우도 있다... 이유는 좀 더 찾아보겠다.
    bool create (const char *file, unsigned initial_size) {
        sema_down(&filesys_sema);
        check_address(file);
        bool result = filesys_create(file, initial_size);
        sema_up(&filesys_sema);
    
        return result;
    }

     

Designed by Tistory.