개발/프로젝트

Pintos Project 3 VM 5편: mmap과 File-backed Page

cedis 2026. 5. 20. 10:57
Project 3 VM 시리즈 5편

파일을 주소공간에 연결하고, fault와 dirty bit 기준으로 읽기와 write-back을 처리하는 흐름을 정리한다.

이번 글에서 다루는 것

mmap은 파일 내용을 단순히 read로 복사하는 기능이 아니다. 파일의 일정 구간을 유저 주소공간에 연결하고, page fault와 dirty bit를 기준으로 읽기와 되쓰기를 나누는 VM 기능이다.

  • mmap 요청을 syscall에서 어떤 조건으로 거르는지 본다.
  • file-backed page가 어떤 metadata를 들고 있는지 확인한다.
  • lazy file page가 fault 시점에 읽히고, munmap/eviction 시점에 dirty page만 파일에 쓰이는 흐름을 정리한다.
기준 코드
글의 기준은 week11-team-03-pintos-vm의 pair-a 브랜치다. COW는 이번 구현 범위에서 제외한다. 아래 코드는 전체 파일을 통째로 복붙하기 위한 목록이 아니라, diff에서 의미가 바뀐 핵심 블록을 따라 읽기 위한 기준점이다.

1. mmap은 파일을 메모리에 붙이는 약속이다

read는 파일 내용을 버퍼로 복사한다. 반면 mmap은 파일 offset과 유저 주소 범위를 연결한다. 프로그램이 그 주소를 읽으면 page fault를 통해 파일을 읽고, 그 주소를 수정하면 나중에 dirty bit를 보고 파일에 다시 쓴다.

이전 상태의 한계
read/write만 있으면 파일 내용을 매번 syscall로 복사해야 한다. VM 입장에서는 그 메모리가 어떤 파일 구간에서 왔는지 모른다.
변경 후 생긴 기준
mmap은 page마다 file, offset, read_bytes, zero_bytes를 기록한다. 그래서 fault 시 읽고, dirty하면 munmap이나 eviction 때 되쓸 수 있다.
mmap파일의 일부를 유저 가상 주소 범위에 연결하는 시스템콜이다.
File-backed Page원본 파일과 연결된 page다. 필요하면 파일에서 읽고, 수정되면 파일에 다시 쓴다.
Dirty Bitpage가 메모리에 올라온 뒤 수정되었는지 알려주는 비트다.
munmapmmap으로 연결된 범위를 해제하고 필요한 write-back을 수행하는 시스템콜이다.
mmap의 핵심 연결
file offset파일의 몇 번째 바이트부터 page에 대응되는지
mmap auxfault 전까지 필요한 metadata를 보관
file_pagefault 후 page 안에 저장되는 file-backed metadata
dirty write-back수정된 page만 파일에 다시 기록

2. syscall 입구에서 먼저 걸러야 하는 조건

mmap은 주소공간을 직접 건드리기 때문에 입구 검사가 중요하다. 주소 정렬, 길이, 파일 descriptor, 기존 SPT와의 겹침을 먼저 거른 뒤 실제 mapping으로 넘어간다.

