가상 메모리 ① 페이징과 주소 변환
목표 학습 시간: 105분 + 복습 15분 · 전공 수준 정독용
이 챕터를 마치면: (1) 가상 메모리가 푸는 세 문제(격리·추상화·효율)를 설명하고, (2) 가상 주소를 VPN/offset으로 비트 단위로 쪼개 변환하고, (3) 단일 페이지 테이블의 크기 문제를 수식으로 보이고, (4) x86-64 4단계 페이지 테이블이 그 문제를 어떻게 푸는지 따라갈 수 있다.
0. 학습 지도 (105분)
| 구간 | 분 | 내용 |
|---|---|---|
| 1 | 18 | 가상 메모리가 푸는 세 문제 |
| 2 | 15 | 페이지·프레임과 페이지 크기의 트레이드오프 |
| 3 | 20 | 가상 주소 분해와 변환(비트 계산) |
| 4 | 12 | 페이지 테이블 항목(PTE)의 내용 |
| 5 | 15 | 단일 테이블의 크기 문제(수식) |
| 6 | 20 | 다단계 페이지 테이블 (x86-64 4단계) |
| 7 | 5 | 공유·보호, 그리고 다음 세션 예고 |
| 복습 | 15 | 인출 + 변환 계산 |
1. 가상 메모리가 푸는 세 문제
옛날엔 프로그램이 물리 주소를 직접 썼다. 그러면 곤란한 일들이 생긴다 — 두 프로그램이 같은 주소를 쓰면 충돌하고, 한 프로그램이 다른 프로그램(또는 OS)의 메모리를 마음대로 읽고 쓰며, 메모리에 프로그램을 올리려면 연속된 빈 공간이 필요하다. 가상 메모리는 이 셋을 한꺼번에 푼다.
각 프로세스는 자기만의 가상 주소 공간(0부터 거대한 범위)을 보고, OS+하드웨어(MMU)가 이 가상 주소를 실제 물리 주소로 번역한다. 이 한 겹의 간접층(indirection)이 세 가지를 준다.
1.1 격리(isolation)와 보호(protection)
프로세스 A의 가상 0x4000과 B의 가상 0x4000은 서로 다른 물리 프레임으로 번역된다. 그래서 둘은 같은 주소를 써도 충돌하지 않고(세션 2의 가설 정답), 서로의 메모리를 건드릴 수도 없다. 각 페이지에 읽기/쓰기/실행 권한을 걸어 코드 영역을 읽기 전용으로 보호하는 것도 여기서 나온다.
1.2 추상화(abstraction)
프로세스는 "메모리는 0부터 쭉 이어진 거대한 내 공간"이라는 단순하고 균일한 모델을 본다. 물리 메모리가 실제로 얼마든, 어디에 조각나 있든 신경 쌀 필요가 없다. 컴파일러·링커도 이 균일한 가상 공간을 가정해 코드를 만든다.
1.3 물리 메모리의 효율적 사용
- 유연한 배치: 물리 메모리가 조각나 있어도(비연속) 가상에선 연속처럼 보이게 매핑할 수 있다 → 외부 단편화 문제를 페이징이 해소.
- 공유: 같은 물리 프레임을 여러 프로세스의 페이지 테이블이 가리키게 하면 메모리를 공유한다(공유 라이브러리, copy-on-write).
- 오버커밋·디맨드 페이징: 실제 쓰는 페이지만 물리에 올리고 나머지는 디스크에 두거나 아직 안 만든다(세션 6).
핵심 한 줄: 가상 메모리 = "물리 주소를 직접 쓰는 대신, 프로세스마다 번역되는 가상 주소를 쓰게 한 한 겹의 간접층." 이 간접층이 격리·추상화·효율을 동시에 산다.
2. 페이지와 프레임
2.1 왜 바이트가 아니라 덩어리 단위인가
가상→물리 번역을 매 바이트마다 기록하면 번역표가 메모리보다 커진다. 그래서 메모리를 고정 크기 덩어리로 잘라 덩어리 단위로 번역한다.
- 페이지(page): 가상 주소 공간을 자른 고정 크기 덩어리(표준 4KB).
- 프레임(frame, page frame): 물리 메모리를 같은 크기로 자른 덩어리.
- 번역이란 결국 "가상 페이지 번호(VPN) → 물리 프레임 번호(PFN)" 의 매핑이다. 페이지 안에서의 위치(offset)는 번역하지 않고 그대로 둔다.
2.2 페이지 크기의 트레이드오프
왜 하필 4KB? 크기 선택엔 양면이 있다.
- 작은 페이지(예: 4KB): 내부 단편화(페이지 끝의 낭비)가 작고, 필요한 만큼만 세밀하게 올릴 수 있다. 대신 같은 메모리를 덮는 데 페이지 수가 많아 페이지 테이블·TLB 부담이 커진다.
- 큰 페이지(huge page, 2MB·1GB): 페이지 수가 적어 테이블이 작고 TLB 도달 범위(reach) 가 넓다(세션 6). 대신 내부 단편화가 커지고 세밀함을 잃는다.
4KB는 이 둘의 오랜 절충점이다. 대용량 메모리를 다루는 DB·가상화에서는 huge page로 TLB 압박을 줄인다.
3. 가상 주소의 분해와 변환
3.1 offset과 VPN으로 쪼개기
페이지가 4KB = 2¹² 바이트이므로, 가상 주소의 하위 12비트는 페이지 안 위치(offset), 나머지 상위 비트는 VPN이다.
가상 주소 (48비트 예)
┌───────────────────────────────┬──────────────┐
│ VPN (상위 36비트) │ offset(12비트)│
└───────────────────────────────┴──────────────┘
│ │
│ 페이지 테이블에서 VPN→PFN │ 그대로 복사
▼ ▼
┌───────────────────────────────┬──────────────┐
│ PFN (물리 프레임 번호) │ offset(12비트)│ = 물리 주소
└───────────────────────────────┴──────────────┘
핵심 규칙: offset은 절대 번역하지 않는다. 페이지 안에서의 상대 위치는 가상이든 물리든 동일하니까. 번역되는 건 오직 페이지 번호(VPN→PFN)다.
3.2 비트 계산 워크드 예제
- 페이지 4KB → offset 12비트.
- 가상 주소
0x00003ABC라고 하자(이진으로 보면 하위 12비트가 offset).- offset = 하위 12비트 =
0xABC. - VPN = 나머지 상위 =
0x00003.
- offset = 하위 12비트 =
- 페이지 테이블에서 VPN
0x3항목을 보니 PFN = 0x57 이라 하자. - 물리 주소 = PFN 뒤에 offset을 그대로 붙임 =
0x57‖0xABC=0x00057ABC.
연습으로 페이지 크기를 8KB(offset 13비트)로 바꿔 같은 절차를 해보면, "offset 비트 수 = log₂(페이지 크기)"라는 규칙이 손에 붙는다.
4. 페이지 테이블 항목(PTE)의 내용
페이지 테이블은 "VPN→PFN" 매핑을 담은 표로 프로세스마다 하나씩 있고, 보통 물리 메모리에 산다. MMU가 이 표를 읽어 하드웨어로 번역한다. 각 항목(PTE)에는 PFN 말고도 중요한 비트들이 있다.
| 비트/필드 | 의미 |
|---|---|
| PFN | 이 가상 페이지가 매핑된 물리 프레임 번호 |
| Present(유효) | 이 페이지가 지금 물리 메모리에 있나? (없으면 페이지 폴트 — 세션 6) |
| R/W | 읽기 전용인가 쓰기 가능인가 |
| U/S | 사용자 모드 접근 허용인가, 커널 전용인가 |
| NX | 실행 금지(코드 주입 방어, W^X) |
| Accessed | 최근에 접근됐나 (교체 정책이 참고 — 세션 6) |
| Dirty | 수정됐나 (스왑 아웃 시 디스크에 다시 써야 하나) |
이 비트들이 가상 메모리를 단순 번역기 이상으로 만든다 — 보호(R/W/NX/US), 디맨드 페이징(Present), 교체 정책(Accessed/Dirty) 이 전부 PTE 비트에서 나온다.
5. 단일 페이지 테이블의 크기 문제
가장 단순한 설계는 "VPN을 인덱스로 하는 거대한 1차원 배열"이다. 그런데 이게 왜 망하는지 수식으로 보자.
가정: 48비트 가상 주소, 4KB(2^12) 페이지
가상 페이지 수 = 2^48 / 2^12 = 2^36 개
PTE 하나 = 8바이트 라 하면
표 하나의 크기 = 2^36 × 8 = 2^39 = 512 GB
프로세스마다 512GB짜리 페이지 테이블을 둬야 한다? 물리 메모리가 그보다 작을 텐데 불가능하다.
게다가 더 큰 낭비가 있다. 대부분의 프로세스는 이 거대한 주소 공간 중 극히 일부만 쓴다(코드 약간, 힙 약간, 스택 약간, 가운데는 텅 빔). 그런데 단일 1차원 표는 안 쓰는 영역의 항목까지 전부 자리를 차지한다. "쓰지도 않는 페이지들을 위한 표"가 메모리를 다 먹는 것이다.
6. 다단계 페이지 테이블 — 트리로 푼다
6.1 아이디어: 표를 트리로
해법은 표를 계층(트리) 으로 만드는 것이다. 상위 표가 하위 표를 가리키고, 실제로 쓰는 영역에 해당하는 하위 표만 만든다. 안 쓰는 영역은 상위 항목을 "없음(invalid)"으로 비워, 그 아래 거대한 하위 표를 아예 만들지 않는다. 거대한 빈 공간의 표를 생략하는 것 — 이게 공간 절약의 핵심이다.
6.2 x86-64의 4단계 (구체적으로)
x86-64는 48비트 가상 주소를 9 + 9 + 9 + 9 + 12 로 쪼갠다.
가상 주소 48비트
┌──────┬──────┬──────┬──────┬───────────┐
│ 9비트│ 9비트│ 9비트│ 9비트│ offset 12 │
│ PML4 │ PDPT │ PD │ PT │ │
└──┬───┴──┬───┴──┬───┴──┬───┴───────────┘
│ │ │ │
L1표 ──┘ │ │ │ ← CR3 레지스터가 L1(PML4) 시작 주소를 가리킴
└─> L2표 ───┘ │ │
└─> L3표 ───┘ │
└─> L4표(PT)─┘ → PFN
PFN ‖ offset = 물리 주소
- 각 단계 표는 512개 항목(2⁹) × 8바이트 = 정확히 4KB(한 페이지) 다. 표 자체도 페이지 단위라 관리가 깔끔하다.
- 번역은 CR3 레지스터가 가리키는 최상위 표(PML4)에서 시작해, 9비트씩 인덱스로 단계를 타고 내려가 최종 PT에서 PFN을 얻는다. 이 과정을 페이지 워크(page walk) 라 한다.
- 공간 절약 효과: 작은 프로그램이라면 PML4 1개, 그 아래 실제 쓰는 가지의 PDPT/PD/PT 몇 개면 충분하다. 512GB가 아니라 수 KB~수십 KB로 끝난다. 쓰는 만큼만 표가 생기기 때문이다.
6.3 대가: 번역이 비싸졌다
트리를 타고 내려가려면 단계마다 메모리에서 표를 읽어야 한다. 4단계면 번역 한 번에 메모리 접근 4번 + 실제 데이터 1번 = 5번. 모든 메모리 접근이 5배 느려지는 셈이다. 이건 받아들일 수 없다 → 다음 세션의 TLB가 정확히 이 비용을 캐싱으로 없앤다.
6.4 대안 한 줄: 역 페이지 테이블
프레임 수에 비례하는 역 페이지 테이블(inverted page table) 도 있다(물리 프레임마다 "어느 프로세스의 어느 VPN인가"를 기록). 표가 물리 메모리 크기에만 비례해 작지만, 검색이 복잡해 보통 해시와 함께 쓴다. 주류는 다단계지만 설계 대안으로 알아두자.
7. 공유·보호, 그리고 다음 세션 예고
페이지 테이블은 번역기일 뿐 아니라 공유와 보호의 도구다.
- 공유 라이브러리: libc의 코드 페이지를 여러 프로세스의 페이지 테이블이 같은 물리 프레임으로 가리키면, 한 벌만 물리에 두고 모두가 공유한다.
- 읽기 전용 코드: text 페이지를 R/W=읽기전용 + NX 해제로 설정해 보호.
- copy-on-write 준비: fork 시 부모·자식이 같은 프레임을 읽기 전용으로 공유하다가, 쓰기 시 폴트로 복사(세션 6에서 깊게).
다음 세션은 (1) 번역 비용을 없애는 TLB, (2) Present 비트가 만드는 페이지 폴트와 디맨드 페이징, (3) mmap·스왑·COW 로 이어진다.
8. 흔한 오해 바로잡기
- ❌ "가상 주소가 곧 RAM의 그 위치다." → 가상 주소는 번역 전 값이다. MMU가 페이지 테이블로 물리 주소로 바꾼다.
- ❌ "offset도 번역된다." → offset은 그대로 복사. 번역되는 건 페이지 번호뿐.
- ❌ "페이지 테이블은 CPU 안에 있다." → 보통 물리 메모리에 있다(그래서 번역이 메모리 접근을 유발하고, TLB가 필요해진다). CR3가 그 시작 주소를 가리킨다.
- ❌ "다단계는 단일 표보다 항상 빠르다." → 다단계는 공간을 아끼지만 번역당 메모리 접근이 늘어 더 느릴 수 있다(그래서 TLB로 보완).
- ❌ "페이지가 작을수록 무조건 좋다." → 작으면 내부 단편화는 줄지만 테이블·TLB 부담이 커진다. 트레이드오프다.
9. 한 장 정리
- 가상 메모리 = 프로세스마다 번역되는 가상 주소를 쓰게 한 한 겹의 간접층 → 격리·추상화·효율.
- 번역은 페이지(4KB) 단위: 가상주소 = VPN ‖ offset, offset은 그대로, VPN만 PFN으로.
- PTE엔 PFN 외에 Present·R/W·NX·Accessed·Dirty 가 있어 보호·디맨드 페이징·교체의 토대가 된다.
- 단일 표는 48비트·4KB에서 프로세스당 512GB + 안 쓰는 영역까지 점유 → 불가.
- 다단계(트리) 는 쓰는 가지의 하위 표만 만들어 공간을 아끼지만, 번역당 메모리 접근이 늘어(4단계=5접근) → 세션 6의 TLB로 보완.
10. 복습 (15분) — 답을 가리고
Q1. 가상 메모리가 푸는 세 문제를 한 줄씩.
격리/보호(프로세스끼리 충돌·간섭 차단), 추상화(균일하고 큰 주소 공간이라는 단순 모델), 물리 메모리 효율(유연 배치·공유·디맨드 페이징).
Q2. 페이지 4KB일 때 가상 주소 0x00012F40의 VPN과 offset, 그리고 PFN=0x88이면 물리 주소는?
offset = 하위 12비트 =
0xF40. VPN =0x12. 물리 주소 = PFN ‖ offset =0x88F40(= 0x00088F40).
Q3. 페이지 크기를 8KB로 바꾸면 offset 비트 수는? 이유는?
13비트. offset 비트 = log₂(페이지 크기) = log₂(8192) = 13.
Q4. 48비트 주소, 4KB 페이지, PTE 8B에서 단일 페이지 테이블의 크기를 계산하라.
페이지 수 = 2^48/2^12 = 2^36. 크기 = 2^36 × 8 = 2^39 = 512GB (프로세스마다!).
Q5. 다단계 페이지 테이블이 공간을 아끼는 핵심 원리는?
표를 트리로 만들어, 실제 쓰는 영역에 해당하는 하위 표만 생성하고 안 쓰는 영역은 상위 항목을 invalid로 비워 거대한 하위 표 자체를 만들지 않는다. 쓰는 만큼만 표가 생긴다.
Q6. x86-64에서 48비트 주소는 어떻게 쪼개지며, 번역은 어디서 시작하나? 번역 한 번의 메모리 접근 수는?
9+9+9+9+12 (PML4/PDPT/PD/PT/offset). CR3가 가리키는 PML4에서 시작해 9비트씩 타고 내려간다(page walk). 4단계라 번역에 메모리 접근 4번 + 데이터 1번 = 총 5번.
Q7. PTE의 Present·Accessed·Dirty 비트는 각각 이후 어느 메커니즘에서 쓰이나?
Present → 페이지 폴트/디맨드 페이징(세션 6). Accessed → 교체 정책(LRU/클럭 근사). Dirty → 스왑 아웃 시 디스크에 다시 써야 하는지 판단.
다음: 세션 6 — 가상 메모리 ② TLB·페이지 폴트·mmap (이어지는 파일)