개발/공부 기록

Pintos 완주 후 다시 읽기 3: page fault는 오류가 아니라 VM의 실행 지점이었다

cedis 2026. 5. 22. 21:54

 

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를 장부로 보면

1
va는 user virtual address를 나타낸다.
2
operations는 이 page가 anon인지 file-backed인지, 어떤 방식으로 swap in/out할지 알려준다.
3
writablecow는 fault가 났을 때 쓰기 허용 여부와 COW 분기 판단에 쓰인다.
4
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);
}
상황 처리 흐름 의미
SPT에 page가 있음 vm_do_claim_page(page)로 실제 frame을 붙인다. lazy loading이 실제 로딩으로 바뀐다.
SPT에 page가 없지만 stack 후보 vm_stack_growth(addr)를 호출한다. stack이 필요할 때 확장된다.
not_present가 아님 vm_handle_wp(page)로 보낸다. COW write fault 가능성을 본다.
write인데 writable이 아님 false 반환 정상 lazy fault가 아니라 보호 위반이다.

3. lazy loading은 실패를 미룬 것이 아니라 일을 미룬 것이다

실행 파일을 load할 때 모든 바이트를 즉시 메모리에 올리면 단순하지만 비싸다. Pintos VM 구현에서는 page metadata를 먼저 등록하고, 실제로 접근하는 순간에 file에서 읽는다. 그래서 load 시점과 fault 시점이 분리된다.

lazy loading의 시간차

1
load 시점: 이 주소는 나중에 파일의 어느 offset에서 읽어야 하는지 SPT에 등록한다.
2
첫 접근: page fault가 발생한다.
3
fault handler: SPT에서 page metadata를 찾는다.
4
claim: frame을 얻고, page 종류별 swap_in 또는 initializer를 실행한다.

4. COW까지 들어오면 page fault는 더 중요해진다

fork에서 모든 page를 즉시 복사하면 비싸다. COW는 부모와 자식이 같은 frame을 공유하다가, 누군가 write를 시도하는 순간에만 복사한다. 이 순간도 page fault로 들어온다.

COW 없이 복사하면

fork 시점에 모든 page를 물리적으로 복사한다. 단순하지만 실제로 쓰지 않는 page까지 비용이 든다.

COW로 복사하면

처음에는 frame을 공유하고 read-only로 둔다. write fault가 발생한 page만 새 frame에 복사한다.

5. 어떤 테스트가 이 흐름을 묻는가

테스트 감각 확인하는 흐름
lazy loading 계열 등록만 해둔 page가 실제 접근 시점에 frame을 얻는가
stack growth 계열 rsp 근처 접근을 정상 stack 확장으로 인정하는가
mmap 계열 file-backed page가 fault 시점에 읽히고 munmap 때 dirty write-back 되는가
swap 계열 frame 부족 시 victim을 고르고 page 종류별 swap_out/in이 되는가
COW 계열 fork 후 read 공유, write 시 복사가 분리되는가

이번 글에서 기억할 것

  • 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는 실패 메시지가 아니라, 미뤄둔 메모리 작업을 실제로 수행하는 스위치였다.