case SYS_MMAP: {
	void *addr = (void *) f->R.rdi;
	size_t length = (size_t) f->R.rsi;
	int writable = (int) f->R.rdx;
	int fd = (int) f->R.r10;
	off_t offset = (off_t) f->R.r8;
	uint64_t start = (uint64_t) addr;
	uint64_t end = start + length;
	bool overlap = false;
	struct file *file;

	f->R.rax = (uint64_t) NULL;
	if (addr == NULL || pg_ofs (addr) != 0)
		break;
	if (offset < 0 || offset % PGSIZE != 0)
		break;
	if (length == 0 || process_get_fd_type (fd) != PROCESS_FD_FILE)
		break;

	file = process_get_file (fd);
	if (file == NULL || end < start)
		break;
	if (!is_user_vaddr (addr) || !is_user_vaddr ((void *) (end - 1)))
		break;

	for (uint64_t page_addr = start; page_addr < end; page_addr += PGSIZE) {
		if (spt_find_page (&thread_current ()->spt, (void *) page_addr) != NULL) {
			overlap = true;
			break;
		}
	}
	if (overlap)
		break;

	f->R.rax = (uint64_t) do_mmap (addr, length, writable, file, offset);
	break;
}
코드에서 먼저 볼 줄
  • pg_ofs(addr) != 0는 page-aligned 주소만 허용하기 위한 검사다.
  • offset % PGSIZE != 0도 파일 offset을 page 기준으로 맞추기 위한 조건이다.
  • process_get_fd_type(fd)는 mmap 대상이 실제 file descriptor인지 확인한다.
  • spt_find_page 반복문은 이미 등록된 page와 mmap 범위가 겹치는지 확인한다.

3. file-backed page metadata

mmap page는 파일에서 어디를 읽어야 하는지 기억해야 한다. fault 전에는 aux가 이 정보를 들고 있고, fault 후에는 page->file 쪽으로 옮겨진다.

struct file_page {
	struct file *file;
	off_t offset;
	size_t read_bytes;
	size_t zero_bytes;
	void *map_base;
	size_t page_count;
};

struct mmap_aux {
	struct file *file;
	off_t offset;
	size_t read_bytes;
	size_t zero_bytes;
	void *map_base;
	size_t page_count;
};

4. do_mmap은 page를 등록하고, 읽기는 미룬다

do_mmap()은 파일 내용을 바로 읽지 않는다. page마다 file reference를 복제하고 aux를 만든 뒤, VM_FILE lazy page로 등록한다. 이 점은 2편의 lazy loading과 같은 구조다.

void *
do_mmap (void *addr, size_t length, int writable,
		struct file *file, off_t offset) {
	size_t page_count = DIV_ROUND_UP (length, PGSIZE);
	off_t current_offset = offset;
	size_t remaining_file_bytes;
	void *upage = addr;

	lock_acquire (&filesys_lock);
	off_t file_len = file_length (file);
	lock_release (&filesys_lock);

	if (file_len <= 0 || offset >= file_len)
		return NULL;

	remaining_file_bytes = file_len - offset;
	if (remaining_file_bytes > length)
		remaining_file_bytes = length;

	for (size_t i = 0; i < page_count; i++) {
		struct mmap_aux *aux = malloc (sizeof *aux);
		struct file *opened_file = file_duplicate (file);

		aux->file = opened_file;
		aux->offset = current_offset;
		aux->read_bytes = remaining_file_bytes < PGSIZE
				? remaining_file_bytes : PGSIZE;
		aux->zero_bytes = PGSIZE - aux->read_bytes;
		aux->map_base = addr;
		aux->page_count = page_count;

		if (!vm_alloc_page_with_initializer (VM_FILE, upage, writable,
				lazy_load_file_page, aux))
			return NULL;

		upage += PGSIZE;
		current_offset += aux->read_bytes;
		remaining_file_bytes -= aux->read_bytes;
	}
	return addr;
}
mmap의 시간 순서
1
syscall에서 mmap 인자를 검증한다.
2
page마다 mmap_aux를 만들어 파일 위치와 읽을 크기를 저장한다.
3
SPT에 VM_FILE lazy page를 등록한다.
4
처음 접근하면 lazy_load_file_page가 파일을 읽고 page->file metadata를 채운다.
5
munmap 또는 eviction 때 dirty bit를 보고 필요한 page만 파일에 다시 쓴다.

5. fault 시 읽고, dirty할 때만 파일에 쓴다

