[Pintos-KAIST] Project 3 :: Memory Mapped Files

2023. 5. 22. 05:51Computer Science

728x90
반응형

Memory-mapped page

Memory-mapped page는 Anonymous page와 달리 파일을 기반으로 매핑하는 페이지이다.
즉, 이 페이지에 담기는 내용은 실제 디스크에 존재하는 파일의 데이터가 담긴다.

 

🤔 왜 매핑을 해야 하지?
파일과 메모리를 매핑하면, 파일을 메모리처럼 접근할 수 있게 되어서 
파일 입출력의 효율성을 향상할 수 있다.
이렇게 파일과 매핑된 메모리 영역은 가상 주소 공간에서 접근 가능하며, 해당 영역에 대한 읽기/쓰기 작업이 가능해진다.
쓰기 작업으로 인해 파일의 내용이 변경되면 매핑이 해제(unmap)될 때 디스크의 실제 파일에 해당 변경 사항이 반영된다.

 

Mapping

파일과 메모리 매핑은 System call인 mmap()을 통해 이루어진다.
Pintos에서는 vm/file.c의 do_mmap()을 이용해서 매핑을 수행하는데, 그 이전에 mmap()에 전달된 인자들에 대한 검증이 먼저 이루어져야 한다.

 

시스템 콜 mmap()

매핑이 이루어질 수 없는 경우는 아래와 같다.

  • addr가 없는 경우
  • addr가 page-aligned 되지 않은 경우
  • offset이 page-aligned되지 않은 경우
  • addr가 user 영역이 아닌 경우
  • addr + length가 user 영역이 아닌 경우
    (addr부터 파일의 내용을 매핑해나가면 마지막 주소는 addr+length가 되는데, 이 주소도 user 영역 안에 존재해야 한다.)
  • addr에 할당된 페이지가 이미 존재하는 경우
  • fd에 해당하는 file이 없는 경우
  • file의 길이가 0이거나 0보다 작은 경우

 

위 경우를 제외하고, do_mmap()을 호출해서 파일과 가상 주소 간의 매핑을 진행하고, 매핑된 가상 주소를 반환한다.

/* syscall.c */

void syscall_handler(struct intr_frame *f UNUSED)
{
    int syscall_n = f->R.rax; /* 시스템 콜 넘버 */
#ifdef VM
    thread_current()->rsp = f->rsp;
#endif
    switch (syscall_n)
    {
    ...

    case SYS_MMAP:
        f->R.rax = mmap(f->R.rdi, f->R.rsi, f->R.rdx, f->R.r10, f->R.r8);
        break;

    }
}

void *mmap(void *addr, size_t length, int writable, int fd, off_t offset)
{
    if (!addr || addr != pg_round_down(addr))
        return NULL;

    if (offset != pg_round_down(offset))
        return NULL;

    if (!is_user_vaddr(addr) || !is_user_vaddr(addr + length))
        return NULL;

    if (spt_find_page(&thread_current()->spt, addr))
        return NULL;

    struct file *f = process_get_file(fd);
    if (f == NULL)
        return NULL;

    if (file_length(f) == 0 || (int)length <= 0)
        return NULL;

    return do_mmap(addr, length, writable, f, offset); // 파일이 매핑된 가상 주소 반환
}

 

do_mmap()

lazy loading으로 할당

file-backed page도 anonymous page와 마찬가지로 lazy loading을 이용한다.
page가 초기화될 때 파일에서 내용을 읽어올 수 있도록 lazy_load_segment와 로딩할 때 필요한 데이터를 페이지에 저장해두어야 한다.

 

파일에 대한 독립적인 참조

유닉스 규칙에 따라 매핑은 매핑을 해제하는 munmap이 호출되거나 프로세스가 종료될 때까지 유효하다. (파일을 닫거나 제거해도 매핑은 해제되지 않는다.)

 

동일한 파일에 대해 여러 프로세스에 의해 여러 개의 매핑이 존재한다고 생각해 보자.
이때 한 프로세스에서 이 파일을 닫거나 제거하면 다른 프로세스에서도 이 매핑된 영역에 대한 참조가 유효하지 않게 되어버린다!
또한, 한 매핑에서 파일의 내용을 변경하면 다른 매핑에도 영향을 주게 된다.

 

file_reopen 함수를 사용하면 이 파일에 대한 새로운 파일 디스크립터를 얻게 되어 다른 매핑에 영향을 주거나 영향을 받지 않는 독립적인 매핑을 가질 수 있게 된다.

 

여러 페이지에 매핑되는 파일 처리

읽어올 길이인 length가 한 페이지 사이즈를 넘어갈 경우에는 한 파일의 내용이 여러 페이지에 걸쳐서 매핑된다.
매핑을 진행할 때는 간단하게 (페이지 주소 addr + PGSIZE에 위치하는) 다음 페이지에 이어서 매핑하면 되지만, 매핑을 해제할 때는 몇 번째 페이지까지 해제해야 하는지를 알아야 한다. 따라서 총 몇 페이지를 사용해서 매핑이 이루어졌는지에 대한 정보도 page 구조체에 함께 저장해 둔다.

 

