프로세스마다 자기만의 연속된 메모리 공간이 있는 것처럼 보여 주는 기법. 실제 물리 메모리와는 별개의 주소 체계다.
무엇을 해결하나
가상 메모리가 없다면
- 프로그램이 물리 주소를 직접 써야 한다 → 어디에 적재될지 미리 알아야 함
- 다른 프로세스의 메모리를 마음대로 읽고 쓸 수 있다 → 보안·안정성 붕괴
- 물리 메모리보다 큰 프로그램을 아예 못 돌린다
- 메모리가 조각나면 연속 공간을 못 찾는다
주소 변환
프로세스가 보는 것 (가상) 실제 (물리)
0x0000_1000 ──────────────────► 0x8A3F_2000
0x0000_2000 ──────────────────► 0x1B04_7000 ← 흩어져 있어도 무방
0x0000_3000 ──────────────────► 디스크(스왑) ← 아예 메모리에 없을 수도
페이지 테이블이 이 대응표다. CPU의 MMU(메모리 관리 장치)가 매 접근마다 변환한다.
페이지 단위로 관리하는 이유
메모리를 고정 크기 조각(보통 4KB) 으로 나눈다.
가상 주소 0x00001234 (32비트, 페이지 4KB일 때)
┌─────────────────────┬──────────────┐
│ 페이지 번호 0x00001 │ 오프셋 0x234 │
└─────────────────────┴──────────────┘
↓ 페이지 테이블 조회
프레임 번호 0x8A3F
↓
물리 주소 0x8A3F234 ← 오프셋은 그대로 붙인다
고정 크기라 외부 단편화가 없다. 어느 빈 프레임에든 넣을 수 있기 때문이다. (대신 마지막 페이지에 남는 내부 단편화는 생긴다.)
얻는 것들
| 효과 | 어떻게 |
|---|---|
| 프로세스 격리 | 각자 페이지 테이블이 달라 남의 메모리를 가리킬 수 없다 |
| 메모리보다 큰 프로그램 | 필요한 페이지만 올리고 나머지는 디스크에(요구 페이징) |
| 공유 | 같은 물리 프레임을 여러 프로세스의 페이지가 가리키면 됨(공유 라이브러리) |
| Copy-on-Write | fork() 시 복사 없이 공유하다 쓸 때만 복사 |
| 보호 | 페이지마다 읽기/쓰기/실행 권한 |
Copy-on-Write가 중요한 이유
-
fork() 호출
-
전통적으로는 부모의 메모리 전체를 복사해야 함 (수 GB면 재앙)
-
CoW: 페이지 테이블만 복사하고 모든 페이지를 "읽기 전용 공유"로 표시
-
누군가 쓰려고 하면 그때 그 페이지만 복사
-
대부분의 fork는 곧바로 exec 하므로 사실상 복사가 거의 안 일어난다
Redis의 백그라운드 저장(BGSAVE)이 이 원리로 동작한다 — 자식 프로세스가 스냅샷을 뜨는 동안 부모는 계속 서비스하고, 변경된 페이지만 실제 복사된다. 그래서 쓰기가 많으면 메모리 사용량이 최대 2배까지 늘 수 있다.
다단계 페이지 테이블
64비트 주소 공간 전체의 대응표를 만들면 테이블만으로 메모리가 다 찬다. 그래서 트리 구조로 나눠 실제 쓰는 부분만 만든다(x86-64는 4~5단계).
대가는 변환마다 메모리를 4번 읽어야 한다는 것 — 이 비용을 없애는 것이 TLB다.
대가
- 변환 오버헤드 (→ TLB로 완화)
- 페이지 폴트 시 디스크 접근이면 수만 배 느려진다
- 스왑이 과해지면 스래싱