Pintos 완주 후 다시 읽기
SPT, lazy loading, stack growth, mmap, COW를 page fault 처리 흐름으로 묶어서 다시 읽는다.
이 글에서 다루는 것
처음 page fault를 보면 잘못된 메모리 접근으로만 느껴진다. 하지만 Project 3을 끝내고 보면 page fault는 VM이 미뤄둔 일을 실제로 처리하는 입구이기도 하다.
- SPT가 왜 PML4와 별도로 필요했는지
- lazy page, stack growth, COW가 page fault에서 어떻게 갈라지는지
- mmap과 swap이 왜 page 종류별 연산으로 분리되는지
1. PML4는 현재 매핑만 알고, SPT는 의도를 기억한다
PML4는 현재 어떤 가상주소가 어떤 물리 프레임에 연결되어 있는지에 가깝다. 그런데 lazy loading에서는 아직 물리 프레임이 없을 수 있다. 그래도 이 주소가 나중에 파일에서 읽혀야 하는지, zero page인지, writable인지 기억해야 한다. 그 장부가 SPT다.
struct page {
const struct page_operations *operations;
void *va;
struct frame *frame;
bool writable;
bool cow;
struct thread *owner;
size_t mmap_page_cnt;
struct hash_elem hash_elem;
union {
struct uninit_page uninit;
struct anon_page anon;
struct file_page file;
};
};
struct supplemental_page_table {
struct hash pages;
bool initialized;
};
SPT를 장부로 보면
va는 user virtual address를 나타낸다.operations는 이 page가 anon인지 file-backed인지, 어떤 방식으로 swap in/out할지 알려준다.writable과 cow는 fault가 났을 때 쓰기 허용 여부와 COW 분기 판단에 쓰인다.hash_elem은 SPT hash table에서 page를 찾기 위한 연결고리다.2. page fault가 나면 먼저 장부를 찾는다
VM의 핵심 흐름은 vm_try_handle_fault()에 모인다. fault 주소가 커널 주소인지, SPT에 등록된 page인지, write-protection fault인지, stack growth 후보인지 차례로 판단한다.
bool
vm_try_handle_fault (struct intr_frame *f, void *addr,
bool user, bool write, bool not_present) {
struct supplemental_page_table *spt = &thread_current ()->spt;
struct page *page = NULL;
void *rsp;
if (addr == NULL || is_kernel_vaddr (addr))
return false;
page = spt_find_page (spt, addr);
if (!not_present)
return vm_handle_wp (page);
if (page == NULL) {
rsp = user ? (void *) f->rsp : thread_current ()->rsp;
if (rsp != NULL &&
(uint8_t *) addr >= (uint8_t *) rsp - 8 &&
(uint8_t *) addr < (uint8_t *) USER_STACK &&
(uint8_t *) addr >= (uint8_t *) USER_STACK - (1 << 20)) {
vm_stack_growth (addr);
return pml4_get_page (thread_current ()->pml4, addr) != NULL;
}
return false;
}
if (write && !page->writable)
return false;
return vm_do_claim_page (page);
}
3. lazy loading은 실패를 미룬 것이 아니라 일을 미룬 것이다
실행 파일을 load할 때 모든 바이트를 즉시 메모리에 올리면 단순하지만 비싸다. Pintos VM 구현에서는 page metadata를 먼저 등록하고, 실제로 접근하는 순간에 file에서 읽는다. 그래서 load 시점과 fault 시점이 분리된다.
lazy loading의 시간차
4. COW까지 들어오면 page fault는 더 중요해진다
fork에서 모든 page를 즉시 복사하면 비싸다. COW는 부모와 자식이 같은 frame을 공유하다가, 누군가 write를 시도하는 순간에만 복사한다. 이 순간도 page fault로 들어온다.
fork 시점에 모든 page를 물리적으로 복사한다. 단순하지만 실제로 쓰지 않는 page까지 비용이 든다.
처음에는 frame을 공유하고 read-only로 둔다. write fault가 발생한 page만 새 frame에 복사한다.
5. 어떤 테스트가 이 흐름을 묻는가
이번 글에서 기억할 것
- PML4는 현재 매핑이고, SPT는 앞으로 만들어질 page의 의도까지 기억한다.
- page fault는 무조건 오류가 아니라 lazy loading, stack growth, COW의 실행 지점이다.
- VM 코드는 page 종류별 연산을 분리해야 mmap, swap, COW가 같은 fault 입구에서 갈라질 수 있다.
스스로 점검
- SPT 없이 lazy loading을 구현하려 하면 무엇을 잃는가?
- page가 SPT에 없는데도 stack growth로 인정할 수 있는 조건은 무엇인가?
- COW에서 write fault와 일반 not-present fault는 왜 다르게 봐야 하는가?
다음 글 예고
다음 글에서는 VM이 만든 user buffer가 파일 시스템까지 내려갔을 때, 경로 문자열이 inode와 FAT chain으로 바뀌는 과정을 본다.
한 줄 정리
Project 3의 page fault는 실패 메시지가 아니라, 미뤄둔 메모리 작업을 실제로 수행하는 스위치였다.