이렇게 위에서 설정한 정보들로 addr에 페이지가 할당되고, addr에 첫 번째로 접근할 때 page fault가 발생하면서 저장해 둔 정보를 통해서 파일에서 내용을 읽어와 매핑되어 있는 메모리에 로딩된다.

 

process.c의 load_segment를 참고해서 구현했다.

/* file.c */
void *
do_mmap(void *addr, size_t length, int writable,
        struct file *file, off_t offset)
{
    struct file *f = file_reopen(file);
    void *start_addr = addr; // 매핑 성공 시 파일이 매핑된 가상 주소 반환하는 데 사용
    // 이 매핑을 위해 사용한 총 페이지 수
    int total_page_count = length <= PGSIZE ? 1 : length % PGSIZE ? length / PGSIZE + 1
                                                                  : length / PGSIZE; 

    size_t read_bytes = file_length(f) < length ? file_length(f) : length;
    size_t zero_bytes = PGSIZE - read_bytes % PGSIZE;

    ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
    ASSERT(pg_ofs(addr) == 0);      // upage가 페이지 정렬되어 있는지 확인
    ASSERT(offset % PGSIZE == 0); // ofs가 페이지 정렬되어 있는지 확인

    while (read_bytes > 0 || zero_bytes > 0)
    {
        /* 이 페이지를 채우는 방법을 계산합니다.
        파일에서 PAGE_READ_BYTES 바이트를 읽고
        최종 PAGE_ZERO_BYTES 바이트를 0으로 채웁니다. */
        size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
        size_t page_zero_bytes = PGSIZE - page_read_bytes;

        struct lazy_load_arg *lazy_load_arg = (struct lazy_load_arg *)malloc(sizeof(struct lazy_load_arg));
        lazy_load_arg->file = f;
        lazy_load_arg->ofs = offset;
        lazy_load_arg->read_bytes = page_read_bytes;
        lazy_load_arg->zero_bytes = page_zero_bytes;

        // vm_alloc_page_with_initializer를 호출하여 대기 중인 객체를 생성합니다.
        if (!vm_alloc_page_with_initializer(VM_FILE, addr,
                                            writable, lazy_load_segment, lazy_load_arg))
            return NULL;

        struct page *p = spt_find_page(&thread_current()->spt, start_addr);
        p->mapped_page_count = total_page_count;

        /* Advance. */
        // 읽은 바이트와 0으로 채운 바이트를 추적하고 가상 주소를 증가시킵니다.
        read_bytes -= page_read_bytes;
        zero_bytes -= page_zero_bytes;
        addr += PGSIZE;
        offset += page_read_bytes;
    }

    return start_addr;
}

 

Unmapping

매핑을 끊을 때, 수정사항이 있다면 기존의 파일에 수정된 사항을 반영해야 하므로 파일에 대한 정보를 페이지가 가지고 있어야 한다.
수정사항을 반영한 뒤에는 프로세스의 가상 페이지 목록에서 페이지를 제거해서 매핑을 해제한다.

 

file_backed_initializer()

수정사항을 파일에 다시 기록하기 위해서는 매핑을 해제하는 시점에 해당 페이지에 매핑된 파일의 정보를 알 수 있어야 한다.
이를 위해 file-backed page가 초기화될 때 호출되는 file_backed_initializer에서 파일에 대한 정보를 page 구조체에 추가해 둔다.

/* file.c */

bool file_backed_initializer(struct page *page, enum vm_type type, void *kva)
{
    /* Set up the handler */
    // 먼저 page->operations에 file-backed pages에 대한 핸들러를 설정합니다.
    page->operations = &file_ops;

    struct file_page *file_page = &page->file;

    // todo: page struct의 일부 정보(such as 메모리가 백업되는 파일과 관련된 정보)를 업데이트할 수도 있습니다.
    struct lazy_load_arg *lazy_load_arg = (struct lazy_load_arg *)page->uninit.aux;
    file_page->file = lazy_load_arg->file;
    file_page->ofs = lazy_load_arg->ofs;
    file_page->read_bytes = lazy_load_arg->read_bytes;
    file_page->zero_bytes = lazy_load_arg->zero_bytes;
}

 

file_backed_destroy()

프로세스가 종료될 때도 마찬가지로 매핑이 해제되어야 하므로, 수정사항을 파일에 반영하고 가상 페이지 목록에서 페이지를 제거한다.

/* file.c */

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

 

munmap() 시스템 콜

/* syscall.c */

void syscall_handler(struct intr_frame *f UNUSED)
{
    int syscall_n = f->R.rax; /* 시스템 콜 넘버 */
#ifdef VM
    thread_current()->rsp = f->rsp;
#endif
    switch (syscall_n)
    {
    ...

    case SYS_MUNMAP:
        munmap(f->R.rdi);
        break;

    }
}

