Project 3 VM 시리즈 2편
파일을 먼저 다 읽는 방식에서, 접근한 page만 fault 시점에 읽는 구조로 바뀐 흐름을 정리한다.
이번 글의 핵심은 실행 파일을 load 시점에 전부 읽지 않는 것이다. pair-a 구현은 실행 파일의 각 구간을 SPT에 먼저 등록하고, 실제 파일 읽기는 page fault가 났을 때 수행한다.
- load 시점에는 page와 aux만 등록한다.
- page fault 시점에
vm_try_handle_fault()가 SPT를 찾아 claim한다. - uninit page의 initializer가 실행되며 파일 내용이 frame에 채워진다.
글의 기준은 week11-team-03-pintos-vm의 pair-a 브랜치다. COW는 이번 구현 범위에서 제외한다. 아래 코드는 전체 파일을 통째로 복붙하기 위한 목록이 아니라, diff에서 의미가 바뀐 핵심 블록을 따라 읽기 위한 기준점이다.
1. Lazy Loading은 게으른 게 아니라 시점을 늦추는 것이다
일반적으로 load라고 하면 파일 내용을 메모리에 바로 올리는 장면을 떠올리기 쉽다. 하지만 VM에서는 “언젠가 필요하면 읽겠다”는 약속만 남기는 방식이 더 중요하다. 실행 파일의 모든 page가 실제로 사용되는 것은 아니기 때문이다.
이전 상태의 한계
load_segment가 파일을 즉시 읽어 frame에 넣는 구조라면, 아직 접근하지 않은 코드와 데이터까지 모두 물리 메모리를 차지한다.
변경 후 생긴 기준
load_segment는 page를 SPT에 등록하고, 파일 위치와 읽을 바이트 수를 aux에 저장한다. 실제 읽기는 fault가 난 page에 대해서만 수행된다.
Lazy Loading필요할 때까지 실제 데이터 읽기를 미루는 방식이다.
Page FaultCPU가 접근한 가상 주소가 현재 매핑되어 있지 않을 때 발생하는 예외다.
Uninit Page아직 anon/file page로 완전히 초기화되지 않은 page다. initializer를 들고 있다.
Aux나중에 page를 채울 때 필요한 파일 위치, 읽을 크기 같은 보조 정보다.
1
load 시점: 실행 파일의 segment를 보며 page별 aux를 만든다.
2
SPT 등록: 각 upage를 uninit page로 등록한다.
3
접근 시점: 유저 프로그램이 그 주소를 처음 읽거나 실행하면 page fault가 난다.
4
claim: frame을 얻고 lazy initializer를 실행해 파일 내용을 읽는다.
2. load_segment는 데이터를 읽지 않고 약속을 남긴다
아래 블록에서 중요한 점은 file_read()가 이 함수 안에 없다는 것이다. 대신 파일, offset, 읽을 바이트 수를 aux에 담아 vm_alloc_page_with_initializer()로 넘긴다.
struct lazy_load_aux {
struct file *file;
off_t ofs;
size_t page_read_bytes;
size_t page_zero_bytes;
bool owns_file;
};
while (read_bytes > 0 || 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_aux *aux = malloc (sizeof *aux);
aux->file = file;
aux->ofs = ofs;
aux->page_read_bytes = page_read_bytes;
aux->page_zero_bytes = page_zero_bytes;
aux->owns_file = false;
if (!vm_alloc_page_with_initializer (VM_ANON, upage, writable,
lazy_load_segment, aux))
return false;
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
upage += PGSIZE;
ofs += page_read_bytes;
}
aux->ofs는 나중에 파일의 어느 위치부터 읽을지 저장한다.page_read_bytes와page_zero_bytes는 한 page 안에서 파일에서 읽을 부분과 0으로 채울 부분을 나눈다.vm_alloc_page_with_initializer가 호출되지만, 여기서 frame을 채우지는 않는다.lazy_load_segment가 나중에 실행될 함수 포인터로 등록된다.
3. page fault가 나면 약속이 실행된다
page fault handler는 먼저 이 주소가 SPT에 등록되어 있는지 찾는다. 등록된 page라면 frame을 얻고, uninit page의 initializer를 실행한다. 그 순간에야 파일 내용이 실제 메모리로 들어온다.
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;
if (addr == NULL || !is_user_vaddr (addr) || !not_present)
return false;
struct page *page = spt_find_page (spt, addr);
if (page == NULL)
return false;
if (write && !page->writable)
return false;
return vm_do_claim_page (page);
}
static bool
lazy_load_segment (struct page *page, void *aux_) {
struct lazy_load_aux *aux = aux_;
void *kva = page->frame->kva;
file_seek (aux->file, aux->ofs);
if (file_read (aux->file, kva, aux->page_read_bytes)
!= (int) aux->page_read_bytes)
return false;
memset (kva + aux->page_read_bytes, 0, aux->page_zero_bytes);
lazy_load_aux_destroy (aux);
return true;
}
fault 주소CPU가 접근한 주소가 PML4에 없다.
SPT 조회그 주소가 lazy page로 등록되어 있는지 확인한다.
frame 확보물리 메모리 한 칸을 얻고 page와 연결한다.
파일 읽기aux에 저장된 offset과 size로 파일을 읽는다.
4. 여기서 자주 생기는 착각
5. 이 단계까지 오면 의미 있는 테스트
이번 글에서 기억할 것
- Lazy loading은 파일 읽기를 생략하는 것이 아니라 page fault 시점으로 옮기는 것이다.
- load 시점에는 SPT에 page와 aux가 등록된다.
- fault 시점에는 SPT 조회, frame claim, initializer 실행이 이어진다.
스스로 점검
load_segment()안에 실제 파일 읽기 코드가 없다면, 그 읽기는 어디서 발생하는가?- page fault가 오류가 아니라 정상 흐름이 되는 조건은 무엇인가?
- aux에 offset과 read_bytes가 빠지면 어떤 page가 잘못 채워질 수 있는가?
다음 글 예고
다음 글에서는 SPT에 없는 주소가 곧바로 실패가 아니라 stack growth 후보일 수 있다는 점을 본다. syscall이 유저 포인터를 검사할 때도 이 흐름이 다시 등장한다.
Lazy loading은 load와 read를 분리하고, page fault를 실제 로딩 시점으로 바꾸는 구조다.