본문 바로가기

[pintOS] mmap 구현기

@정소민fan2025. 6. 5. 00:15

mmap은 메모리에 파일의 내용을 그대로 옮겨서 매핑한다. 따라서 file_write를 통해 disk I/O가 지속적으로 발생해서 속도가 느려질 걱정이 없다. 우리는 메모리에서 빠르게 내용을 변경하고 mmap 파일이 닫힐 때 그 내용을 파일에 write_back 만 해주면 되는 것이다.

 

우리는 시스템 콜 mmap과 munmap을 구현해야 한다.

void *mmap (void *addr, size_t length, int writable, int fd, off_t offset);
void munmap(void *addr);

그럼 당연히 이들을 syscall_handler에 등록해줘야겠지?

void syscall_handler(struct intr_frame *f UNUSED){
...
    case SYS_MMAP:
        f->R.rax = sys_mmap(arg1, arg2, arg3, arg4, arg5);
        break;
    case SYS_MUNMAP:
        sys_munmap(arg1);
        break;
...
}

mmap 파일이 닫힐 때 write back을 해줘야 한다고 한다. write back이 뭘까? 변경사항을 메모리에만 저장해두고 파일이 닫힐 때만 파일에 변경사항을 쓰는 것을 write back이라고 한다. 그럼 우리가 한 페이지의 write back을 위해 저장해야 할 메타데이터는 어떤 것들이 있을까?

struct file_page

struct file_page
{
    int mapping_count;
    struct file *file;
    off_t offset;
    size_t read_byte;
    size_t zero_byte;
};

아마 이런 것들이 필요하지 않을까? 이 페이지가 어떤 파일에 매핑되었는지, 어느 위치부터 매핑되었는지, 어디까지 읽었고 패딩 크기는 얼마나 되는지 말이다.

 

그러면 mapping_count는 대체 뭘하는 필드지? mmap을 할 때 파일의 크기가 한 페이지 안에서만 논다는 보장은 전혀 없다. 따라서 여러개의 페이지를 하나의 파일을 위해 매핑해야 할 수도 있다. 이 때 페이지의 순서를 나타내기 위한 count이다. 순서가 왜 필요하냐고? 그건 munmap 에서 자세히 다루겠다.

여러개의 페이지 mmap

그럼 여러개의 페이지를 매핑하려면 위의 그림과 같이 매핑하면 되겠지?

아 참고로 보통 낮은 주소에서 높은 주소로 매핑된다고 한다.

mmap

sys_mmap

void *sys_mmap(void *addr, size_t length, int writable, int fd, off_t offset)

이제 본격적으로 mmap을 구현해보자 !! 그 전에 먼저 과제 설명서에 적힌 실패 조건을 잘 살펴보자.

참 많기도 하다. 그리고 과제 설명서에 나와있지 않은, 테스트 케이스에서만 검사하는 실패 조건도 있다 ㅡㅡ

이건 진짜 디버깅 하나하나 해가면서 찾아냈다.

그러면 실패 조건을 먼저 어떻게 검사했는지 알아보자.

/* 파일 입출력에는 mmap이 불가능합니다 */
if (fd < 2)
    return MAP_FAILED;

먼저 콘솔 입출력 !! 이건 뭐 당연히 파일이 아니니까 불가능하다

/* 파일의 사이즈가 0이면 mmap이 불가능합니다 */
int filesize = sys_filesize(fd);
if (filesize == 0 || length == 0)
    return MAP_FAILED;

그리고 파일의 사이즈가 0이면 이것도 실패다. 사이즈가 0인 파일을 open할 수는 있지만 mmap은 안된다. 왜일까?

아마 내 추측으로는 mmap 자체가 disk I/O 없이 빠르게 내용을 수정한다음 한꺼번에 write back을 하려고 하는 것인데, 0인 파일을 mmap 해봤자 수정할 내용이 없어서 그런거 아닐까 싶다.

아니면 매핑할 데이터 자체가 없어서 그런가?

/* 길이가 비정상적일떄 mmap이 불가능합니다 */
if (length > (uintptr_t)addr)
    return MAP_FAILED;

그리고 테스트 중 하나... 무슨 파일 사이즈 1억짜리를 매핑하려 들길래 이를 막아줘야 했다. 비정상적인 파일 길이를 어떻게 체크할까... 했는데, 그냥 주소보다 큰 길이를 매핑하려 들 때 실패하도록 만들었다. 이게 정확한 판단 기준이 될지는 모르겠지만, 테스트는 통과하니 장떙 아닐까?

