Files
document-haness/docs/virtualization/tech-log-studio/memory-virtualization/concept/concept-guest-memory-address-translation.md
T

20 KiB

id, kind, slug, title, topic, topicName, project, status, basisVersion, studio, assets, sourceRevision, source
id kind slug title topic topicName project status basisVersion studio assets sourceRevision source
2f2ffafc-91ff-4971-a6a8-7c0e7d844ca2 CONCEPT guest-memory-address-translation Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA memory-virtualization 메모리 가상화 virtualization 초안 x86-64 Intel EPT · AMD 는 NPT 계열로 대응 · Linux KVM 의 KVM_SET_USER_MEMORY_REGION · QEMU/libvirt · 기본 page 4 KiB https://hyeonworks.com/studio/documents/2f2ffafc-91ff-4971-a6a8-7c0e7d844ca2/edit
key file
guest-memory-address-translation-path ../../../final/assets/diagrams/guest-memory-address-translation-path/guest-memory-address-translation-path.svg
no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
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 가상 머신에서 게스트 애플리케이션이 메모리를 한 번 읽는 길을 가장 단순하게 그리면 이렇다.

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 이 변수 하나를 읽는다고 하면, 그 한 번의 접근이 세 주소를 차례로 지난다.

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 이고, 물리 메모리도 같은 크기의 프레임 단위로 볼 수 있다. 그래서 가상 주소는 페이지 번호와 페이지 안의 오프셋으로 갈린다.

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 안에 들어 있는 변환 하드웨어여서, 커널이 구성해 둔 페이지 테이블을 읽어 가상 주소를 물리 주소로 바꾼다. 만드는 쪽과 쓰는 쪽이 이렇게 갈린다.

Guest Linux Kernel
       │
       │ Page Table 구성/관리
       ▼
   Page Table
       ▲
       │ 사용
       │
      MMU
       │
       │ 주소 변환
       ▼
Memory Access

TLB 가 페이지 테이블 탐색을 건너뛴다

메모리에 접근할 때마다 페이지 테이블 전체를 훑으면 비용이 크다. 그래서 CPU 는 최근 변환 결과를 TLB(Translation Lookaside Buffer, 주소 변환 캐시)에 담아 둔다. 가상 페이지 1 이 물리 프레임 7 로 이어진다는 변환이 TLB 에 있으면 같은 페이지의 다음 접근에서는 그 탐색을 건너뛴다.

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 의 메모리도 일반 호스트 프로세스의 메모리와 같은 길을 지난다.

QEMU Host Virtual Address
        ↓
Host Page Table
        ↓
Host Physical Address

그다음 QEMU 는 자신이 마련한 호스트 사용자 공간 메모리 영역이 게스트의 어느 GPA 범위에 대응하는지를 KVM 에 등록한다. 대표 ioctl 이 KVM_SET_USER_MEMORY_REGION 이고, 개념적으로 게스트 GPA 범위와 QEMU 호스트 가상 주소 범위가 어떻게 대응하는지를 전달한다. 등록이 끝나면 세 계층이 하는 일이 갈린다.

QEMU
→ Guest RAM을 위한 Host userspace backing 제공

KVM
→ Guest memory region 및 virtualization mapping 관리

CPU
→ 실제 runtime address translation 수행

QEMU 가 메모리 접근마다 EPT 를 소프트웨어로 검색하지는 않는다. 등록은 관리 동작이고 변환은 하드웨어 동작이다.

설정한 메모리와 호스트가 지금 붙잡은 메모리는 다르다

가상 머신에 16 GiB 를 설정했다고 해서, 어떤 일반 구성에서든 시작하는 순간 실제 호스트 RAM 16 GiB 가 반드시 전부 곧바로 물리적으로 점유되는 것은 아니다. 값 셋을 따로 세어야 한다.

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 로 바꿔야 한다.

GVA
 ↓
Guest Page Table Walk
 │
 │ Page Table 자체가 Guest Memory에 존재
 └────→ EPT를 이용해 실제 RAM에서 entry를 읽음
 ↓
