메모리를 고정 크기 조각으로 나눠 관리하는 방식.
- 가상 주소 공간의 조각 — = 페이지 (page)
- 물리 메모리의 조각 — = 프레임 (frame)
둘의 크기가 같다 (보통 4KB) 페이지 테이블이 "어느 페이지가 어느 프레임에 있는지" 를 기록한다
주소 변환
가상 주소 32비트, 페이지 4KB 기준
┌──── 20비트 ─────┬── 12비트 ───┐
│ 페이지 번호 │ 오프셋 │
└─────────────────┴─────────────┘
↓ 페이지 테이블에서 찾는다
┌──── 프레임 번호 ─┬── 오프셋 ──┐ ← 오프셋은 그대로
└──────────────────┴────────────┘
물리 주소
이 변환을 MMU(하드웨어)가 한다. 소프트웨어가 매번 계산하면 너무 느리다.
무엇을 얻나
- ① 격리 — 프로세스마다 페이지 테이블이 달라 남의 메모리를 볼 수 없다
- ② 외부 단편화 없음 — 크기가 같으니 아무 빈 프레임에나 넣으면 된다
- ③ 물리 메모리보다 큰 주소 공간 — 필요한 페이지만 올려 둔다
- ④ 공유 — 같은 프레임을 여러 프로세스의 페이지가 가리킬 수 있다 (라이브러리 · copy-on-write)
페이지 테이블이 너무 커진다
32비트 · 4KB 페이지 → 페이지 100만 개 → 항목당 4바이트면 4MB
- 프로세스마다 4MB? 감당이 안 된다
다단계 페이지 테이블
- 실제로 쓰는 부분의 하위 테이블만 만든다
- 64비트에서는 4~5단계를 쓴다
대신 변환에 메모리 접근이 4~5번 필요해진다 → 그래서 TLB 가 필수다
큰 페이지
Huge Page (2MB · 1GB)
→ 같은 메모리를 덮는 데 페이지 수가 적다
→ 페이지 테이블이 작아지고 TLB 적중률이 오른다
→ 큰 메모리를 쓰는 DB · JVM 에서 효과가 있다
→ 대신 내부 단편화가 커진다
cat /proc/meminfo | grep -i huge
cat /proc/<pid>/smaps | grep -i huge
면접 함정
- ❌ "페이징은 외부 단편화를 없앤다" → 맞다. 대신 내부 단편화가 생긴다(마지막 페이지의 남는 공간).
- ❌ "가상 메모리 = 스왑" → 가상 메모리는 주소 변환 체계이고, 스왑은 그중 한 기능이다.
페이지 테이블 항목에 뭐가 더 있나
프레임 번호 외에
present 메모리에 있는가 (없으면 페이지 폴트)
R/W 쓰기 가능한가 (copy-on-write 가 이 비트를 쓴다)
U/S 유저가 접근 가능한가 (커널 영역 보호)
accessed 접근된 적 있는가 (페이지 교체 알고리즘이 쓴다)
dirty 수정됐는가 (내보낼 때 디스크에 써야 하는지 판단)
NX 실행 금지 (데이터 영역에서 코드 실행을 막는다 — 보안)
NX 비트가 버퍼 오버플로 공격의 상당수를 막는다 — 스택에 코드를 심어도 실행되지 않는다.
주소 공간이 실제로 어떻게 쓰이나
cat /proc/<pid>/maps
# 55a1b2c00000-55a1b2c21000 r-xp ... /usr/bin/app 코드 (읽기+실행)
# 55a1b2e20000-55a1b2e21000 r--p ... /usr/bin/app 읽기 전용 데이터
# 55a1b3000000-55a1b3021000 rw-p ... [heap]
# 7ffd8a000000-7ffd8a021000 rw-p ... [stack]
pmap -x <pid> # 사람이 읽기 좋은 형태
r-xp(읽기+실행, 쓰기 없음) 가 코드 영역의 표식이다 — 여기에 쓰려고 하면 SIGSEGV가 난다.