
2025.06.01
오늘한 내용 : PintOS - Project3: Virtual Memory - gitbook 정리 중! - Memory Mapped Files, Swap In/Out, Copy-on-Write(Extra), FAQ
WEEK 12 : 정글 끝까지(PintOS) - Virtual Memory
메모리 매핑 파일(Memory Mapped Files)은 익명 페이지와 달리 파일을 그대로 백업 저장소(backing store)로 사용하는 방식이다.
mmap()void *mmap(void *addr, size_t length, int writable, int fd, off_t offset);
fd로 열린 파일의 offset 바이트부터 length 바이트를 가상 주소 addr부터 연속 페이지로 매핑length보다 작으면 마지막 페이지에서 파일 끝 이후 바이트는 0으로 채움NULL 반환addr가 페이지 정렬(4KB)되지 않음 → 실패length == 0 → 실패fd가 콘솔 입출력(stdin/stdout) → 실패addr == NULL → 실패(간단화를 위해 자동 주소 배정 미지원)do_mmap() (vm/file.c) 호출file_reopen(fd)) 복제 후 사용length 바이트를 페이지 단위로 나누어 각 페이지마다 vm_alloc_page_with_initializer(VM_FILE, va, writable, file_backed_initializer, aux) 호출aux에 file, offset, read_bytes, zero_bytes 정보 포함do_mmap()이 정상 완료되면 addr 반환, 아니면 NULL 반환munmap()void munmap(void *addr);
mmap으로 매핑된 가상 주소 addr 영역을 해제(unmap)addr는 이전 mmap()이 반환한 정확한 가상 주소여야 함do_munmap() (vm/file.c) 호출page->operations->swap_out(page) 또는 file_backed_destroy(page)로 파일에 쓰기 수행struct page 객체를 생성한다.file_backed_initializer가 호출되어 파일로부터 페이지 내용을 읽어 물리 프레임에 올린다.vm_alloc_page_with_initializer(VM_FILE, va, writable, file_backed_initializer, aux) → struct page만 생성swap_in 실행 → lazy_load_segment와 유사하게 파일에서 read_bytes만큼 읽어오고, 나머지 zero_bytes 부분은 0으로 채움dirty 비트를 확인하여 swap_out 또는 file_backed_destroy에서 파일에 덮어씀vm_file_init() (vm/vm.c)void vm_file_init(void);
file_backed_initializer() (vm/vm.c)bool file_backed_initializer(struct page *page, enum vm_type type, void *kva);
mmap 이후 첫 페이지 폴트 또는 SPT 복사·fork 시page->operations에 swap_in, swap_out, destroy 함수 포인터 등록
page->operations->swap_in = file_backed_swap_in;
page->operations->swap_out = file_backed_swap_out;
page->operations->destroy = file_backed_destroy;
page->operations->type = VM_FILE;
struct file_page 멤버에 파일 핸들, 읽기 오프셋, 읽어야 할 바이트 수, 쓰기 권한 등 저장
물리 프레임은 아직 할당하지 않음 (lazy loading)
file_backed_destroy() (vm/vm.c)static void file_backed_destroy(struct page *page);
munmap() 수행 중dirty 상태이면, 파일 핸들로 덮어쓰기 (file_write_at)vm_file_init() 호출하여 파일 기반 VM 서브시스템 초기화mmap() 시스템콜do_mmap() 실행file_reopen(fd)) → 페이지 영역 수 계산vm_alloc_page_with_initializer(VM_FILE, va, writable, file_backed_initializer, aux) 반복 호출addr 반환, 에러 시 NULL 반환vm_try_handle_fault() 내에서 spt_find_page() → page->operations->swap_in(page, page->frame->kva)file_backed_swap_in() 호출 → 파일에서 read_bytes 읽어와 frame->kva 채움, 남은 부분 0으로 초기화install_page()로 매핑 완료dirty 비트 세팅 → file_backed_swap_out() 또는 file_backed_destroy()에서 파일에 반영munmap() 시스템콜do_munmap() 실행file_backed_destroy(page) 호출 및 SPT/PTE/프레임 정리위 단계를 구현하면 메모리 매핑 파일 관련 기능이 정상 동작하며, 페이지 폴트마다 필요한 데이터가 파일에서 지연 로딩 되어 메모리에 올라오고, unmap 시에는 변경 내용이 파일에 반영
프로그래밍 실행 중 물리 메모리가 부족하면, 현재 사용되지 않는 페이지를 디스크로 내보내(swap out) 메모리를 확보할 수 있다. 이후 해당 페이지에 접근 요청이 들어오면 디스크에서 다시 읽어 들여(swap in) 원래 상태로 복원한다. 이 과정을 통해 물리 메모리를 최대한 활용할 수 있다.
struct page_operations와 함수 포인터struct page_operations 안에 swap_in, swap_out, destroy 함수 포인터를 갖고 있다.page->operations->swap_in(page, kva)처럼 함수 포인터를 통해 실행된다.swap_in/swap_out 함수를 구현해야 한다.vm_anon_init()void vm_anon_init(void);
anon_initializer()bool anon_initializer(struct page *page, enum vm_type type, void *kva);**역할**: 익명 페이지를 처음 생성할 때 호출
page->operations->swap_in = anon_swap_in;
page->operations->swap_out = anon_swap_out;
page->operations->destroy = anon_destroy;
page->operations->type = VM_ANON;
struct anon_page 안에 스왑 슬롯 인덱스를 저장할 수 있도록 멤버 추가
size_t swap_slot;SWAP_NOTHING)으로 표시물리 프레임은 할당하지 않으므로 page->frame = NULL 상태 유지
anon_swap_out()static bool anon_swap_out(struct page *page);
page->frame->kva)을 스왑 디스크의 해당 슬롯에 복사swap_disk_write(slot, page->frame->kva)와 유사한 함수 사용struct anon_page에 swap_slot = slot 저장pagedir_clear_page(thread->pagedir, page->va)나 유사 함수 호출palloc_free_page(page->frame->kva)page->frame = NULLtrue, 오류(panic 제외) 시 falseanon_swap_in()static bool anon_swap_in(struct page *page, void *kva);
page->anon.swap_slot 값 읽기SWAP_NOTHING이면 스왑된 적 없는 새 페이지이므로 제로 초기화kva 파라미터는 이미 할당된 물리 프레임의 커널 가상 주소kva == NULL이라면 vm_get_frame()으로 새로운 프레임 확보swap_slot 위치에서 메모리로 읽기swap_disk_read(slot, kva)와 유사한 함수 사용install_page(page->va, kva, true)page->anon.swap_slot = SWAP_NOTHING로 초기화true, 아니면 falsemunmap() 호출 시점에, 메모리에서 수정된 내용이 있으면 실제 파일에 덮어쓰기(write-back)하여 파일을 최신 상태로 유지한다.file_backed_swap_out()static bool file_backed_swap_out(struct page *page);
file_write_at으로 파일의 지정 오프셋에 페이지 내용을 덮어쓰기file_write_at(page->file.file, page->frame->kva, page->file.read_bytes, page->file.offset)pagedir_clear_page(thread->pagedir, page->va)palloc_free_page(page->frame->kva)page->frame = NULLpagedir_set_dirty(thread->pagedir, page->va, false)true, 실패 시 falsefile_backed_swap_in()static bool file_backed_swap_in(struct page *page, void *kva);
kva 파라미터로 전달된 커널 가상 주소 또는 vm_get_frame() 호출page->file.read_bytes만큼 kva로 읽어오기file_read_at(page->file.file, kva, page->file.read_bytes, page->file.offset)zero_bytes = PGSIZE - page->file.read_bytes 만큼 제로 초기화memset(kva + page->file.read_bytes, 0, zero_bytes)install_page(page->va, kva, page->file.writable)pagedir_set_dirty(thread->pagedir, page->va, false)true, 실패 시 falsevm_anon_init() 호출 → 스왑 슬롯 관리 비트맵 생성vm_file_init() 호출 → 파일 매핑 페이지 관련 자료구조 초기화anon_initializer()로 swap_in/out/destroy 함수 포인터 설정, swap_slot 초기화file_backed_initializer()로 함수 포인터 설정, 파일 핸들·오프셋 저장vm_try_handle_fault() 호출 → page->operations->swap_in(page, kva) 실행 → 익명/파일 페이지에 맞는 swap_in 수행vm_get_frame()에서 실패할 때page->operations->swap_out(page) 호출file_backed_destroy() 혹은 anon_destroy() 호출위 과정을 구현하면, 익명 페이지와 파일 매핑 페이지 모두 필요할 때 디스크로 내보내고, 다시 접근 시 복원하는 스왑 메커니즘이 완성
ref_count 필드를 프레임 구조체에 추가한다.struct frame {
void *kva; /* 커널 가상 주소(실제 물리 페이지) */
struct page *page; /* 대표 페이지(struct page)를 가리킴 */
int ref_count; /* 현재 이 프레임을 참조하는 페이지 수 */
/* 필요 시 lock 추가 가능 */
};ref_count = 1로 초기화ref_count++install_page(va, frame_kva, writable=false)로 PTE에 매핑하여 쓰기 시도를 페이지 폴트로 유도anon_initializer, file_backed_initializer)에서 writable 인자를 강제로 false로 변경하거나, COW 전용 별도 초기화 함수를 사용할 수 있다.vm_try_handle_fault()가 호출되면,SPT에서 page 찾기 → struct page *p
user 영역·not_present 여부 검사 후, 쓰기 폴트(write fault)인지 확인
bool is_write_fault = (write && !pagedir_is_writable(cur->pagedir, va));
쓰기 폴트(write fault)이고, COW 대상 페이지라면
frame = p->frame;frame->ref_count > 1인지 확인new_frame = vm_get_frame();memcpy(new_frame->kva, frame->kva, PGSIZE);frame->ref_count--, new_frame->ref_count = 1p->frame = new_frameinstall_page(p->va, new_frame->kva, true); // 쓰기 허용swap_in/install_page로 변경만 하면 됨읽기 폴트(read fault)이거나 쓰기 폴트가 아니면서 not_present면 기존 로직—스왑인(swap_in) 또는 lazy-load—수행
supplemental_page_table_copy()는 새 페이지마다 vm_do_claim_page()로 실제 페이지를 복제해 왔다.bool supplemental_page_table_copy(struct supplemental_page_table *dst,
struct supplemental_page_table *src) {
// src SPT의 모든 struct page 순회
for each struct page *p_src in src->spt {
// 1) 새 struct page *p_dst 할당
struct page *p_dst = malloc(sizeof *p_dst);
// 2) p_dst->va, p_dst->operations, p_dst->aux = p_src->aux 등 복제
*p_dst = *p_src; // 얕은 복사 후 필요한 필드 수정
// 3) p_dst->frame = p_src->frame;
// 4) frame->ref_count++;
p_src->frame->ref_count++;
// 5) PTE 복제: 부모 페이지 테이블에서 va에 매핑된 프레임 번호 읽어 와
// install_page_in_child(child_pagedir, p_dst->va, p_dst->frame->kva, false);
// (쓰기 금지 상태로 복제)
install_page(dst->pagedir, p_dst->va, p_dst->frame->kva, false);
// 6) dst SPT에 p_dst 삽입
spt_insert_page(dst, p_dst);
}
return true;
}struct page 복사본 생성frame = 부모가 가리키는 frame 연결frame->ref_count++install_page(child_pagedir, va, frame_kva, false) 호출 → 읽기만 허용, 쓰기 금지struct page 삽입요약: 부모와 자식은 같은 물리 페이지를 읽기 전용으로 공유한다.
mov [rsp], eax 등으로 쓰기 시도 → MMU가 PTE에서 writable = false 감지 → 페이지 폴트 발생vm_try_handle_fault() 진입va를 pg_round_down(fault_addr)로 페이지 단위 정렬
p = spt_find_page(&cur->spt, va)로 struct page 조회
쓰기 폴트 감지:
if (write && !pagedir_is_writable(cur->pagedir, va)) {
// COW candidate
}
COW 수행 조건 검사
p->operations->type == VM_ANON 또는 VM_FILE (공유 가능한 페이지)frame = p->frame; frame->ref_count > 1 (현재 물리 페이지를 2개 이상이 공유 중)COW 실행
new_frame = vm_get_frame();memcpy(new_frame->kva, frame->kva, PGSIZE);frame->ref_count--; new_frame->ref_count = 1;p->frame = new_frame;install_page(cur->pagedir, p->va, new_frame->kva, true);COW 불필요 조건
frame->ref_count == 1인 경우: 현재 페이지를 오직 이 프로세스만 사용 중이므로 그냥 install_page(..., true)로 쓰기 권한만 회복not_present(swap-in, lazy-load) 케이스: 기존 swap_in, lazy_load_segment 로직 실행struct frame에 int ref_count 추가vm_get_frame()에서 새 프레임 생성 시 ref_count = 1 초기화anon_initializer(), file_backed_initializer() 호출 시,install_page(cur->pagedir, va, frame_kva, false); 와 같이 쓰기 금지로 PTE를 생성supplemental_page_table_copy()에서 물리 복사(vm_do_claim_page) 대신,p_dst->frame = p_src->frame; p_src->frame->ref_count++;install_page(child_pagedir, va, frame_kva, false)vm_try_handle_fault)p->frame->ref_count > 1)이면 COW 분기 수행supplemental_page_table_kill() 실행 시,destroy(page) 호출 → 익명 또는 파일 기반 소멸자에서 frame->ref_count--ref_count == 0이 되면 물리 페이지 해제(palloc_free_page)ref_count > 1이면 즉시 COW 수행 후 스왑하도록 설계해도 무방하다.ref_count > 1)는 순수한 스왑 아웃 대상이 아니다.ref_count 감소 및 최종 해제 정상 동작위 과정을 모두 구현하면 Pintos에서 Copy-on-Write 기능이 정상 작동하며, 포크 성능이 크게 개선된다.
프로젝트 2가 제대로 동작해야 프로젝트 3을 구현할 수 있나?
예. 프로젝트 2가 올바르게 통과되어야 프로젝트 3 작업을 시작할 수 있다.
페이지 폴트 처리를 완료한 뒤 어떻게 프로세스를 재개하나?
page_fault() 함수에서 반환하면 현재 사용자 프로세스가 바로 재개된다. 예외 발생 지점의 명령어를 다시 시도한다.
스택 포인터 위쪽 주소에서 페이지 폴트가 발생하는 이유는 무엇인가?
스택 확장 테스트에서, 유저 프로그램은 현재 스택 포인터보다 위쪽(주소 상으로 낮은)에 있는 주소를 액세스할 수 있다.
PUSH 명령어는 스택 포인터를 먼저 감소시키지 않고 메모리 권한을 검사한다.rsp - 8 위치가 아직 매핑되지 않았더라도, 쓰기 권한 확인을 먼저 하며 페이지 폴트가 날 수 있다.가상 메모리 시스템에서 데이터 세그먼트(변수 영역) 확장 기능이 필요한가?
아니다. 데이터 세그먼트 크기는 링커가 결정한다. 현재 Pintos에는 동적 할당 기능이 없으므로, 데이터 세그먼트 확장은 지원하지 않아도 된다. (메모리 맵 파일을 이용해 유저 레벨에서 흉내낼 수는 있다.)
왜 페이지 프레임을 할당할 때 PAL_USER를 사용해야 하나?
PAL_USER를 주면 palloc_get_page()가 사용자 풀(user pool)에서 메모리를 할당한다.palloc_get_page() 위에 별도의 할당자 레이어를 얹어도 되지만, 최종적으로는 PAL_USER를 사용해야 한다.ul 옵션으로 사용자 풀 크기를 제한해, 다양한 사용자 메모리 크기로 VM 구현을 테스트할 수 있다.