페이징
가상 주소 공간을 페이지와 프레임으로 나누고 페이지 테이블을 이용해 논리 주소를 물리 주소로 변환합니다.
세그먼테이션은 외부 단편화를 완전히 해결하지 못했습니다.
세그먼트 크기가 가변적이므로, 시간이 지나면 물리 메모리의 빈 공간이 흩어지는 문제는 여전합니다.
근본적인 해결책은 메모리를 고정 크기의 작은 블록으로 나누는 것입니다.
이것이 페이징(Paging)이며, 현대 운영체제 메모리 관리의 핵심입니다.
가상 메모리의 필요성
실제 RAM이 8GB인 컴퓨터에서 각각 4GB를 요구하는 프로그램 5개를 동시에 실행할 수 있을까요?
가능할 수 있습니다. 실제로 동시에 접근하는 페이지, OS가 사용하는 메모리, 스왑·파일 매핑과 할당 정책에 따라 달라집니다.
주소 공간을 예약한 양과 동시에 물리 RAM에 있어야 하는 양은 같지 않습니다.
가상 메모리(Virtual Memory)는 프로세스에게 물리 메모리보다 큰 주소 공간을 제공하면서, 실제로 사용하는 부분만 물리 메모리에 올리는 기법입니다.
아직 적재하지 않은 파일 내용은 파일에서 읽을 수 있고, 익명 페이지는 처음 쓸 때 0으로 초기화된 메모리를 받거나 스왑에서 복원될 수 있습니다. 모든 가상 페이지가 미리 디스크에 저장되는 것은 아닙니다.
프로세스는 마치 거대한 연속된 메모리를 독점하고 있는 것처럼 동작합니다.
32비트 주소의 이론적 범위는 4GiB이며, 사용자에게 허용되는 범위는 OS 배치에 따라 더 작습니다. 64비트 환경도 CPU가 구현한 주소 비트 수와 OS 정책에 따라 유한한 범위를 제공합니다.
이 환상을 만들어내는 핵심 메커니즘이 페이징입니다.
가상 메모리가 제공하는 이점:
- 물리 메모리 초과 사용: 물리 RAM보다 큰 프로그램을 실행할 수 있습니다.
- 프로세스 격리: 각 프로세스가 독립된 주소 공간을 가지므로, 허용되지 않은 매핑에 대한 접근을 제한합니다.
- 메모리 공유: 여러 프로세스가 같은 물리 페이지를 공유할 수 있습니다(공유 라이브러리).
- 보호: 페이지별로 읽기/쓰기/실행 권한을 설정할 수 있습니다.
페이지와 프레임
페이징에서 논리 메모리는 페이지(Page)라는 고정 크기 블록으로, 물리 메모리는 프레임(Frame)이라는 같은 크기의 블록으로 나뉩니다.
일반적인 페이지 크기:
- 4KiB (4,096바이트): x86과 여러 ARM 구성에서 사용하는 기본 단위.
- 2MiB / 1GiB: 지원되는 x86-64 구성의 대규모 페이지. CPU와 OS 설정을 확인합니다.
- 16KiB / 64KiB: 지원되는 ARM64 구성에서 선택할 수 있는 기본 단위.
이 절의 계산과 코드는 4KiB 페이지를 사용합니다.
프로세스의 페이지들은 물리 메모리의 아무 프레임에나 배치될 수 있습니다.
연속될 필요가 없습니다.
페이지 0은 프레임 5에, 페이지 1은 프레임 100에, 페이지 2는 프레임 3에 배치될 수 있습니다.
이것이 핵심입니다.
같은 크기의 단일 프레임을 할당하는 모델에서는 외부 단편화가 사라집니다. 대규모 페이지처럼 연속된 여러 프레임이 필요한 할당은 별도의 단편화 문제를 만날 수 있습니다.
모든 프레임은 크기가 동일하므로, 아무 빈 프레임이나 사용하면 됩니다.
연속된 큰 공간을 찾을 필요가 없습니다.
내부 단편화는 여전히 존재합니다.
프로세스 크기가 72,000바이트이면, 4KB 페이지 17개(69,632바이트)와 불완전한 18번째 페이지(2,368바이트 사용, 1,728바이트 낭비)가 필요합니다.
이 하나의 연속 영역을 페이지로 올림한 끝부분 낭비는 한 페이지 미만입니다. 여러 매핑·할당 영역의 끝부분 낭비를 합한 값까지 한 페이지 미만이라는 뜻은 아닙니다.
페이지 테이블
각 프로세스는 자신만의 페이지 테이블(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) | 수정 여부 (교체 시 디스크에 써야 하는지 판단) |
| 캐시 속성 | 해당 메모리의 캐시·메모리 유형 정책 |
비트의 이름과 배치는 아키텍처마다 다릅니다. 여기서 valid는 적재 여부를 단순화한 값이며, 실제 접근에는 주소 범위와 권한 검사도 필요합니다.
주소 변환 과정
페이지 번호만 프레임 번호로 바뀝니다
오프셋 8은 그대로 보존합니다. 원문 테이블을 대입한 계산이며 실제 MMU·권한 검사 실행 결과가 아닙니다.
PAGE_SIZE = 4096 # 4KB
def translate(logical_addr, page_table):
"""논리 주소를 물리 주소로 변환"""
page_number = logical_addr >> 12 # 상위 비트 = 페이지 번호
offset = logical_addr & 0xFFF # 하위 12비트 = 오프셋
if page_number < 0 or 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}")이 함수는 비음수 정수 주소와 4KiB 프레임 번호로 만든 교육용 테이블을 가정합니다. 권한 검사·실제 폴트 처리·TLB는 구현하지 않았으며, valid=False에서 던지는 Python 예외는 커널 폴트 실행이 아닙니다.
페이지 크기 선택의 트레이드오프
작은 페이지(4KB): 내부 단편화가 적습니다.
같은 크기의 영역을 매핑하면 더 많은 PTE와 TLB 엔트리가 필요합니다. 실제 미스율은 접근 패턴과 TLB 구성에 따라 달라집니다.
큰 페이지(2MB, 1GB): 같은 영역에 필요한 PTE 수를 줄이고 TLB 도달 범위를 넓힐 수 있습니다.
하지만 내부 단편화가 커지고, 세밀한 메모리 보호가 어렵습니다.
Linux의 Transparent Huge Pages(THP)는 정책과 메모리 상태에 따라 대규모 매핑을 사용하며, 최신 커널은 전통적인 PMD 크기 외에 여러 작은 크기의 mTHP도 지원합니다. 데이터베이스의 명시적 HugePages 설정도 워크로드와 OS 지원을 확인해 선택합니다.
이 변환은 모든 메모리 접근에서 발생합니다.
하나의 명령어를 실행하는 데도 여러 번의 메모리 접근이 필요하므로, 변환 속도가 매우 중요합니다.
다음 절에서는 이 변환을 빠르게 만드는 TLB와 페이지 테이블 최적화를 다루겠습니다.