/* addr이나 offset이 페이지 정렬되어있지 않으면 mmap이 불가능합니다 */
if ((uint64_t)addr == 0 || (uint64_t)addr % PGSIZE != 0 || offset % PGSIZE != 0 || !is_user_vaddr(addr))
    return MAP_FAILED;

여기서는 4가지를 검사한다.

  1. 할당할 주소가 0인가?
  2. 할당할 주소가 페이지 정렬되어있는가?
  3. 오프셋이 페이지 정렬되어있는가?
  4. 할당할 주소가 유저 영역인가?
struct file *target_file = thread_current()->fd_table[fd];
if (target_file == NULL)
    return MAP_FAILED;

당연히 인자로 받은 fd에 파일이 없으면 당연히 실패다.

void *start_page = addr;
void *end_page = pg_round_up(addr + length);

/* 해당 페이지 영역에 이미 할당된 페이지가 있으면 mmap이 불가능 합니다 */
for (; end_page > start_page; start_page += PGSIZE)
{
    if (spt_find_page(&thread_current()->spt, start_page) != NULL)
        return MAP_FAILED;
}

그리고 내가 할당할 영역에 이미 할당되어있는 페이지가 있어도 실패!! 여러 영역에 걸쳐서 매핑해야할 수도 있으니 루프를 돌면서 검사하자.

do_mmap(addr, length, writable, target_file, offset);

return addr;

여기까지 모두 검사하고 나면 드디어 실제 매핑 로직을 호출한다 !! 진짜 검사가 너무 빡세다...

코드 전문은 아래 더보기!!

더보기

코드 전문

void *sys_mmap(void *addr, size_t length, int writable, int fd, off_t offset)
{
    /* 파일 입출력에는 mmap이 불가능합니다 */
    if (fd < 2)
        return MAP_FAILED;

    /* 파일의 사이즈가 0이면 mmap이 불가능합니다 */
    int filesize = sys_filesize(fd);
    if (filesize == 0 || length == 0)
        return MAP_FAILED;

    /* 길이가 비정상적일떄 mmap이 불가능합니다 */
    if (length > (uintptr_t)addr)
        return MAP_FAILED;

    /* addr이나 offset이 페이지 정렬되어있지 않으면 mmap이 불가능합니다 */
    if ((uint64_t)addr == 0 || (uint64_t)addr % PGSIZE != 0 || offset % PGSIZE != 0 || !is_user_vaddr(addr))
        return MAP_FAILED;

    /* 해당 fd에 파일이 없으면 mmap이 불가능합니다 */
    struct file *target_file = thread_current()->fd_table[fd];
    if (target_file == NULL)
        return MAP_FAILED;

    void *start_page = addr;
    void *end_page = pg_round_up(addr + length);

    /* 해당 페이지 영역에 이미 할당된 페이지가 있으면 mmap이 불가능 합니다 */
    for (; end_page > start_page; start_page += PGSIZE)
    {
        if (spt_find_page(&thread_current()->spt, start_page) != NULL)
            return MAP_FAILED;
    }

    do_mmap(addr, length, writable, target_file, offset);

    return addr;
}

그러면 이제 실제로 페이지를 매핑해줄 do_mmap을 구현해보자.

do_mmap

void *do_mmap(void *addr, size_t length, int writable,
			  struct file *file, off_t offset)

 

 

 

 

여기서 로직을 직접 작성하자 !!

/* 지연 로딩과 스왑 시의 백업 정보 저장 */
off_t file_size = file_length(file);
off_t read_size = file_size - offset;
if (read_size < 0)
    read_size = 0;
size_t remain_length = (size_t)read_size;
void *cur_addr = addr;
off_t cur_offset = offset;
struct file *reopen_file = file_reopen(file);
int mapping_count = 0; /* mmap은 여러 페이지에 걸쳐 매핑될 수 있습니다.
munmap 시에 어디까지 해제해줄 것인지 판단할 기준이 됩니다 */

먼저 파일 사이즈를 가져온다. 하지만 이 파일 사이즈가 곧 우리가 읽어야할 길어가 되지는 않는다. 파일의 중간부터 매핑될 수도 있기 때문이다.

여기서 주의할 점은 file_length로 가져온 off_t는 signed int 형이다. 하지만 여기서 루프를 돌면서 남은 길이를 확인할 remain_length는 unsinged 형인 size_t이다. 따라서 타입 캐스팅을 할 때 음수를 unsinged 형으로 캐스팅해버리면 이상하게 큰 수가 나타나버린다.