static bool
lazy_load_file_page (struct page *page, void *aux_) {
	struct mmap_aux *aux = aux_;
	void *kva = page->frame->kva;

	file_seek (aux->file, aux->offset);
	if (file_read (aux->file, kva, aux->read_bytes)
			!= (int) aux->read_bytes)
		return false;

	memset (kva + aux->read_bytes, 0, aux->zero_bytes);

	page->file.file = aux->file;
	page->file.offset = aux->offset;
	page->file.read_bytes = aux->read_bytes;
	page->file.zero_bytes = aux->zero_bytes;
	page->file.map_base = aux->map_base;
	page->file.page_count = aux->page_count;
	free (aux);
	return true;
}

static bool
file_backed_swap_out (struct page *page) {
	if (pml4_is_dirty (thread_current ()->pml4, page->va)) {
		file_write_at (page->file.file, page->frame->kva,
				page->file.read_bytes, page->file.offset);
		pml4_set_dirty (thread_current ()->pml4, page->va, false);
	}
	return true;
}
코드에서 먼저 볼 줄
  • lazy_load_file_page는 aux 정보를 실제 file_page metadata로 옮긴다.
  • read_bytes만 파일에서 읽고, 남은 page 공간은 0으로 채운다.
  • pml4_is_dirty가 false라면 파일에 다시 쓰지 않는다.
  • file_write_at은 mmap된 page의 파일 offset 위치에 되쓴다.

6. munmap은 범위를 따라 page를 정리한다

munmap은 mmap 시작 주소부터 page_count만큼 돌며 SPT에서 page를 제거한다. fault가 이미 난 page라면 file_page metadata로 개수를 알 수 있고, 아직 fault가 나지 않은 uninit page라면 aux 안의 metadata를 확인해야 한다.

void
do_munmap (void *addr) {
	struct supplemental_page_table *spt = &thread_current ()->spt;
	struct page *first = spt_find_page (spt, addr);
	size_t page_count = get_mmap_page_count (first, addr);

	if (page_count == 0)
		return;

	for (size_t i = 0; i < page_count; i++) {
		void *upage = addr + i * PGSIZE;
		struct page *page = spt_find_page (spt, upage);

		if (page != NULL && page_get_type (page) == VM_FILE)
			spt_remove_page (spt, page);
	}
}

7. 이 단계까지 오면 의미 있는 테스트

테스트 감각 무엇을 보는가 실패하면 먼저 볼 곳 이번 글 밖
mmap-read mmap 주소를 읽을 때 파일 내용이 lazy하게 들어오는가 do_mmap(), lazy_load_file_page() frame 부족 압박은 4편
mmap-write, mmap-close dirty page만 파일에 되쓰는가 file_backed_swap_out() 파일 syscall fd 구조는 별도
mmap-overlap, mmap-bad-fd 입구 검증이 잘못된 mapping을 막는가 syscall mmap validation, SPT overlap 프로세스 관계는 별도
munmap 계열 범위 전체가 정리되고 file이 닫히는가 do_munmap(), file-backed destroy COW는 제외

이번 글에서 기억할 것

  • mmap은 파일 내용을 즉시 복사하는 기능이 아니라 파일 구간과 유저 주소를 연결하는 기능이다.
  • file-backed page도 lazy loading 구조를 사용한다.
  • dirty bit를 확인해야 수정된 page만 파일에 되쓸 수 있다.

스스로 점검

  • mmap에서 addr과 offset이 page-aligned여야 하는 이유를 설명할 수 있는가?
  • fault가 나기 전 aux와 fault가 난 뒤 page->file metadata는 어떤 관계인가?
  • dirty bit를 보지 않고 항상 write-back하면 어떤 비효율이나 문제가 생길 수 있는가?

다음 글 예고

다음 단계
COW는 이번 pair-a 구현에서 제외되어 있다. 후속 글로 확장한다면 write protection fault와 page 공유, copy-on-write 분기 처리를 별도로 다루는 편이 맞다.
한 줄 정리
mmap은 파일을 주소공간에 붙이고, page fault와 dirty bit를 기준으로 읽기와 되쓰기를 나누는 VM 기능이다.