접근하려는 페이지가 물리 메모리에 없을 때 발생하는 예외.
처리 과정
- ① CPU 가 주소 변환을 시도한다 → 페이지 테이블에 '없음' 표시
- ② 페이지 폴트 예외가 발생한다 → 커널로 진입
- ③ 커널이 판단한다
- 정당한 접근인가? 아니면 → SIGSEGV (프로그램 종료)
- ④ 빈 프레임을 확보한다 (없으면 페이지 교체)
- ⑤ 필요하면 디스크에서 읽어 온다
- ⑥ 페이지 테이블을 갱신하고 그 명령을 '다시 실행' 한다
⑥이 중요하다 — 실패한 명령이 처음부터 다시 실행되므로 프로그램은 폴트가 있었는지도 모른다.
두 종류를 구분한다
minor fault (soft)
- 디스크를 읽지 않는다
- 이미 메모리에 있는 페이지(페이지 캐시 · 공유 라이브러리)를 매핑만 한다
- copy-on-write 로 복사한다
- 처음 접근하는 힙 페이지에 0 채운 프레임을 준다
- 빠르다 (마이크로초)
major fault (hard)
- 디스크에서 읽어 와야 한다
- 느리다 (밀리초 — 만 배 차이)
- 이게 많으면 성능이 무너진다
ps -o min_flt,maj_flt,cmd -p <pid>
/usr/bin/time -v ./app | grep -i fault
# Major (requiring I/O) page faults: 12
# Minor (reclaiming a frame) page faults: 45231
vmstat 1 # si/so 열이 스왑 in/out — 0이 아니면 경고
minor가 많은 건 정상이다. major가 지속적으로 발생하면 메모리가 부족하다는 뜻이다.
demand paging — 필요할 때만 가져온다
프로그램을 시작할 때 전부 메모리에 올리지 않는다
- 실제로 접근하는 페이지만 그때그때 올린다
효과
- 시작이 빠르다
- 물리 메모리보다 큰 프로그램을 돌릴 수 있다
- 안 쓰는 코드(에러 처리 경로 등)는 영영 안 올라온다
// mmap 은 이 성질을 그대로 활용한다
void *p = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
// 파일을 '매핑' 만 한다. 실제 읽기는 접근하는 순간 페이지 폴트로 일어난다
면접 함정
- ❌ "페이지 폴트는 에러다" → 정상적인 메커니즘이다. 잘못된 접근일 때만 SIGSEGV가 된다.
- ❌ "페이지 폴트가 많으면 문제" → minor는 정상이다. major를 봐야 한다.
첫 접근 비용
char *p = malloc(1UL << 30); // 1GB 할당 — 거의 즉시 끝난다
// 아직 물리 메모리를 안 받았다. 주소 공간만 예약됐다
memset(p, 0, 1UL << 30); // 여기서 페이지 폴트가 26만 번 (4KB 기준)
// 이때 실제로 물리 메모리가 붙는다
"할당은 빠른데 처음 쓸 때 느리다" 가 이 구조에서 나온다. 벤치마크에서 워밍업이 필요한 이유 중 하나다.
mmap(..., MAP_POPULATE, ...); // 미리 물리 페이지를 붙여 둔다
mlock(p, size); // 스왑 아웃되지 않게 고정한다
힙이 늘어나는 과정
- malloc 이 작은 크기면 → 미리 받아 둔 힙에서 잘라 준다 (시스템 콜 없음)
- 힙이 모자라면 — → brk 나 mmap 으로 주소 공간을 더 받는다
- 실제 물리 메모리는 — → 그 주소를 처음 건드릴 때 페이지 폴트로 붙는다
세 단계가 분리돼 있어서 "free 는 늘었는데 RSS 는 안 줄어드는" 현상이 생긴다
- free 한 메모리를 할당자가 OS 에 즉시 반납하지 않고 재사용을 위해 쥐고 있다