가상 메모리의 페이징과 페이지 관리
가상 메모리의 주소 변환, 요구 페이징, 페이지 교체, 스래싱과 TLB·NUMA·가상화 환경의 최적화 방식을 정리한다.
2026-08-14 · 최초 발행 2026-01-04
물리 메모리에 모두 올릴 수 없는 프로그램도 실행할 수 있는 이유는 프로세스가 보는 주소와 실제 RAM 주소가 분리돼 있기 때문이다. 가상 메모리는 이 분리를 바탕으로 보조기억장치를 활용하고, 프로세스 격리·메모리 보호·공유까지 함께 제공한다.
논리 주소를 물리 메모리에 연결하는 방식
프로세스는 논리 주소(Virtual Address)를 사용하고, MMU는 이를 물리 주소(Physical Address)로 변환한다. 논리 메모리는 일정한 크기의 페이지(Page)로 나뉘며, 물리 메모리에서는 같은 크기의 프레임(Frame)에 배치된다. 일반적인 페이지 크기는 4KB다.
이 구조는 물리 메모리보다 큰 프로그램의 실행을 가능하게 한다. 각 프로세스에는 독립된 주소 공간이 주어지고, 다른 프로세스 메모리로의 접근을 막을 수 있다. 필요한 경우에는 여러 프로세스가 메모리를 공유하며, 프로그램을 특정 물리 위치에 고정하지 않고 로드할 수 있다.
페이지 테이블이 주소를 해석하는 과정
논리 주소는 페이지 번호와 페이지 오프셋으로 나뉜다. 페이지 번호로 페이지 테이블을 찾고, 여기서 얻은 프레임 번호와 오프셋을 조합해 물리 주소를 만든다.
페이지 테이블 엔트리(PTE)는 단순히 프레임 위치만 담지 않는다. 페이지가 현재 메모리에 있는지를 나타내는 유효 비트(Valid Bit), 수정 여부를 기록하는 변경 비트(Dirty Bit), 최근 접근 여부를 나타내는 참조 비트(Reference Bit), 읽기·쓰기·실행 권한을 담는 보호 비트(Protection Bits)가 함께 관리된다.
주소 공간이 커지면 하나의 페이지 테이블을 모두 유지하는 비용도 커진다. 다단계 페이지 테이블은 실제 사용하는 주소 영역에 대해서만 하위 테이블을 만들 수 있어 희소한 주소 공간에 적합하다.
x86-64는 4단계 페이징을 사용하며 최신 CPU에서는 5단계 페이징도 사용한다. 여러 단계의 테이블을 거치면 메모리 접근 횟수가 늘어나지만, TLB가 이 부담을 완화한다. 일반 페이지와 대용량 페이지를 함께 운용할 수도 있다.
필요한 순간에 적재하는 요구 페이징
요구 페이징(Demand Paging)은 페이지를 실행 시작 시점에 모두 적재하지 않는다. CPU가 접근한 페이지가 메모리에 없을 때만 페이지 폴트를 일으켜 가져온다.
유효 비트가 0인 페이지에 접근하면 하드웨어가 트랩을 생성한다. 운영체제는 현재 레지스터 상태를 보존한 뒤 디스크에서 해당 페이지를 읽는다. 이 I/O에는 수 ms가 걸릴 수 있다. 이후 프레임 번호와 유효 비트를 갱신하고, 폴트를 일으킨 명령어를 다시 실행한다.
유효 접근 시간(EAT)은 (1-p) × 메모리 접근 시간 + p × 페이지 폴트 시간으로 나타낼 수 있다. 여기서 p는 페이지 폴트율이며 일반적으로 0.001 이하가 목표다. 시간적·공간적 지역성을 활용하고 활발히 쓰는 페이지 집합인 워킹셋을 유지하는 일이 폴트율 관리의 기반이 된다.
교체 대상은 어떤 기준으로 고르는가
빈 프레임이 없으면 새 페이지를 넣기 위해 기존 페이지 하나를 내보내야 한다. FIFO는 가장 먼저 적재된 페이지를 교체한다. 구현은 단순하지만 성능이 최적이라는 보장은 없고 Belady's Anomaly가 발생할 수 있다.
LRU(Least Recently Used)는 가장 오래 사용되지 않은 페이지를 교체한다. 지역성을 활용해 히트율이 우수하지만, 모든 접근 때 사용 이력을 갱신해야 한다. 스택이나 카운터로 추적할 수 있으며, Clock이나 NRU 같은 근사 알고리즘으로 구현 부담을 줄일 수 있다.
Clock 알고리즘은 FIFO에 참조 비트를 결합한 Second Chance 방식이다. 포인터가 원형 리스트를 순회하며 참조 비트가 1인 페이지는 0으로 바꾸고 다음 페이지로 이동한다. 참조 비트가 0인 페이지가 교체 대상이 된다.
참조 비트는 MMU가 자동으로 설정한다. Clock은 LRU의 근사 방식이면서 오버헤드가 낮고, 참조 비트와 변경 비트를 함께 쓰는 Enhanced Clock 변형도 있다. 많은 운영체제에서 실제로 사용되는 이유다.
Optimal 알고리즘은 미래에 가장 나중에 쓰일 페이지를 교체하는 이론적 최선의 방식이다. 미래 접근을 예측할 수 없으므로 일반 환경에서는 구현할 수 없으며, 사전에 접근 패턴을 아는 오프라인 환경에서만 가능하다. 다른 알고리즘의 성능을 비교하는 기준과 연구 도구로 쓰인다.
스래싱이 만드는 악순환
스래싱(Thrashing)은 프로세스가 필요한 페이지보다 적은 프레임을 받으면서 페이지 폴트와 디스크 I/O에 대부분의 시간을 쓰는 상태다.
페이지 폴트가 과도해지면 CPU는 유휴 상태가 되고, 더 많은 프로세스를 넣어도 상황은 악화된다. 결국 실제 작업은 거의 진행되지 않아 시스템이 마비된 것처럼 보일 수 있다.
대응에는 각 프로세스의 워킹셋 크기만큼 프레임을 할당하는 워킹셋 모델이 있다. 페이지 폴트 빈도(PFF)를 관찰해 프레임을 조정하거나, 일부 프로세스를 스왑 아웃해 경쟁을 줄일 수도 있다. 프로세스별 지역 교체 정책과 CPU·I/O 균형을 고려한 다중 프로그래밍 수준도 필요하다.
주소 변환과 스왑 경로의 병목 줄이기
TLB(Translation Lookaside Buffer)는 주소 변환 결과를 보관하는 캐시다. 논리 주소가 들어오면 우선 TLB를 검색하고, 미스일 때만 페이지 테이블을 조회한다.
TLB는 완전 연관 또는 세트 연관 캐시로 구성되며 크기는 64-512 엔트리다. 히트율은 일반적으로 95-99%이고, TLB 미스는 성능에 큰 영향을 준다. ASID/PCID는 컨텍스트 스위칭 때 TLB 플러시를 피하는 데 쓰인다.
대형 페이지(Huge Pages)는 아키텍처에 따라 2MB, 1GB 등의 크기를 사용한다. 하나의 TLB 엔트리로 더 큰 메모리 영역을 다룰 수 있어 데이터베이스, 가상화, HPC에 적용된다. 리눅스의 Transparent Huge Pages (THP)가 여기에 해당한다. 다만 내부 단편화와 교체 시 오버헤드는 고려해야 한다.
Copy-On-Write(COW)는 쓰기 전까지 페이지를 공유한다. fork() 과정에서 부모와 자식 프로세스가 메모리를 공유하고, 실제 수정이 발생할 때만 복사하므로 읽기 전용 페이지는 계속 공유할 수 있다. 메모리를 절약하면서 프로세스 생성 속도를 대폭 향상시키는 방식이다.
선행 페이징(Prepaging)은 필요한 페이지를 미리 적재하고, 클러스터링은 연속된 페이지를 한 번에 스왑한다. 스왑 파티션/파일에서는 전용 파티션이 파일보다 빠르며, zswap·zram 같은 압축 메모리는 스왑 전에 메모리를 압축한다. SSD에 스왑을 배치하면 지연을 줄일 수 있다.
64비트·NUMA·가상화에서 달라지는 조건
64비트 주소 공간의 이론적 크기는 2^64 = 16 엑사바이트다. 실제 지원 범위는 48비트(256TB) 또는 57비트(128PB)이며, Intel의 최신 CPU는 5단계 페이징을 지원한다. 주소 공간 대부분은 사용하지 않으므로 해당 영역의 테이블도 만들지 않으며, 파일을 주소 공간에 직접 매핑할 수 있다.
NUMA 시스템에서는 프로세서마다 로컬과 원격 메모리의 접근 속도가 다르다. 로컬 메모리를 우선 할당하고, 접근 패턴에 따라 페이지를 마이그레이션해 원격 메모리 접근을 줄인다. 리눅스는 numactl, AutoNUMA를 지원한다.
가상화 환경의 중첩 페이징은 게스트 가상 주소를 게스트 물리 주소로, 다시 호스트 물리 주소로 변환한다. Intel EPT와 AMD NPT가 이 과정에 사용되며, 2차원 페이지 워킹은 지연을 증가시킨다. 가상 머신별 TLB 엔트리를 구분하는 TLB 태깅과 게스트 메모리를 회수하는 메모리 벌루닝도 함께 사용된다.