근데 왜 remain_length를 off_t나 int형이 아닌 size_t로 했냐고?? 글쎄... 아마 이 메타데이터를 넣어줄 구조체의 자료형이 size_t여서 그랬었는데... 지금 보니까 굳이 size_t로 할 필요가 없었던 것 같다. 그냥 int로 만들어주는게 훨 나아보이지만... 이미 작동하는 코드는 건드리지 말라는 성현의 말씀이 있었다.

 

여기서 나온 변수들 몇몇개를 대강 설명하자면,

  1. remain_length : 루프를 위해 사용되는 매핑되고 남은 길이
  2. cur_addr : 매핑할 현재 주소
  3. cur_offset : 매핑할 파일의 오프셋
  4. reopen_file : 매핑할 파일. 무조건 reopen으로 다시 열어줘야 한다 !!
while (remain_length > 0)
{
    /* 남은 mmap 매핑 길이가 4KB보다 크면 4KB로 맞춥니다 */
    size_t allocate_length = remain_length > PGSIZE ? PGSIZE : remain_length;
    /* 지연 로딩 시 필요한 정보를 생성합니다 */
    struct lazy_load_info *info = make_info(reopen_file, cur_offset, allocate_length);
    struct mmap_info *mmap_info = make_mmap_info(info, mapping_count);
    void *aux = mmap_info;

    /* mmap 또한 지연 로딩이 필요합니다 */
    vm_alloc_page_with_initializer(VM_MMAP, cur_addr, writable, lazy_load_segment, aux);
    /* remain_length는 unsigned 형입니다. 따라서 음수가 없기에 미리 break를 해줘야 합니다 */
    if (remain_length < PGSIZE)
        break;
    remain_length -= PGSIZE;
    cur_addr += PGSIZE;
    cur_offset += PGSIZE;
    mapping_count++;
}

이제 실제로 mmap 을 미초기화 페이지로 매핑해주자. 한 페이지는 4KB니까 남은 길이가 4KB보다 크면 다운시켜서 4KB로 맞춰주자. 그 다음 메타데이터들을 aux에 저장하기 위해 구조체를 만들어주고, vm_alloc_with_initializer를 통해 미초기화 페이지를 만들어주자. 여기서 lazy_load_segment가 들어가는데, 사실 pintOS 코드의 의도는 메모리 매핑 파일을 위한 함수를 하나 만들어서 거기서 aux에 있는 메타데이터를 통해 초기화를 진행하라는 것 같았지만, 같은 동작을 가진 함수를 한개 더 만들기 싫어서 lazy_load_segment 를 static에서 전역으로 만들고 process.h에 추가해주었다.

 

마지막으로 다음 루프를 위해 cur_addr과 cur_offset에는 PGSIZE를 더해주고,  remain_length에는 PGSIZE만큼 빼주자. 그리고 mapping_count를 증감시켜서 같은 파일 매핑임을 알려주자.

 

전체 코드와 위에서 사용된 구조체, 다른 함수들은 더보기에 !!

더보기

do_mmap 전문

void *do_mmap(void *addr, size_t length, int writable,
			  struct file *file, off_t offset)
{
    /* 지연 로딩과 스왑 시의 백업 정보 저장 */
    off_t file_size = file_length(file);
    off_t read_size = file_size - offset;
    if (read_size < 0)
        read_size = 0;
    size_t remain_length = (size_t)read_size;
    void *cur_addr = addr;
    off_t cur_offset = offset;
    struct file *reopen_file = file_reopen(file);
    int mapping_count = 0; /* mmap은 여러 페이지에 걸쳐 매핑될 수 있습니다.
    munmap 시에 어디까지 해제해줄 것인지 판단할 기준이 됩니다 */

    while (remain_length > 0)
    {
        /* 남은 mmap 매핑 길이가 4KB보다 크면 4KB로 맞춥니다 */
        size_t allocate_length = remain_length > PGSIZE ? PGSIZE : remain_length;
        /* 지연 로딩 시 필요한 정보를 생성합니다 */
        struct lazy_load_info *info = make_info(reopen_file, cur_offset, allocate_length);
        struct mmap_info *mmap_info = make_mmap_info(info, mapping_count);
        void *aux = mmap_info;

        /* mmap 또한 지연 로딩이 필요합니다 */
        vm_alloc_page_with_initializer(VM_MMAP, cur_addr, writable, lazy_load_segment, aux);
        /* remain_length는 unsigned 형입니다. 따라서 음수가 없기에 미리 break를 해줘야 합니다 */
        if (remain_length < PGSIZE)
            break;
        remain_length -= PGSIZE;
        cur_addr += PGSIZE;
        cur_offset += PGSIZE;
        mapping_count++;
    }
}

