왜 가상 메모리가 필요한가?

초기 컴퓨터에서는 프로그램이 물리 메모리의 주소를 직접 사용했다. 프로그램 A가 주소 0x1000을 쓰고 있는데 프로그램 B도 같은 주소를 사용하면 충돌이 발생한다. 여러 프로그램을 동시에 실행하려면 각 프로그램이 메모리의 어느 영역을 사용할지 미리 조율해야 했고, 이는 실용적이지 않았다.

가상 메모리는 이 문제를 근본적으로 해결한다. 각 프로세스에게 자신만의 독립된 주소 공간을 부여하여, 모든 프로세스가 주소 0x0부터 시작하는 것처럼 동작할 수 있게 한다. 프로세스 A가 보는 주소 0x1000과 프로세스 B가 보는 주소 0x1000은 물리 메모리에서 전혀 다른 위치에 매핑된다. 이 변환을 하드웨어적으로 수행하는 장치가 MMU(Memory Management Unit)이다.

주소 변환의 원리

가상 메모리 시스템에서 CPU가 생성하는 모든 주소는 가상 주소이다. 이 가상 주소가 실제 물리 메모리의 물리 주소로 변환되어야 메모리에 접근할 수 있다. 변환의 기본 단위는 페이지(page)로, 일반적으로 4KB 크기이다.

가상 주소 (예: 0x00401234)
┌──────────────────┬──────────────┐
│  가상 페이지 번호  │  페이지 오프셋 │
│   (VPN: 0x401)   │  (0x234)     │
└────────┬─────────┴──────┬───────┘
         │                │
    페이지 테이블         │
    에서 변환             │
         │                │
         ▼                │
┌──────────────────┬──────┴───────┐
│  물리 프레임 번호  │  페이지 오프셋 │
│   (PFN: 0x8A3)   │  (0x234)     │
└──────────────────┴──────────────┘
물리 주소 (예: 0x8A3234)

가상 페이지 번호(VPN)가 물리 프레임 번호(PFN)로 변환되고, 페이지 내부의 오프셋은 그대로 유지된다. 이 매핑 정보를 저장하는 자료 구조가 페이지 테이블이다.

페이지 테이블

가장 단순한 형태의 페이지 테이블은 단일 레벨 페이지 테이블이다. 가상 페이지 번호를 인덱스로 사용하여 물리 프레임 번호를 직접 조회하는 배열 구조이다. 그런데 이 방식에는 치명적인 문제가 있다. 64비트 주소 공간에서 4KB 페이지를 사용하면 페이지 테이블 엔트리가 2^52개 필요하다. 엔트리 하나가 8바이트라면 페이지 테이블 자체가 32PB의 메모리를 차지하게 되는 셈이다.

이 문제를 해결하기 위해 현대 프로세서는 다단계 페이지 테이블을 사용한다. x86-64에서는 4단계 페이지 테이블 구조를 채택하고 있다.

가상 주소 (48비트 사용)
┌────────┬────────┬────────┬────────┬──────────────┐
│ PML4   │ PDPT   │  PD    │  PT    │   Offset     │
│ (9bit) │ (9bit) │ (9bit) │ (9bit) │   (12bit)    │
└───┬────┴───┬────┴───┬────┴───┬────┴──────────────┘
    │        │        │        │
    ▼        ▼        ▼        ▼
  PML4 → PDP 테이블 → PD 테이블 → PT 테이블 → 물리 프레임

이 구조의 핵심적인 장점은 사용되지 않는 주소 범위에 대해서는 하위 테이블을 아예 할당하지 않아도 된다는 것이다. 실제 프로세스가 사용하는 메모리는 전체 가상 주소 공간의 극히 일부에 불과하므로, 다단계 구조를 통해 페이지 테이블의 메모리 소비를 크게 줄일 수 있다.

TLB: 주소 변환의 캐시

4단계 페이지 테이블을 사용한다는 것은 하나의 메모리 접근을 위해 최대 4번의 추가적인 메모리 접근이 필요하다는 뜻이다. 메모리 접근 한 번에 수십 나노초가 걸리므로, 이 오버헤드는 받아들일 수 없는 수준이 된다.

이 문제를 해결하는 것이 TLB(Translation Lookaside Buffer)이다. TLB는 최근에 사용된 가상 주소-물리 주소 매핑을 저장하는 작은 캐시로, CPU 내부에 위치한다. TLB에 원하는 매핑이 있으면(TLB 히트) 추가적인 메모리 접근 없이 즉시 물리 주소를 얻을 수 있다.

과연 이 작은 캐시가 효과적일 수 있는 것인가? 그럴 수 있다. 프로그램의 메모리 접근에는 강한 지역성이 존재하기 때문이다. 일반적인 워크로드에서 TLB 히트율은 99%를 넘는다. TLB 엔트리가 64~1024개 정도밖에 되지 않음에도 이러한 높은 히트율이 가능한 이유는, 하나의 TLB 엔트리가 4KB 페이지 전체를 커버하기 때문이다. 64개의 엔트리만으로도 256KB의 메모리 영역에 대한 변환을 캐싱할 수 있는 셈이다.

페이지 폴트와 요구 페이징

프로세스가 접근하려는 페이지가 물리 메모리에 존재하지 않는 경우 페이지 폴트가 발생한다. 이는 하드웨어가 발생시키는 예외로, CPU가 운영체제의 페이지 폴트 핸들러로 제어를 넘기게 된다.

