페이징
세그먼테이션은 외부 단편화를 완전히 해결하지 못했습니다. 세그먼트 크기가 가변적이므로, 시간이 지나면 프레임의 빈 공간이 흩어지는 문제는 여전합니다. 근본적인 해결책은 메모리를 고정 크기의 작은 블록으로 나누는 것입니다. 이것이 페이징(Paging)이며, 현대 운영체제 메모리 관리의 핵심입니다.
가상 메모리의 필요성
실제 RAM이 8GB인 컴퓨터에서 각각 4GB를 요구하는 프로그램 5개를 동시에 실행할 수 있을까요? 가능합니다. 모든 프로그램이 4GB 전부를 동시에 사용하지는 않기 때문입니다.
가상 메모리(Virtual Memory)는 프로세스에게 물리 메모리보다 큰 주소 공간을 제공하면서, 실제로 사용하는 부분만 물리 메모리에 올리는 기법입니다. 나머지는 디스크에 보관됩니다.
프로세스는 마치 거대한 연속된 메모리를 독점하고 있는 것처럼 동작합니다. 32비트 프로세스는 4GB, 64비트 프로세스는 사실상 무한한 주소 공간을 봅니다. 이 환상을 만들어내는 핵심 메커니즘이 페이징입니다.
가상 메모리가 제공하는 이점:
- 물리 메모리 초과 사용: 물리 RAM보다 큰 프로그램을 실행할 수 있습니다.
- 프로세스 격리: 각 프로세스가 독립된 주소 공간을 가지므로, 다른 프로세스의 메모리를 침범할 수 없습니다.
- 메모리 공유: 여러 프로세스가 같은 물리 페이지를 공유할 수 있습니다(공유 라이브러리).
- 보호: 페이지별로 읽기/쓰기/실행 권한을 설정할 수 있습니다.
페이지와 프레임
페이징에서 논리 메모리는 페이지(Page)라는 고정 크기 블록으로, 물리 메모리는 프레임(Frame)이라는 같은 크기의 블록으로 나뉩니다.
일반적인 페이지 크기:
- 4KB (4,096 바이트): x86, ARM의 기본 페이지 크기. 가장 보편적.
- 2MB / 1GB: x86의 대규모 페이지(Huge Page). 데이터베이스, 과학 계산에서 사용.
- 16KB / 64KB: ARM64에서 선택 가능. Apple Silicon(macOS)은 16KB 사용.
프로세스의 페이지들은 물리 메모리의 아무 프레임에나 배치될 수 있습니다. 연속될 필요가 없습니다. 페이지 0은 프레임 5에, 페이지 1은 프레임 100에, 페이지 2는 프레임 3에 배치될 수 있습니다.
이것이 핵심입니다. 외부 단편화가 완전히 사라집니다. 모든 프레임은 크기가 동일하므로, 아무 빈 프레임이나 사용하면 됩니다. 연속된 큰 공간을 찾을 필요가 없습니다.
내부 단편화는 여전히 존재합니다. 프로세스 크기가 72,000바이트이면, 4KB 페이지 17개(69,632바이트)와 불완전한 18번째 페이지(2,368바이트 사용, 1,728바이트 낭비)가 필요합니다. 하지만 최대 낭비는 한 페이지 미만(4KB 미만)이므로 연속 할당의 단편화에 비하면 매우 작습니다.
페이지 테이블
각 프로세스는 자신만의 페이지 테이블(Page Table)을 가집니다. 페이지 테이블은 논리적 페이지 번호를 물리적 프레임 번호로 매핑합니다.
논리 주소의 구조
논리 주소는 두 부분으로 나뉩니다.
- 페이지 번호(p): 상위 비트. 페이지 테이블의 인덱스.
- 페이지 오프셋(d): 하위 비트. 페이지 내의 바이트 위치.
32비트 시스템에서 페이지 크기가 4KB()이면:
- 하위 12비트 = 오프셋 (0~4095)
- 상위 20비트 = 페이지 번호 (개 페이지)
64비트 시스템(x86-64, 48비트 주소 사용)에서:
- 하위 12비트 = 오프셋
- 상위 36비트 = 페이지 번호 (억 개 페이지)
페이지 테이블 엔트리(PTE)
각 엔트리에는 프레임 번호 외에 여러 제어 비트가 포함됩니다.
| 비트 | 용도 |
|---|---|
| 유효 비트(Valid) | 1: 메모리에 존재, 0: 디스크에 있거나 미할당 |
| 보호 비트(Protection) | R/W/X 권한 |
| 참조 비트(Reference) | 최근 접근 여부 (페이지 교체 알고리즘에 사용) |
| 더티 비트(Dirty/Modified) | 수정 여부 (교체 시 디스크에 써야 하는지 판단) |
| 캐시 비트 | 캐시 활성화 여부 (I/O 매핑 메모리에서 비활성화) |
주소 변환 과정
CPU가 논리 주소를 생성하면 MMU가 다음 과정을 수행합니다.
- 논리 주소에서 페이지 번호 p와 오프셋 d를 추출합니다.
- 페이지 번호 p로 페이지 테이블을 조회하여 프레임 번호 f를 얻습니다.
- 물리 주소 =
PAGE_SIZE = 4096 # 4KB
def translate(logical_addr, page_table):
"""논리 주소를 물리 주소로 변환"""
page_number = logical_addr >> 12 # 상위 비트 = 페이지 번호
offset = logical_addr & 0xFFF # 하위 12비트 = 오프셋
if page_number >= len(page_table):
raise Exception(f"Invalid page: {page_number}")
entry = page_table[page_number]
if not entry["valid"]:
raise Exception(f"Page fault at page {page_number}")
frame_number = entry["frame"]
physical_addr = (frame_number << 12) | offset
return physical_addr
# 페이지 테이블 예제
page_table = [
{"frame": 5, "valid": True}, # 페이지 0 → 프레임 5
{"frame": 8, "valid": True}, # 페이지 1 → 프레임 8
{"frame": 1, "valid": True}, # 페이지 2 → 프레임 1
{"frame": 0, "valid": False}, # 페이지 3 → 디스크 (page fault)
]
# 논리 주소 8200 = 페이지 2 (8200 // 4096 = 2), 오프셋 8
logical = 8200
physical = translate(logical, page_table)
print(f"논리: {logical} → 물리: {physical}")
# 페이지 2 → 프레임 1, 물리 = 1*4096 + 8 = 4104
# 논리 주소 0x3100 → 페이지 3 → page fault!
try:
translate(0x3100, page_table)
except Exception as e:
print(f"예외: {e}")페이지 크기 선택의 트레이드오프
작은 페이지(4KB): 내부 단편화가 적습니다. 하지만 페이지 테이블이 커지고, TLB 미스가 자주 발생합니다.
큰 페이지(2MB, 1GB): 페이지 테이블이 작아지고, TLB 적중률이 높아집니다. 하지만 내부 단편화가 커지고, 세밀한 메모리 보호가 어렵습니다.
현대 시스템은 기본 4KB + 선택적 대규모 페이지를 지원합니다. Linux의 Transparent Huge Pages(THP)는 자동으로 2MB 페이지를 활용합니다. 데이터베이스(Oracle, PostgreSQL)는 성능을 위해 명시적으로 HugePages를 설정합니다.
이 변환은 모든 메모리 접근에서 발생합니다. 하나의 명령어를 실행하는 데도 여러 번의 메모리 접근이 필요하므로, 변환 속도가 매우 중요합니다. 다음 절에서는 이 변환을 빠르게 만드는 TLB와 페이지 테이블 최적화를 다루겠습니다.