--- id: 2f2ffafc-91ff-4971-a6a8-7c0e7d844ca2 kind: CONCEPT slug: guest-memory-address-translation title: Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA topic: memory-virtualization topicName: 메모리 가상화 project: virtualization status: 초안 basisVersion: x86-64 Intel EPT · AMD 는 NPT 계열로 대응 · Linux KVM 의 KVM_SET_USER_MEMORY_REGION · QEMU/libvirt · 기본 page 4 KiB studio: "https://hyeonworks.com/studio/documents/2f2ffafc-91ff-4971-a6a8-7c0e7d844ca2/edit" assets: - key: guest-memory-address-translation-path file: ../../../final/assets/diagrams/guest-memory-address-translation-path/guest-memory-address-translation-path.svg sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다 source: - final/document.md#29-이-문서에서-먼저-고정할-전체-구조 - final/document.md#30-일반-linux의-virtual-memory부터-시작한다 - final/document.md#31-page와-physical-frame - final/document.md#32-virtual-address-=-page-+-offset - final/document.md#33-guest-page-table - final/document.md#34-mmu-실제-주소-변환을-수행하는-cpu-하드웨어 - final/document.md#35-tlb-주소-변환-결과의-cpu-cache - final/document.md#36-bare-metal과-vm의-차이 - final/document.md#37-ept-extended-page-tables - final/document.md#38-왜-ept가-필요한가 - final/document.md#39-shadow-page-table과-ept의-의미 - final/document.md#40-qemu는-guest-ram을-어떻게-준비하는가 - final/document.md#41-kvm_set_user_memory_region - final/document.md#42-configured-memory와-실제-physical-ram-사용량은-같지-않을-수-있다 - final/document.md#43-guest-page-table-자체도-메모리에-있다 - final/document.md#44-정상-memory-access는-매번-vm-exit하지-않는다 - final/document.md#79-전체-memory-virtualization-실행-경로 - final/document.md#80-전체-memory-virtualization-관리-경로 - final/document.md#82-핵심-claim-registry - final/document.md#87-최종-기준-그림 - final/document.md#88-결론 --- # Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA 가상 머신 안의 프로세스가 메모리를 한 번 읽으면 그 주소는 이름이 두 번 바뀐다. 게스트 페이지 테이블이 GVA(Guest Virtual Address)를 GPA(Guest Physical Address)로 바꾼다. 그 GPA 를 EPT 가 다시 HPA(Host Physical Address)로 바꾼 뒤에야 접근이 실제 호스트 RAM 에 닿는다. 게스트 Linux 는 GPA 를 자기 물리 주소로 여기지만, 그 값이 서버 RAM 의 주소와 같을 필요는 없다. 메모리를 마련하는 쪽과 주소를 바꾸는 쪽도 갈린다. QEMU 가 게스트 RAM 을 자기 호스트 가상 주소 공간에 마련해 KVM 에 등록하고, 접근마다 일어나는 변환은 CPU 의 MMU 가 맡는다. 가상 머신 위에서 Keycloak 실험을 돌리는 동안 「가상 머신에 RAM 을 몇 GiB 줬다」가 호스트 RAM 에서 무엇을 뜻하는지 가르려면 이 경로가 먼저 필요하다. ## 관계 - **VM 에서 page fault 는 세 계층에서 따로 일어난다** 이 경로의 어느 단계에서 접근이 완료되지 못하는지, 그때 어느 계층이 그 사건을 받는지를 그 글이 이어 받는다. - **Huge Page 를 VM 에서 볼 때 갈라지는 세 자리** 페이지 크기를 키우는 선택이 이 경로의 두 변환 단계 가운데 어디에 걸리는지를 다룬다. - **Host 메모리 압박이 Guest 와 스토리지까지 내려가는 경로** HPA 뒤의 호스트 RAM 이 모자라질 때 메모리 회수(reclaim)와 스왑이 무엇을 하는지는 그 글이 맡는다. - **NUMA 에서는 vCPU 배치와 메모리 배치를 함께 본다** 이 경로의 마지막 단계인 호스트 물리 페이지가 어느 NUMA 노드에 있는지를 다룬다. - **이 호스트의 가상 머신들은 설정한 메모리와 지금 잡고 있는 메모리가 얼마나 다른가** 설정한 메모리와 게스트가 쓰는 메모리, 호스트에서 실제로 상주(resident)하는 메모리를 이 글은 구분만 했고 값은 재지 않았다. - **QEMU 프로세스가 실제로 붙잡고 있는 호스트 메모리는 얼마이고 어떻게 나뉘어 있는가** QEMU 가 마련한 메모리가 이 서버에서 실제로 물리 메모리를 얼마나 붙잡는지를 그 질문이 받는다. - **메모리 증상 하나로 계층을 단정하지 않는다** 이 경로가 그 기준이 쓰는 계층 구분을 만든다. - **KVM 에서 vCPU 가 물리 CPU 에서 실행되기까지** 같은 가상 머신을 CPU 쪽에서 본 글이다. VM Exit 을 그쪽에서 읽고 오면 메모리 접근마다 Exit 이 나는지가 먼저 걸린다. ## 본문 ## GVA · GPA · HPA — 주소 이름이 두 번 바뀐다 KVM/QEMU 가상 머신에서 게스트 애플리케이션이 메모리를 한 번 읽는 길을 가장 단순하게 그리면 이렇다. ```text label="Guest Application 의 memory access 경로" Guest Application │ │ Guest Virtual Address (GVA) ▼ Guest Page Table │ │ Guest Physical Address (GPA) ▼ EPT (Intel) / NPT (AMD) │ │ Host Physical Address (HPA) ▼ Physical RAM ``` 세 주소가 각각 누구의 주소인지부터 갈라 놓는다. | 주소 | 누구의 주소인가 | |---|---| | GVA | 게스트 프로세스가 쓰는 가상 주소 | | GPA | 게스트 OS 가 물리 메모리라고 여기는 주소 | | HPA | 실제 호스트 서버 RAM 의 물리 주소 | 게스트 안에서 도는 Keycloak 이 변수 하나를 읽는다고 하면, 그 한 번의 접근이 세 주소를 차례로 지난다. ```text label="Guest 안의 Keycloak 이 변수를 읽을 때 (SSOT 의 예시 주소)" Keycloak │ │ GVA 0x7f001234 ▼ Guest Page Table │ │ GPA 0x00101234 ▼ EPT │ │ HPA 0x8a101234 ▼ Physical RAM ``` 게스트 Linux 는 GPA 를 자기 실제 물리 주소라고 여긴다. 가상 머신이므로 그 GPA 가 실제 서버의 HPA 와 같을 필요는 없고, KVM 과 CPU 가상화 계층이 둘을 갈라 둔다. 위 주소 값은 원문이 설명하려고 든 예시이고 이 서버에서 읽은 값이 아니다. ## 일반 Linux 프로세스도 물리 주소를 직접 쓰지 않는다 첫 단계는 KVM 고유의 기능이 아니다. 일반적인 Linux 프로세스도 실제 RAM 주소를 직접 쓰지 않는다. 게스트 안에서 Keycloak 과 PostgreSQL 이 함께 돌면 두 프로세스에 각각 독립된 가상 주소 공간이 생긴다. 그래서 둘 다 `0x1000` 이라는 주소를 쓸 수 있는데, 같은 가상 주소라도 서로 다른 물리 프레임으로 매핑할 수 있기 때문이다. 그리고 가상 머신 안에서 이 물리 주소는 정확히는 GPA 다. Linux 는 메모리를 주소 하나씩 매핑하지 않고 일정 크기 단위로 나누어 관리한다. x86-64 Linux 에서 흔히 쓰는 기본 페이지 크기는 4 KiB 이고, 물리 메모리도 같은 크기의 프레임 단위로 볼 수 있다. 그래서 가상 주소는 페이지 번호와 페이지 안의 오프셋으로 갈린다. ```text label="4 KiB page 에서 0x1234 를 나눈 결과" Virtual Address 0x1234 ┌──────────────┬─────────────┐ │ Virtual Page │ Offset │ │ 1 │ 0x234 │ └──────────────┴─────────────┘ ``` 페이지 테이블에 가상 페이지 1 이 게스트 물리 프레임 7 로 이어지는 매핑이 있으면, 변환 뒤에도 페이지 안의 오프셋 `0x234` 는 그대로다. 페이지 테이블은 어느 물리 프레임으로 갈 것인가를 정하고 오프셋은 건드리지 않는다. ## 페이지 테이블은 게스트 커널이 만들고 MMU 가 읽는다 게스트 Linux 커널은 프로세스마다 가상 페이지와 게스트 물리 프레임의 매핑을 관리한다. 프로세스를 만들 때, `mmap()` 을 부를 때, 페이지를 할당할 때, 권한을 바꿀 때, copy-on-write 가 일어날 때 페이지 테이블을 만들거나 고친다. 다만 CPU 가 메모리에 접근할 때마다 게스트 커널 코드가 직접 테이블을 하나씩 검색하지는 않는다. 주소 변환을 실제로 수행하는 주체는 CPU 의 MMU(Memory Management Unit, 메모리 관리 장치)다. MMU 는 CPU 안에 들어 있는 변환 하드웨어여서, 커널이 구성해 둔 페이지 테이블을 읽어 가상 주소를 물리 주소로 바꾼다. 만드는 쪽과 쓰는 쪽이 이렇게 갈린다. ```text label="Page Table 을 구성하는 쪽과 사용하는 쪽" Guest Linux Kernel │ │ Page Table 구성/관리 ▼ Page Table ▲ │ 사용 │ MMU │ │ 주소 변환 ▼ Memory Access ``` ## TLB 가 페이지 테이블 탐색을 건너뛴다 메모리에 접근할 때마다 페이지 테이블 전체를 훑으면 비용이 크다. 그래서 CPU 는 최근 변환 결과를 TLB(Translation Lookaside Buffer, 주소 변환 캐시)에 담아 둔다. 가상 페이지 1 이 물리 프레임 7 로 이어진다는 변환이 TLB 에 있으면 같은 페이지의 다음 접근에서는 그 탐색을 건너뛴다. ```text label="TLB HIT 과 MISS" Virtual Address ↓ TLB ┌──┴──┐ │ │ HIT MISS │ │ │ ▼ │ Page Table Walk │ │ └──┬──┘ ▼ Physical Address ``` TLB 에 없으면 TLB Miss 가 나고, 페이지 테이블을 조회해 정상 매핑을 찾은 뒤 실행을 이어 간다. 접근 자체를 완료할 수 없는 Page Fault 와는 다른 사건이며, 그 구분과 계층별 처리는 fault 를 다루는 글이 이어 받는다. ## 베어메탈은 변환이 한 번, 가상 머신은 두 번 베어메탈 Linux 에서는 프로세스의 가상 주소가 페이지 테이블을 지나 호스트 물리 주소가 되고 거기서 끝난다. 가상 머신에서는 게스트가 얻은 물리 주소가 실제 호스트 물리 주소가 아니어서 한 단계가 더 붙는다. 이 `GPA → HPA` 두 번째 변환을 위해 Intel 에서는 EPT(Extended Page Tables)를 쓴다. EPT 는 Intel 의 second-level address translation 기술이고, AMD 에는 대응되는 NPT(Nested Page Tables) 계열 기능이 있다. | 무엇이 | 무엇을 무엇으로 바꾸나 | 주요 관리 주체 | |---|---|---| | 게스트 페이지 테이블 | GVA → GPA | 게스트 OS | | EPT | GPA → HPA | KVM/호스트 가상화 계층 | | 실행 시점의 실제 변환 | 두 변환 계층 활용 | CPU MMU | 게스트 페이지 테이블과 EPT 는 같은 테이블이 아니다. 하나는 게스트 OS 가 자기 프로세스를 위해 관리하고, 다른 하나는 가상화 계층이 게스트 전체의 물리 주소 공간을 호스트 RAM 에 이으려고 관리한다. 두 번째 변환이 왜 필요한지는 가상 머신 두 대를 놓고 보면 드러난다. 원문이 든 예에서는 각각 8 GiB RAM 을 가진 VM1 과 VM2 가 둘 다 GPA `0x1000` 을 쓸 수 있고, EPT 가 그 둘을 서로 다른 HPA 로 잇는다. 그래서 게스트가 보는 물리 메모리 주소 공간을 실제 호스트 RAM 에서 격리해 구현할 수 있다. 하드웨어가 두 번째 변환을 맡아 주지 않던 시절에는 방식이 달랐다. 하이퍼바이저가 게스트 페이지 테이블 변경을 추적하면서, GVA 에서 실제 호스트 메모리까지 이어지는 shadow mapping 을 관리하는 방식이 쓰일 수 있었다. 게스트 페이지 테이블이 바뀔 때마다 하이퍼바이저가 관련 매핑을 유지해야 하므로 관리 비용과 복잡성이 커질 수 있다. EPT 와 NPT 는 그 두 단계 변환을 CPU 가 하드웨어로 지원하게 한다. ## QEMU 가 게스트 RAM 을 마련하고 KVM 이 그 영역을 등록한다 가상 머신에 8 GiB RAM 을 설정했다고 하자. QEMU 는 호스트의 사용자 공간 프로세스이므로 QEMU 자신도 호스트 가상 주소 공간을 갖고, 게스트 RAM 을 담을 메모리도 그 안에 마련한다. QEMU 가 직접 "물리 주소 X부터 8 GiB를 달라"고 RAM 하드웨어를 제어하는 것이 아니다. QEMU 의 메모리도 일반 호스트 프로세스의 메모리와 같은 길을 지난다. ```text label="QEMU 가 마련한 Guest RAM backing 이 Host 물리 메모리에 닿는 길" QEMU Host Virtual Address ↓ Host Page Table ↓ Host Physical Address ``` 그다음 QEMU 는 자신이 마련한 호스트 사용자 공간 메모리 영역이 게스트의 어느 GPA 범위에 대응하는지를 KVM 에 등록한다. 대표 ioctl 이 `KVM_SET_USER_MEMORY_REGION` 이고, 개념적으로 게스트 GPA 범위와 QEMU 호스트 가상 주소 범위가 어떻게 대응하는지를 전달한다. 등록이 끝나면 세 계층이 하는 일이 갈린다. ```text label="Guest 메모리를 두고 QEMU · KVM · CPU 가 하는 일" QEMU → Guest RAM을 위한 Host userspace backing 제공 KVM → Guest memory region 및 virtualization mapping 관리 CPU → 실제 runtime address translation 수행 ``` QEMU 가 메모리 접근마다 EPT 를 소프트웨어로 검색하지는 않는다. 등록은 관리 동작이고 변환은 하드웨어 동작이다. ## 설정한 메모리와 호스트가 지금 붙잡은 메모리는 다르다 가상 머신에 16 GiB 를 설정했다고 해서, 어떤 일반 구성에서든 시작하는 순간 실제 호스트 RAM 16 GiB 가 반드시 전부 곧바로 물리적으로 점유되는 것은 아니다. 값 셋을 따로 세어야 한다. ```text label="같다고 볼 수 없는 세 값" Configured Memory ≠ Guest가 현재 실제 사용하는 Memory ≠ Host에서 현재 resident한 Physical Memory ``` 물리 메모리가 실제로 붙는 시점과 방식은 호스트 쪽 구성에 따라 달라질 수 있다. 호스트의 demand paging, 어떤 메모리로 뒷받침하는지, HugeTLB, memory locking, preallocation, overcommit 정책 등이 거기에 걸린다. 그래서 "VM RAM 16GiB = Host RAM에서 고정된 연속 16GiB"라고 단순화하면 안 된다. 여기 쓴 16 GiB 도 원문의 예시 값이고 이 서버의 설정이 아니다. 이 호스트에서 그 셋을 견주지도 않았다. 세 값은 같은 시점에 나란히 읽어야 갈리는데, 그 관찰을 아직 돌리지 않았다. ## 게스트 페이지 테이블 자체도 게스트 메모리에 있다 두 단계 변환에 비용이 붙는 까닭이 여기 있다. 게스트 페이지 테이블은 게스트 물리 메모리에 저장된 자료구조이므로, CPU 가 게스트 페이지 테이블 엔트리를 읽는 과정에서도 그 엔트리가 저장된 GPA 를 실제 HPA 로 바꿔야 한다. ```text label="entry 를 읽는 데에도 EPT 변환이 든다" GVA ↓ Guest Page Table Walk │ │ Page Table 자체가 Guest Memory에 존재 └────→ EPT를 이용해 실제 RAM에서 entry를 읽음 ↓ GPA 획득 ↓ EPT ↓ HPA ``` 그래서 두 단계에 걸친 페이지 테이블 탐색에는 비용이 들고 TLB 가 그만큼 중요해진다. Huge Page 가 TLB 로 덮는 범위와 페이지 테이블 탐색 부담을 함께 건드리는 까닭도 여기에서 온다. ## 정상 메모리 접근은 매번 VM Exit 하지 않는다 원문은 이 대목을 「잘못된 이해」라고 이름 붙인 그림으로 먼저 연다. ```text label="원문이 먼저 놓은 「잘못된 이해」" 잘못된 이해 Guest Memory Access ↓ VM Exit ↓ KVM ↓ RAM ``` 게스트의 메모리 접근이 곧장 VM Exit 을 거쳐 KVM 으로 가고 거기서 RAM 에 닿는 경로다. 제1부에서 VM Exit 을 읽고 온 사람이 메모리 접근에 그대로 옮겨 붙일 그림이라, 원문은 맞는 경로를 그리기 전에 이것부터 아니라고 적었다. 정상 매핑이 있으면 CPU 하드웨어가 직접 변환을 수행한다. ```text label="정상 mapping 이 있을 때의 실행 경로" Guest instruction ↓ CPU MMU / TLB ↓ Guest Page Table + EPT ↓ HPA ↓ Physical RAM ``` 따라서 정상적인 게스트 RAM 접근마다 QEMU/KVM 의 사용자 공간·커널 경로를 왕복하지는 않는다. 하이퍼바이저가 끼어드는 때는 지금 조건으로 접근을 완료할 수 없을 때이고, 두 번째 변환 단계에서 그렇게 되면 EPT Violation 으로 VM Exit 이 일어난다. ## 실행 경로와 관리 경로를 나누어 본다 실행 경로는 CPU 하드웨어가 접근할 때마다 지나는 길이고, 관리 경로는 가상 머신을 만들고 바꿀 때 지나는 길이다. 둘을 한 그림에 두면 `virsh` 명령 하나가 메모리 접근 경로에 끼어 있는 것처럼 읽힌다. ```text label="Memory Virtualization 관리 경로" User ↓ virsh ↓ libvirt ↓ QEMU │ ├─ Guest RAM backing ├─ QEMU HVA ├─ virtio-balloon device │ └─ ioctl(KVM_SET_USER_MEMORY_REGION) ↓ KVM │ ├─ Guest memory slots/regions 관리 └─ EPT 관련 virtualization mapping 관리 ↓ CPU ``` `virsh` 와 libvirt 는 가상 머신 구성과 관리를 맡고, QEMU 는 게스트 RAM 을 담을 호스트 사용자 공간 메모리와 장치 구성을 마련하며, KVM 은 게스트 메모리 영역을 하드웨어 가상화 기능에 잇는다. 실행 시점의 변환은 CPU 의 MMU 와 EPT 하드웨어가 한다. 실행 경로 쪽은 게스트 애플리케이션에서 시작해 TLB 와 게스트 페이지 테이블을 지나 GPA 가 된다. 그다음 VM Boundary 를 넘어 EPT 를 지나 HPA 가 되고, 호스트 물리 페이지와 그 페이지가 속한 NUMA(Non-Uniform Memory Access) 노드로 이어진다. ![Guest 구역 안의 Keycloak 과 PostgreSQL 에서 나온 GVA 가 Guest TLB 를 지나고, TLB Miss 이면 Guest Page Table 을 조회해 GPA 가 되며, 그 GPA 가 VM Boundary 를 넘어 EPT 에서 HPA 로 바뀐 뒤 Host RAM 구역의 Host Physical Memory 에 닿고 마지막에 NUMA Node 0 과 NUMA Node 1 로 갈라지는 주소 변환 경로도. 상자 안의 작은 글씨는 그 단계가 어느 계층인지와 무엇을 무엇으로 바꾸는지를 적은 것이다.](../../../final/assets/diagrams/guest-memory-address-translation-path/guest-memory-address-translation-path.svg) 위쪽 점선 상자 안에서 끝나는 것이 첫 번째 변환이고, 그 상자를 나온 뒤의 EPT 가 두 번째 변환을 맡는다. 관리 경로는 이 그림에 없다 — 위의 `virsh` 목록이 그쪽을 그린 것이다. ## 이 글이 확정하지 않는 것 이 호스트에서 잰 값은 하나도 없다. 원문 제2부는 개념 서술이고, 실제 서버에 딸린 설정과 동작은 OQ(Open Question) 번호가 붙은 확인 목록 열넷으로 미뤄져 있다. 그 목록을 아직 돌리지 않았으므로 이 글은 구조가 어떻게 동작하는지까지만 말한다. 설정한 메모리와 실제로 붙잡힌 메모리의 차이는 OQ-2 와 OQ-3 이 받는다. 가상 머신에 설정된 메모리와 지금 쓰는 메모리를 읽고, 같은 시점에 QEMU 프로세스가 호스트에서 상주하는 메모리를 읽어 견주는 확인이다. ```bash label="OQ-2 · OQ-3 이 이 호스트에서 읽을 값" virsh dominfo virsh dommemstat ps -o pid,rss,vsz,cmd -p ``` 원문이 커널·QEMU·libvirt 버전을 한 번도 적지 않아 이 글도 특정 버전에 고정하지 않았다. 여기 적은 것은 그 문서가 서술한 구성이고, 버전이 달라져 서술이 낡았는지는 위 확인을 실제 호스트에서 돌릴 때 드러난다. 범위 밖으로 미룬 것도 있다. 호스트 메모리 압박과 회수, 스왑, ballooning, OOM(Out Of Memory), NUMA 배치는 이 경로 위에서 벌어지는 자원 압박 쪽이라 각각 다른 글이 다룬다. Huge Page 도 마찬가지로 이 경로의 두 변환 단계에 걸리는 선택이어서 따로 뺐다.