이번 주의 베이스는 page fault였다
지난 Pintos 회고에서 나는 “완성된 교향곡에서 베이스를 찾는 과정”이라는 문장으로 OS를 읽는 경험을 정리했다. Project 1에서 그 베이스가 interrupt와 테스트 케이스였다면, Project 3에서 다시 찾은 베이스는 page fault였다. 처음에는 page fault를 단순한 실패로 봤지만, 지나고 보니 그것은 lazy loading, stack growth, swap-in, mmap을 한 박자로 묶는 입구였다.
이번 주를 한 줄로 정리하면
VM은 메모리를 더 많이 쓰게 만드는 기술이 아니라, 아직 메모리에 없는 상태도 설명할 수 있게 만드는 약속이었다.
이 문장이 이번 주 회고의 중심이다. 그래서 이번 WIL은 구현 성공담보다, 내가 page fault를 “오류”에서 “흐름”으로 다시 읽게 된 과정을 정리한다.
이번 주 목표 3가지
목표 1. Project 3 VM을 SPT, lazy loading, stack growth, swap, mmap으로 쪼개서 설명한다.
목표 2. page fault가 실패인지, 정상 로딩 입구인지, 권한 위반인지 구분한다.
목표 3. COW로 넘어가기 전에 COW 제외 범위까지 명확히 닫고 간다.
이 글은 COW를 제외한 Pintos Project 3 VM 본편 회고다. 이미 따로 발행한 VM 기술 글 0~5편을 다시 코드 단위로 반복하기보다, 이번 주에 무엇을 했고, 어떤 관점이 바뀌었고, 다음 COW와 Project 4로 무엇을 가져갈지를 정리한다.
- 기술 범위: SPT, lazy loading, stack growth, syscall user pointer, frame table, swap, mmap
- 제외 범위: Copy-on-Write(COW)는 후속 글로 분리
- 회고 범위: 구현 흐름 정리, 테스트 관점 변화, 정글 10대 역량 성장 기록
1. 이번 주에 실제로 한 일
이번 주 산출물을 “Project 3를 했다”로만 적으면 너무 흐릿하다. 실제로는 VM 전체를 하나의 큰 덩어리로 보지 않고, 기능이 쌓이는 순서대로 다시 나누어 글과 코드 흐름을 정리했다.
완료 · pair-a 브랜치 기준으로 Project 3 VM 전체 diff 흐름을 큰 기능 단위로 분리했다.
완료 · SPT를 “PML4를 보조하는 VM 장부”로 정리하고, page와 frame을 분리해서 설명했다.
완료 · lazy loading을 load 시점 등록과 fault 시점 읽기로 나누어 정리했다.
완료 · stack growth에서 fault address와 user rsp의 관계를 따로 설명했다.
완료 · syscall user pointer 검증이 SPT와 stack growth를 함께 봐야 하는 이유를 정리했다.
완료 · frame table과 eviction을 accessed bit 기반 second chance 흐름으로 재구성했다.
완료 · anonymous page의 swap out/in과 file-backed page의 dirty write-back을 구분했다.
완료 · mmap을 “파일 읽기”가 아니라 “파일 구간과 주소공간 연결”로 다시 정의했다.
완료 · 각 글마다 이전 상태의 한계, diff로 추가된 구조, 실제 코드 블록, 의미 있는 테스트 계열을 한 세트로 묶었다.
보류 · COW는 Project 3 후속 주제로 남겼다. fork와 write fault의 관계를 별도 글에서 다룰 예정이다.
2. 처음에 헷갈렸던 것
Project 3 초반에는 VM을 “페이지 테이블을 더 복잡하게 만드는 과제” 정도로 봤다. 그런데 그 관점으로는 lazy loading, swap, mmap이 한 흐름으로 이어지지 않았다. 이번 주에 가장 많이 고친 것은 코드보다도 내 머릿속 용어의 위치였다.
3. Project 3를 관통한 흐름
이번 프로젝트를 하나의 흐름으로 줄이면 아래와 같다. 유저 프로그램이 어떤 주소에 접근한다. 현재 PML4에 매핑이 없으면 page fault가 난다. 커널은 그 주소를 무조건 실패로 처리하지 않고, SPT를 열어 “이 주소를 어떻게 처리하기로 했는지” 확인한다.
실행 중인 프로그램이 code, data, stack, mmap 영역 중 하나에 접근한다.
PML4에 현재 매핑이 없으면 fault가 난다. 여기서 바로 실패라고 판단하지 않는다.
lazy page인지, stack으로 자랄 수 있는 주소인지, swap에서 되살릴 page인지 판단한다.
빈 frame이 없으면 victim을 고르고, 기존 page를 swap/file 쪽으로 내보낸다.
page와 frame을 연결하고 CPU가 다시 같은 명령을 실행할 수 있게 만든다.
이 흐름을 이해하고 나니, Project 3는 메모리를 많이 쓰는 프로그램을 억지로 버티게 하는 과제가 아니었다. 아직 메모리에 없는 상태, 잠시 쫓겨난 상태, 파일과만 연결된 상태를 모두 설명할 수 있게 만드는 과제였다.
4. 테스트를 보는 방식도 바뀌었다
Project 1 때부터 테스트 이름을 문제 이름처럼 읽는 방식이 도움이 됐다. Project 3에서는 그 방식이 더 중요했다. 실패한 테스트 이름은 대체로 “어느 VM 흐름이 깨졌는지”를 알려주는 단서였다.
5. 어려웠던 지점과 해결 방식
이번 주 회고가 단순한 기능 나열이 되지 않으려면, 내가 실제로 어디서 막혔는지가 보여야 한다. 가장 크게 막혔던 지점은 세 군데였다. 모두 “개념을 몰라서”라기보다, 같은 현상을 어느 자료구조의 책임으로 봐야 하는지 몰라서 생긴 문제였다.
처음에는 “주소가 유효하다면 PML4에도 매핑이 있어야 하는 것 아닌가?”라고 생각했다. 그래서 lazy loading을 볼 때 SPT에 page metadata만 등록해두고 실제 frame은 없는 상태가 모순처럼 느껴졌다.
다시 잡은 기준은 이렇다. PML4는 지금 당장 CPU가 접근할 수 있는 매핑이고, SPT는 아직 메모리에 올라오지 않았더라도 커널이 기억해야 하는 약속이다. 이 차이를 잡고 나서야 lazy page가 “없는 page”가 아니라 “fault가 오면 만들 page”로 보였다.
eviction을 보기 전까지는 page와 frame을 거의 같은 말처럼 읽었다. 그런데 물리 메모리가 부족해지면 frame은 빼앗길 수 있지만, 그 page가 어떤 유저 주소였는지, swap에 있는지, 파일에서 다시 읽을 수 있는지는 남아 있어야 했다.
그래서 page는 “가상 주소와 그 주소의 정체성”, frame은 “현재 빌려 쓰는 실제 자리”로 나누었다. 이 구분이 생기자 swap-out은 page를 지우는 일이 아니라, frame을 비우면서 page가 되살아날 방법을 기록하는 일로 정리됐다.
mmap을 처음에는 “파일 내용을 메모리로 읽어오는 기능”으로 이해했다. 하지만 그렇게 보면 dirty bit, munmap, file-backed swap-out이 왜 필요한지 설명이 되지 않았다.
이번에 바뀐 정의는 “mmap은 파일 구간과 유저 주소공간을 연결하는 약속”이라는 것이다. 실제 읽기는 page fault 때 일어나고, 다시 파일에 쓸지는 dirty bit와 munmap 시점에서 결정된다. 이 관점으로 바꾸고 나서 mmap 테스트가 단순 파일 입출력 테스트가 아니라 VM 상태 관리 테스트로 보였다.
6. 정글 10대 핵심 역량 회고
이번 주의 핵심 역량 평가는 “잘했다/못했다”보다, 지난주 대비 무엇이 구체적으로 바뀌었는지를 기준으로 적었다. 달성률은 절대 점수가 아니라 이번 주 목표 대비 체감 기준이다.
목표: 실패 테스트를 “어디가 틀렸다”가 아니라 “어느 VM 흐름이 끊겼다”로 해석한다.
이번 주 발전: page 계열은 SPT/lazy claim, stack 계열은 fault address와 rsp, swap 계열은 frame 부족과 swap slot, mmap 계열은 file-backed page와 dirty write-back으로 나누어 읽었다.
지난주 대비: 이전에는 개별 함수 수정에 집중했다면, 이번에는 실패 테스트를 보고 “어느 상태 전이가 끊겼는가”를 먼저 묻는 쪽으로 바뀌었다.
목표: VM 기능을 단일 패치가 아니라 책임이 나뉜 구조로 이해한다.
이번 주 발전: SPT, frame table, swap, mmap이 서로 어떤 정보를 넘겨야 하는지 구조적으로 정리했다.
지난주 대비: Project 2에서는 함수 하나의 역할을 따라갔다면, Project 3에서는 자료구조 간 계약을 더 많이 봤다.
목표: 최종 코드가 어떤 요구사항을 만족하는지 설명 가능한 수준으로 읽는다.
이번 주 발전: SPT, lazy loading, stack growth, eviction, mmap을 각각 “이전 한계 -> 추가된 코드 구조 -> 의미 있는 테스트” 순서로 분해했다.
지난주 대비: 단순히 통과 여부만 보지 않고, “이 코드가 없으면 어떤 주소 접근 흐름이 설명되지 않는가”까지 같이 적었다.
목표: 통과한 기능과 아직 제외된 범위를 명확히 구분한다.
이번 주 발전: COW를 무리하게 포함하지 않고, COW 제외 상태를 글마다 명확히 적었다.
지난주 대비: “거의 됐다”보다 “여기까지 됐고, 여기는 후속이다”라는 표현을 더 신경 쓰게 됐다.
목표: 나중에 다시 봐도 기능별 흐름을 복원할 수 있는 기록을 남긴다.
이번 주 발전: 0~5편으로 기능 경계를 나누어, SPT부터 mmap까지 다시 읽을 수 있는 구조를 만들었다.
지난주 대비: 코드 자체보다 코드가 놓인 위치와 이유를 문서화하는 쪽으로 발전했다.
목표: 팀 구현을 내 담당만이 아니라 전체 흐름 안에서 이해한다.
이번 주 발전: pair-a 브랜치 기준으로 전체 구조를 읽고, 내가 직접 작성한 부분과 팀 결과물이 만나는 지점을 글로 정리했다.
지난주 대비: 내 코드만 설명하는 단계에서 팀 결과물을 기능 단위로 해석하고, COW처럼 아직 남은 범위를 함께 표시하는 단계로 이동했다.
목표: 이해가 흐릿한 상태에서 넘어가지 않고, 글로 설명 가능한 수준까지 끌어올린다.
이번 주 발전: 기능별 글 6개와 이번 WIL까지 이어가며, 이해가 빈 곳을 계속 드러내고 고쳤다.
지난주 대비: 막연한 불안보다 “어떤 단어가 아직 안 잡혔는가”를 확인하는 방식이 늘었다.
목표: OS 내부 구현이 사용자 경험과 어떤 식으로 연결되는지 최소한의 감각을 잡는다.
이번 주 발전: lazy loading과 mmap이 메모리 효율, 실행 지연, 파일 접근 방식에 영향을 준다는 정도는 연결했다.
지난주 대비: 아직 약한 역량이다. 다만 시스템 내부 구현이 실제 프로그램 실행 효율로 이어진다는 관점은 조금 생겼다.
목표: AI를 단순 답안 생성기가 아니라 요구사항 분해, 테스트 해석, 코드 리뷰 보조 도구로 쓴다.
이번 주 발전: 글을 작성할 때마다 “비전공자~학부생이 이 글만 보고 흐름을 복원할 수 있는가”, “색 대비가 티스토리에서 깨지지 않는가”, “COW 제외 범위를 과장하지 않았는가”를 반복 점검했다.
지난주 대비: 질문을 던지는 방식이 구체화됐다. 이제는 결과물을 받는 데서 끝내지 않고, 기준을 세운 뒤 그 기준을 통과할 때까지 다시 고치는 방식에 가까워졌다.
목표: 낯선 VM 개념을 빠르게 용어, 흐름, 테스트 기준으로 쪼갠다.
이번 주 발전: SPT에서 mmap까지 범위를 한 번에 외우지 않고, fault를 중심으로 연결해 학습했다.
지난주 대비: 새 개념이 나왔을 때 바로 구현으로 뛰기보다, 먼저 “이 개념은 어느 흐름에 들어가는가”를 묻기 시작했다.
7. 다음 주로 가져갈 기준
- 테스트 이름을 먼저 기능 질문으로 번역한다.
- page fault를 실패/정상 흐름/권한 위반으로 나눠 본다.
- 코드가 통과했는지만 보지 말고, 어떤 자료구조 간 계약을 만든 것인지 확인한다.
- COW에서는 “복사”보다 “공유하다가 write fault에서 분리”되는 순간을 중심으로 본다.
- 어떤 도구를 쓰더라도 최종 기준은 테스트, 요구사항, 내가 설명 가능한 흐름으로 둔다.
한눈에 보는 성장 요약
마무리하며
Project 3를 시작할 때는 가상 메모리를 “페이지 테이블을 더 잘 다루는 과제”라고 생각했다. 하지만 끝나고 보니 핵심은 조금 달랐다.
Project 3는 메모리를 더 많이 쓰게 만드는 과제가 아니라, 메모리에 아직 없는 상태도 설명할 수 있게 만드는 과제였다.
이제 COW로 넘어가면 fork는 단순 복사가 아니라 공유와 분리의 문제가 된다. 이번 주에 잡은 기준이 다음 단계에서 흔들리지 않는지 다시 확인해볼 차례다.