운영체제는 디스크(또는 SSD)에서 해당 페이지를 물리 메모리로 읽어 들이고, 페이지 테이블을 업데이트한 후, 중단되었던 명령어를 재실행한다. 이 메커니즘이 요구 페이징(demand paging)이다. 프로그램의 모든 페이지를 처음부터 메모리에 로드하지 않고, 실제로 접근하는 시점에 필요한 페이지만 로드한다.

이 방식 덕분에 물리 메모리보다 큰 프로그램도 실행할 수 있다. 8GB의 물리 메모리를 가진 시스템에서 각각 4GB를 사용하는 프로세스 세 개를 동시에 실행할 수 있는 이유가 바로 이것이다. 모든 프로세스의 모든 페이지가 동시에 물리 메모리에 존재할 필요는 없으며, 현재 활발히 사용되는 페이지만 물리 메모리에 유지하면 되기 때문이다.

페이지 교체 알고리즘

물리 메모리가 부족해지면 운영체제는 기존 페이지 중 하나를 디스크로 내보내(evict) 새로운 페이지를 위한 공간을 확보해야 한다. 이때 어떤 페이지를 내보낼 것인지를 결정하는 것이 페이지 교체 알고리즘이다.

이론적으로 최적의 알고리즘은 가장 먼 미래에 사용될 페이지를 교체하는 것이다(OPT 알고리즘). 그러나 미래를 예측할 수 없으므로 실용적인 근사 알고리즘이 필요하다. LRU(Least Recently Used)는 가장 오래전에 사용된 페이지를 교체하는 방식으로, 과거의 접근 패턴이 미래에도 유사할 것이라는 가정에 기반한다. 실제 운영체제에서는 정확한 LRU 구현의 오버헤드가 크기 때문에 참조 비트를 활용한 클럭 알고리즘과 같은 근사 LRU를 사용한다. Linux의 경우 활성 리스트와 비활성 리스트를 분리하여 관리하는 이중 리스트 기반의 근사 LRU를 구현하고 있다.

메모리 보호

페이지 테이블은 주소 변환뿐 아니라 메모리 보호 기능도 제공한다. 각 페이지 테이블 엔트리에는 해당 페이지에 대한 접근 권한 비트가 포함되어 있다.

비트의미
Present페이지가 물리 메모리에 존재하는지 여부
Read/Write읽기 전용인지 읽기/쓰기 가능인지
User/Supervisor사용자 모드에서 접근 가능한지
Execute Disable (NX)해당 페이지의 코드 실행을 금지할지

사용자 프로세스가 커널 메모리에 접근하려 하면 MMU가 이를 감지하여 예외를 발생시킨다. 읽기 전용 페이지에 쓰기를 시도하거나, 데이터 영역의 코드를 실행하려는 시도도 마찬가지이다. NX 비트는 버퍼 오버플로우 공격에서 스택에 주입된 셸코드의 실행을 방지하는 데 핵심적인 역할을 한다. 이처럼 가상 메모리는 프로세스 간 격리뿐 아니라 보안의 기반이 된다.

대용량 페이지

기본 4KB 페이지만으로는 대규모 메모리를 사용하는 워크로드에서 문제가 발생한다. 64GB의 메모리를 매핑하려면 약 1,600만 개의 페이지 테이블 엔트리가 필요하고, TLB가 커버할 수 있는 범위도 제한적이다.

대용량 페이지(huge page)는 이 문제를 완화한다. x86-64에서는 2MB와 1GB 크기의 대용량 페이지를 지원한다. 2MB 페이지를 사용하면 동일한 메모리 범위를 매핑하는 데 필요한 TLB 엔트리 수가 512분의 1로 줄어들어 TLB 미스율이 크게 감소한다. 데이터베이스나 가상 머신과 같이 대규모 메모리를 연속적으로 접근하는 워크로드에서 대용량 페이지의 성능 향상 효과는 상당한 것이다.

역 페이지 테이블

전통적인 페이지 테이블은 가상 주소 공간 크기에 비례하여 메모리를 소비한다. 역 페이지 테이블(inverted page table)은 반대로 물리 메모리 크기에 비례하는 자료 구조이다. 물리 프레임마다 하나의 엔트리를 두고, 해당 프레임을 어떤 프로세스의 어떤 가상 페이지가 사용하고 있는지를 기록한다.

이 방식은 메모리 사용량 측면에서 효율적이지만, 가상 주소에서 물리 주소로의 변환이 배열 인덱싱이 아닌 해시 테이블 검색이 되므로 변환 시간이 증가할 수 있다. PowerPC와 IA-64 등 일부 아키텍처에서 사용되었으나, 현재 주류인 x86-64와 ARM에서는 다단계 페이지 테이블이 표준으로 자리잡고 있다.

가상 메모리가 가능하게 하는 것들

가상 메모리는 단순한 주소 변환 메커니즘이 아니다. 프로세스 격리, 메모리 보호, 요구 페이징, Copy-on-Write, 메모리 매핑 파일, 공유 라이브러리 등 현대 운영체제의 핵심 기능들이 모두 가상 메모리를 기반으로 구현된다. fork() 시스템 콜이 효율적으로 동작하는 것도, mmap()으로 파일을 메모리처럼 접근할 수 있는 것도, 여러 프로세스가 libc의 코드를 공유하면서도 서로의 데이터에 접근할 수 없는 것도 모두 가상 메모리 덕분인 것이다.

다음 글에서는 I/O와 DMA를 다룬다.