GPA 획득
 ↓
EPT
 ↓
HPA

그래서 두 단계에 걸친 페이지 테이블 탐색에는 비용이 들고 TLB 가 그만큼 중요해진다. Huge Page 가 TLB 로 덮는 범위와 페이지 테이블 탐색 부담을 함께 건드리는 까닭도 여기에서 온다.

정상 메모리 접근은 매번 VM Exit 하지 않는다

원문은 이 대목을 「잘못된 이해」라고 이름 붙인 그림으로 먼저 연다.

잘못된 이해

Guest Memory Access
       ↓
VM Exit
       ↓
KVM
       ↓
RAM

게스트의 메모리 접근이 곧장 VM Exit 을 거쳐 KVM 으로 가고 거기서 RAM 에 닿는 경로다. 제1부에서 VM Exit 을 읽고 온 사람이 메모리 접근에 그대로 옮겨 붙일 그림이라, 원문은 맞는 경로를 그리기 전에 이것부터 아니라고 적었다. 정상 매핑이 있으면 CPU 하드웨어가 직접 변환을 수행한다.

Guest instruction
       ↓
CPU MMU / TLB
       ↓
Guest Page Table + EPT
       ↓
HPA
       ↓
Physical RAM

따라서 정상적인 게스트 RAM 접근마다 QEMU/KVM 의 사용자 공간·커널 경로를 왕복하지는 않는다. 하이퍼바이저가 끼어드는 때는 지금 조건으로 접근을 완료할 수 없을 때이고, 두 번째 변환 단계에서 그렇게 되면 EPT Violation 으로 VM Exit 이 일어난다.

실행 경로와 관리 경로를 나누어 본다

실행 경로는 CPU 하드웨어가 접근할 때마다 지나는 길이고, 관리 경로는 가상 머신을 만들고 바꿀 때 지나는 길이다. 둘을 한 그림에 두면 virsh 명령 하나가 메모리 접근 경로에 끼어 있는 것처럼 읽힌다.

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 로 갈라지는 주소 변환 경로도. 상자 안의 작은 글씨는 그 단계가 어느 계층인지와 무엇을 무엇으로 바꾸는지를 적은 것이다.

위쪽 점선 상자 안에서 끝나는 것이 첫 번째 변환이고, 그 상자를 나온 뒤의 EPT 가 두 번째 변환을 맡는다. 관리 경로는 이 그림에 없다 — 위의 virsh 목록이 그쪽을 그린 것이다.

이 글이 확정하지 않는 것

이 호스트에서 잰 값은 하나도 없다. 원문 제2부는 개념 서술이고, 실제 서버에 딸린 설정과 동작은 OQ(Open Question) 번호가 붙은 확인 목록 열넷으로 미뤄져 있다. 그 목록을 아직 돌리지 않았으므로 이 글은 구조가 어떻게 동작하는지까지만 말한다.

설정한 메모리와 실제로 붙잡힌 메모리의 차이는 OQ-2 와 OQ-3 이 받는다. 가상 머신에 설정된 메모리와 지금 쓰는 메모리를 읽고, 같은 시점에 QEMU 프로세스가 호스트에서 상주하는 메모리를 읽어 견주는 확인이다.

virsh dominfo <VM_NAME>
virsh dommemstat <VM_NAME>
ps -o pid,rss,vsz,cmd -p <QEMU_PID>

원문이 커널·QEMU·libvirt 버전을 한 번도 적지 않아 이 글도 특정 버전에 고정하지 않았다. 여기 적은 것은 그 문서가 서술한 구성이고, 버전이 달라져 서술이 낡았는지는 위 확인을 실제 호스트에서 돌릴 때 드러난다.

범위 밖으로 미룬 것도 있다. 호스트 메모리 압박과 회수, 스왑, ballooning, OOM(Out Of Memory), NUMA 배치는 이 경로 위에서 벌어지는 자원 압박 쪽이라 각각 다른 글이 다룬다. Huge Page 도 마찬가지로 이 경로의 두 변환 단계에 걸리는 선택이어서 따로 뺐다.