struct mmap_info / struct lazy_load_info

struct lazy_load_info
{
    struct file *file;
    off_t offset;
    size_t readbyte;
    size_t zerobyte;
};

struct mmap_info
{
    struct lazy_load_info *info;
    int mapping_count;
};

make_info / make_mmap_info

struct lazy_load_info *make_info(
	struct file *file, off_t offset, size_t read_byte)
{
    struct lazy_load_info *info = malloc(sizeof(struct lazy_load_info));
    info->file = file;
    info->offset = offset;
    info->readbyte = read_byte;
    info->zerobyte = PGSIZE - read_byte;
    return info;
}

struct mmap_info *make_mmap_info(struct lazy_load_info *info, int mapping_count)
{
    struct mmap_info *mmap_info = malloc(sizeof(struct mmap_info));
    mmap_info->info = info;
    mmap_info->mapping_count = mapping_count;
    return mmap_info;
}

munmap

이제 매핑된 mmap 페이지를 해제해주는 로직도 필요하다. 처음에 든 의문은 "munmap에서 만약 매핑된 영역의 중간부터 해제하라고 요청이 들어오면 어떻게 해야되는가?" 였다. 중간부터 해제해줘야하나? 아니면 영역의 내부이긴 하니까 영역의 처음부터 다 해제해줘야 하나? 답은 코치님과 테스트 케이스를 통해 알았는데, 무조건 매핑 영역의 처음 주소를 준다고 한다. 그래서 이런 복잡한 로직까지는 신경쓰지 않고 구현하면 된다.

do_mnumap

구현하기 전에 알아둬야 할 것이 있다. 위 그림처럼 매핑이 되어있다고 할 때 0x10000 ~ 0x13000 영역과 0x13000 ~ 0x15000 까지는 서로 다른 영역이다. 하지만 서로 같은 파일을 매핑하고 있고, offset까지도 이쁘게 정렬되어있다 !! 그러면 이 둘이 다른 영역이라는 것을 어떻게 확인할 것인가? 여기서 사용되는게 바로 mapping_count이다. 0x13000 경계에서 mapping_count에서 2에 3이 되는게 아니라 0이 되어버렸다. 이로 인해 이 경계는 서로 다른 mmap 영역이라는 것을 알 수 있다 !! 이거 완전 씽크빅인데?

struct page *target_page = spt_find_page(spt, addr);
if (target_page == NULL)
    return;

먼저 해당 영역에 해제할만한 페이지가 있는지부터 확인하자 !!

struct file_page *file_page = &target_page->file;
struct file *target_file = file_page->file;
int target_mmap_count = file_page->mapping_count;

그 다음에 해당 page에 저장된 메타데이터를 통해 첫번째 페이지의 정보를 가져오자. 우리는 이 정보를 통해 다음 주소의 페이지들이 이 첫번째 페이지와 같은 영역인지 확인할 것이다.

addr += PGSIZE; // 다음 mmap_file 찾기
/* while 루프를 통해 연속된 페이지가 같은 mmap 영역인지 확인합니다 */
while (is_my_mmap(addr, target_file, ++target_mmap_count))
{
    struct page *remove_page = spt_find_page(spt, addr);
    spt_remove_page(spt, remove_page);
    addr += PGSIZE;
}
/* 첫번째 주소를 해제합니다 */
spt_remove_page(spt, target_page);

그리고 이제 루프문을 돌면서 조건 함수 is_my_mmap을 통해 같은 매핑 영역인지 확인하고, 같다면 spt_remove_page를 통해 제거해주자.

그리고 루프문이 끝나고 나서야 첫번째 페이지를 없애주자. 루프 이전에 미리 없애면 첫번째 페이지에서 가져온 비교 file이 없어진다.

is_my_mmap

static bool is_my_mmap(void *addr, struct file *mmap_file, int mmap_count)
{
    struct supplemental_page_table *spt = &thread_current()->spt;
    struct page *find_page = spt_find_page(spt, addr);
    if (find_page == NULL)
        return false;

    struct file *find_file = find_page->file.file;
    /* 파일이 NULL이거나 인자로 받은 파일과 다른가? */
    /* NULL이거나 다르면 다른 페이지임 !! */
    if (find_file == NULL || find_file != mmap_file)
        return false;

    /**같은 파일이어도 매핑 카운트가 맞지 않으면
     * 서로 다른 페이지임 !!
     */
    if (find_page->file.mapping_count != mmap_count)
        return false;

    return true;
}

