접근하려는 페이지가 물리 메모리에 없을 때 발생하는 예외.
흐름
- ① 프로세스가 가상 주소에 접근
- ② MMU가 페이지 테이블 조회 → "유효 비트 = 0" (메모리에 없음)
- ③ 페이지 폴트 예외 발생 → 커널로 제어 이동
- ④ 커널이 디스크에서 해당 페이지를 읽어 빈 프레임에 적재
- (빈 프레임이 없으면 교체 알고리즘으로 하나를 내보냄)
- ⑤ 페이지 테이블 갱신 (유효 비트 = 1)
- ⑥ 실패했던 명령을 처음부터 다시 실행
⑥이 중요하다 — 프로세스 입장에서는 아무 일도 없었던 것처럼 이어진다. 예외였다는 사실조차 모른다.
세 종류 — 비용이 전혀 다르다
| 종류 | 상황 | 비용 |
|---|---|---|
| Minor (soft) | 페이지가 메모리에는 있음. 페이지 테이블만 연결하면 됨 | 빠름 (~μs) |
| Major (hard) | 디스크에서 읽어와야 함 | 매우 느림 (SSD ~100μs, HDD ~10ms) |
| Invalid | 잘못된 주소 접근 | 프로세스 종료 (세그멘테이션 폴트) |
Minor의 예 — 다른 프로세스가 이미 올려 둔 공유 라이브러리를 처음 쓸 때, CoW 페이지에 처음 쓸 때, 페이지 캐시에 이미 있는 파일을 읽을 때.
모니터링에서 봐야 할 것은 Major다. Minor는 정상 동작의 일부다.
ps -o min_flt,maj_flt -p <pid>
vmstat 1 # si/so (swap in/out) 가 0이 아니면 경보
왜 이런 구조가 좋은가 — 요구 페이징
프로그램 시작 시 전부 올리지 않고, 실제 접근할 때 올린다(demand paging).
- 1GB 프로그램이지만 실제로 쓰는 코드는 50MB
- 50MB만 메모리에 올라간다
- 메모리 4GB로 훨씬 큰 프로그램들을 동시에 돌릴 수 있다
지역성이 이것을 가능하게 한다 — 프로그램은 대개 코드의 일부만 집중적으로 쓴다(80:20).
성능 계산 — 왜 폴트율이 극도로 낮아야 하는가
- 메모리 접근 — = 100ns
- 페이지 폴트 처리 — = 100μs (SSD) = 100,000ns ← 1000배
폴트율 p 일 때 평균 접근 시간 = (1-p) × 100 + p × 100,000
| p = 0.001 (0.1%) | → 100 + 100 | = 200ns | (2배 느려짐) |
|---|---|---|---|
| p = 0.01 | (1%) | → 99 + 1,000 | = 1,099ns (11배) |
1%만 폴트가 나도 10배 이상 느려진다. 페이지 폴트가 얼마나 비싼 사건인지 보여 주는 계산이다.
실무 함의
① JVM 힙과 스왑
- 힙을 물리 메모리보다 크게 잡으면 → GC가 힙 전체를 훑을 때 대량 major fault
- GC 한 번에 수십 초. 사실상 서비스 중단 컨테이너에서는 스왑을 끄고 힙을 물리 메모리 안에 맞추는 것이 정석이다.
② mmap과 페이지 캐시 Kafka가 빠른 이유 중 하나가 페이지 캐시 활용이다. 파일을 읽으면 OS가 페이지 캐시에 올려 두고, 다음 읽기는 minor fault로 끝난다(디스크 접근 없음).