void munmap(void *addr)
{
    do_munmap(addr);
}

 

do_munmap()

같은 파일이 매핑된 페이지가 모두 해제될 수 있도록, 매핑할 때 저장해 둔 매핑된 페이지 수를 이용한다.
destroy 매크로에 연결되어 있는 file_backed_destroy 함수에서 파일의 수정사항을 반영하고 가상 페이지 목록에서 페이지를 제거하도록 해두고, 매핑을 해제할 때 이 함수를 호출해서 사용한다.

/* Do the munmap */
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);
    }
}

 

supplemental_page_table_copy()

자식 프로세스가 부모 프로세스의 실행 컨텍스트를 상속하는 과정에서 SPT를 복사할 때, file_backed_page는 자식 페이지가 부모의 프레임과 매핑되도록 분기를 추가한다.

/* vm.c */

bool supplemental_page_table_copy(struct supplemental_page_table *dst UNUSED,
                                  struct supplemental_page_table *src UNUSED)
{

...
        /* 2) type이 file이면 */
        if (type == VM_FILE)
        {
            struct lazy_load_arg *file_aux = malloc(sizeof(struct lazy_load_arg));
            file_aux->file = src_page->file.file;
            file_aux->ofs = src_page->file.ofs;
            file_aux->read_bytes = src_page->file.read_bytes;
            file_aux->zero_bytes = src_page->file.zero_bytes;
            if (!vm_alloc_page_with_initializer(type, upage, writable, NULL, file_aux))
                return false;
            struct page *file_page = spt_find_page(dst, upage);
            file_backed_initializer(file_page, type, NULL);
            file_page->frame = src_page->frame;
            pml4_set_page(thread_current()->pml4, file_page->va, src_page->frame->kva, src_page->writable);
            continue;
        }

...

    }
    return true;
}

 

여기까지 하면 mmap 관련 테스트가 통과한다!

pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
pass tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
pass tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
pass tests/userprog/fork-once
pass tests/userprog/fork-multiple
pass tests/userprog/fork-recursive
pass tests/userprog/fork-read
pass tests/userprog/fork-close
pass tests/userprog/fork-boundary
pass tests/userprog/exec-once
pass tests/userprog/exec-arg
pass tests/userprog/exec-boundary
pass tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
pass tests/userprog/exec-read
pass tests/userprog/wait-simple
pass tests/userprog/wait-twice
pass tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
pass tests/userprog/multi-recurse
pass tests/userprog/multi-child-fd
pass tests/userprog/rox-simple
pass tests/userprog/rox-child
pass tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
pass tests/vm/pt-grow-stack
pass tests/vm/pt-grow-bad
pass tests/vm/pt-big-stk-obj
pass tests/vm/pt-bad-addr
pass tests/vm/pt-bad-read
pass tests/vm/pt-write-code
pass tests/vm/pt-write-code2
pass tests/vm/pt-grow-stk-sc
pass tests/vm/page-linear
pass tests/vm/page-parallel
pass tests/vm/page-merge-seq
pass tests/vm/page-merge-par
pass tests/vm/page-merge-stk
pass tests/vm/page-merge-mm
pass tests/vm/page-shuffle
pass tests/vm/mmap-read
pass tests/vm/mmap-close
pass tests/vm/mmap-unmap
pass tests/vm/mmap-overlap
pass tests/vm/mmap-twice
pass tests/vm/mmap-write
pass tests/vm/mmap-ro
pass tests/vm/mmap-exit
pass tests/vm/mmap-shuffle
pass tests/vm/mmap-bad-fd
pass tests/vm/mmap-clean
pass tests/vm/mmap-inherit
pass tests/vm/mmap-misalign
pass tests/vm/mmap-null
pass tests/vm/mmap-over-code
pass tests/vm/mmap-over-data
pass tests/vm/mmap-over-stk
pass tests/vm/mmap-remove
pass tests/vm/mmap-zero
pass tests/vm/mmap-bad-fd2
pass tests/vm/mmap-bad-fd3
pass tests/vm/mmap-zero-len
pass tests/vm/mmap-off
pass tests/vm/mmap-bad-off
pass tests/vm/mmap-kernel
pass tests/vm/lazy-file
pass tests/vm/lazy-anon
FAIL tests/vm/swap-file
FAIL tests/vm/swap-anon
FAIL tests/vm/swap-iter
pass tests/vm/swap-fork
pass tests/filesys/base/lg-create
pass tests/filesys/base/lg-full
pass tests/filesys/base/lg-random
pass tests/filesys/base/lg-seq-block
pass tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
pass tests/filesys/base/sm-full
pass tests/filesys/base/sm-random
pass tests/filesys/base/sm-seq-block
pass tests/filesys/base/sm-seq-random
pass tests/filesys/base/syn-read
pass tests/filesys/base/syn-remove
pass tests/filesys/base/syn-write
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
FAIL tests/vm/cow/cow-simple
4 of 141 tests failed.
728x90
반응형