이건 그냥 검사하는 함수. 루프문에서 전치 증감 연산자를 통해 미리 증가시켜서 mmap_count를 보냈기에 다음 페이지와 비교가 가능하다.

코드 전문은 더보기 !!

더보기
void do_munmap(void *addr)
{
    struct supplemental_page_table *spt = &thread_current()->spt;
    /* SPT에 없으면 해제할 수 없습니다 !! */
    /* 항상 mmap 영역의 첫번째 주소를 준다고 합니다 */
    struct page *target_page = spt_find_page(spt, addr);
    if (target_page == NULL)
        return;

    struct file_page *file_page = &target_page->file;
    struct file *target_file = file_page->file;
    int target_mmap_count = file_page->mapping_count;

    addr += PGSIZE; // 다음 mmap_file 찾기
    /* while 루프를 통해 연속된 페이지가 같은 mmap 영역인지 확인합니다 */
    while (is_my_mmap(addr, target_file, ++target_mmap_count))
    {
        struct page *remove_page = spt_find_page(spt, addr);
        spt_remove_page(spt, remove_page);
        addr += PGSIZE;
    }
    /* 첫번째 주소를 해제합니다 */
    spt_remove_page(spt, target_page);
}

하지만 여기서 끝이 아니다 !!

우리는 이 페이지가 destroy될 때 write_back을 해주어야만 한다. 하지만 이는 swap-in/out 에서 다룰 것이기 때문에 지금은 코드만 올려두고 설명은 다음 포스팅에서 하겠다.

file_backed_destroy 코드는 더보기에

더보기
/* 파일 기반 페이지를 소멸시킵니다. PAGE는 호출자가 해제합니다. */
static void
file_backed_destroy(struct page *page)
{
    // file_page는 file-backed 페이지에 대한 메타데이터를 담고 있는 구조체
    struct file_page *file_page UNUSED = &page->file;
    /** TODO: dirty_bit 확인 후 write_back
     * pml4_is_dirty를 사용해서 dirty bit 확인
     * write back을 할 때는 aux에 저장된 파일 정보를 사용
     * file_write를 사용하면 될 것 같아요
     */
    struct thread *curr = thread_current();

    // 파일을 스기 가능하게 설정 → read_only로 열렸을 수도 있으므로
    file_allow_write(file_page->file);

    // 페이지가 dirty 상태 → 메모리 상에서 파일 내용이 수정됨
    if (pml4_is_dirty(thread_current()->pml4, page->va))
    {
        lock_acquire(&filesys_lock);
        off_t written = file_write_at(file_page->file,		// mmap으로 매핑된 파일 객체
                                      page->frame->kva,		// 물리 메모리 상 해당 페이지의 커널 주소
                                      file_page->read_byte, // 실제로 파일에 쓸 바이트 수
                                      file_page->offset);	// mmap할 때 저장된 파일 내부의 오프셋 위치
        lock_release(&filesys_lock);
        ASSERT(written == file_page->read_byte);

        // dirty bit를 false로 초기화(더 이상 수정 X)
        pml4_set_dirty(curr->pml4, page->va, false);
    }

    // 해당 페이지가 물리 프레임에 매핑되어 있으면
    if (page->frame != NULL && page->frame->ref_cnt < 1)
    {
        // 물리 페이지를 해제하고, frame 구조체도 동적 메모리 해제
        palloc_free_page(page->frame->kva);
        free(page->frame);
        page->frame = NULL;
    }

    // 최종적으로 사용자 가상 주소 공간에서 해당 페이지 매핑을 제거
    pml4_clear_page(curr->pml4, page->va);
}

 

'크래프톤 정글' 카테고리의 다른 글

[pintOS] Copy-on-write 구현기  (4) 2025.06.07
[pintOS] swap-in/out 구현기  (5) 2025.06.05
[pintOS] vm 트러블슈팅  (1) 2025.06.04
[pintOS] stack_growth 구현기  (0) 2025.06.03
[pintOS] uninit 페이지 구현기  (0) 2025.06.03
정소민fan
@정소민fan :: 코딩은 관성이야

코딩은 관성적으로 해야합니다 즐거운 코딩 되세요

목차