virt-report虚拟化社区动态

KVM 内存映射与 EPT 缺页处理

本文由 OpenAI Codex (GPT-6-Astra) 辅助编写。

一、客户机访存与宿主内存映射

客户机页表记录客户机虚拟地址到客户机物理地址的映射,宿主页的位置则由宿主内存管理决定。宿主可能按需分配、回收或迁移这些页。第二级地址转换在保留客户机地址空间的同时,将访问映射到当前宿主页;后备页变化时,KVM 需要撤销旧映射并重新建立转换。

Intel EPT 为此增加 GPA → HPA 转换:QEMU 登记 RAM 后备范围,KVM 按需建立并维护 EPT,处理器利用这些页表和 TLB 执行访存。本文跟踪一次 GVA 0x40203080 的读取,依次走过“登记范围 → 缺页建图 → 重试访问 → 后备页变更后重建”。

源码路径采用 x86、Intel EPT 和 KVM TDP MMU。后备内存使用 QEMU 可访问的匿名内存或文件映射,示例以 4 KiB 页展开;私有内存与嵌套虚拟化另有处理路径。

这条路径通常由同一个 vCPU 线程完成:线程运行客户机指令,EPT 退出后在宿主内核中解析内存槽、取得宿主页并安装映射,随后重新进入客户机。QEMU 负责事先登记 RAM 范围;普通 RAM 建图成功时,无需让用户态设备回调参与。

源码版本:QEMU 10.2.0、Linux 6.18。

二、客户机页表与 EPT 的两级转换

客户机页表把客户机虚拟地址 GVA 转成客户机物理地址 GPA,EPT 再把 GPA 转成宿主物理地址 HPA。客户机页表页本身也放在客户机物理内存中,因此硬件读取这些页表项时,同样需要 EPT 转换。TLB 与页表遍历缓存减少了重复遍历,不能把这条路径理解为每次访问都由软件查询两张表。

QEMU 的宿主虚拟地址 HVA 用于定位 RAM 后备内存。KVM 根据 HVA 查询宿主内存管理,取得当前物理页,再建立 EPT。已建立映射后的客户机访存直接使用硬件转换;HVA 不出现在 EPT 页表项中,也不会作为客户机的第三层硬件地址转换。KVM MMU 的地址空间说明

客户机访存与 KVM 建图路径
实线表示客户机的硬件地址转换;虚线表示 KVM 根据 memslot 和宿主映射找到后备页,再填写 EPT。HVA 用于软件建图,不属于客户机硬件遍历的地址层级。
实线表示客户机的硬件地址转换;虚线表示 KVM 根据 memslot 和宿主映射找到后备页,再填写 EPT。HVA 用于软件建图,不属于客户机硬件遍历的地址层级。 打开原图
Mermaid 源码
flowchart TB
    accTitle: EPT 硬件地址转换与 KVM 软件建图
    accDescr: 客户机 GVA 经客户机页表转换为 GPA,再经 EPT 转成 HPA。KVM 按 GPA 查 memslot 得到 QEMU HVA,查询宿主内存得到 PFN,配置 EPT;HVA 不属于硬件客户机访存路径。
    subgraph hardware[客户机访存:处理器执行]
        direction LR
        gva["GVA<br/>0x0000000040203080"] -->|客户机页表| gpa["GPA<br/>0x00403080"]
        gpa -->|EPT| hpa["HPA<br/>0x21000080"]
    end
    subgraph software[KVM 建图:缺页时的软件路径]
        direction LR
        slot["memslot 0<br/>base_gfn = 0x400<br/>npages = 512"]
        hva["QEMU 后备页 HVA<br/>0x7f0000003000"]
        pfn["宿主物理页<br/>PFN = 0x21000"]
        spte["EPT 叶子 SPTE<br/>页基址 0x21000000<br/>读 / 写 / 执行权限"]
        slot -.->|槽内第 3 页| hva
        hva -.->|宿主取页| pfn
        pfn -.->|安装页表项| spte
    end
    hardware -.->|缺页时按 GPA 查槽:GFN = 0x403| software
一次读取经过的阶段处理组件处理结果
准备 RAM 地址范围QEMU 注册 memslotKVM 知道 GPA 范围对应哪个 HVA 范围
目标 EPT 映射尚缺处理器退出,KVM 解析后备页以 HVA 取得当前 PFN,建立 GPA → HPA 叶子项
重试原读取处理器完成两级转换读取目标宿主页;已有缓存可减少后续遍历
宿主替换后备页宿主内存管理通知 KVM撤销旧项与相关转换缓存,后续访问重新建图

EPT 的读、写、执行权限与客户机页表权限共同约束访问。客户机允许写入,仍可能被 EPT 写保护拦截;EPT 允许写入,也不能绕过客户机页表的只读限制。设备 DMA 使用的 IOMMU 映射是另一条路径,见VFIO 的 DMA 地址转换与隔离。

三、首次读取:从 GPA 找到 PFN,再安装 EPT

先准备这个例子使用的 RAM。QEMU 建立后备内存,将它作为 MemoryRegion 接入客户机地址空间;生效的 RAM 范围通过 MemoryListener 通知 KVM 加速器。QEMU 10.2 的 kvm_region_add() 先保存更新,在 kvm_region_commit() 中调用 kvm_set_phys_mem(),构造 KVMSlot 后再向内核登记。这样,QEMU 地址空间中的一段 RAM 才与 KVM 能查找的内存槽衔接起来。通知与提交、MemoryRegion 与地址空间视图

设登记范围从 GPA 0x00400000 开始,长度为 2 MiB,对应 HVA 0x7f0000000000。kvm_set_user_memory_region() 的下列连续赋值将 QEMU 保存的地址关系填入 ioctl 参数:

mem.slot = slot->slot | (kml->as_id << 16);
mem.guest_phys_addr = slot->start_addr;
mem.userspace_addr = (unsigned long)slot->ram;
mem.flags = slot->flags;

函数随后填入长度并调用内存注册 ioctl。内核由此知道“这个 GPA 范围由这一段用户态内存承载”;后备物理页和 EPT 叶子仍可按需建立。下面假定注册成功、客户机已开始执行,但目标数据页还没有 EPT 项。地址登记与 ioctl

1. EPT 映射缺失与内核缺页处理

假设客户机页表已把 GVA 0x0000000040203080 映射到 GPA 0x00403080,客户机页表相关的 EPT 映射也已存在,只有目标数据页尚未建立 EPT 叶子项。处理器因第二级转换无法完成而退出,VMX 后端读取 GPA 和访问类型,进入 KVM 缺页处理。

2. 把地址计算完整走一遍

页大小                  = 0x1000 B = 4 KiB
访问 GPA                 = 0x00403080
GFN                      = 0x403
页内偏移                 = 0x080 B

memslot.base_gfn          = 0x400
memslot.npages            = 0x200 = 512 页
槽内页号                 = 0x403 - 0x400 = 3
后备页 HVA               = 0x7f0000000000 + 3 × 0x1000
                         = 0x7f0000003000

宿主内存管理返回 PFN     = 0x21000
EPT 叶子映射             = GPA 页 0x00403000 → HPA 页 0x21000000
本次访问的 HPA           = 0x21000000 + 0x080
                         = 0x21000080

GFN 是 GPA 除以页大小得到的客户机页号,PFN 是宿主物理页号。KVM 先用槽内偏移找到 HVA,再让宿主内存管理提供当前 PFN;负责这两步的入口分别是 __gfn_to_hva_memslot() 与 __kvm_faultin_pfn()。后备页未驻留时还需分配、调入或等待 I/O。这里取得的是临时页引用,不能推导为整台 VM 的 RAM 永久固定在同一物理地址。GFN → HVA、宿主取页

3. 提交映射后,处理器重试同一条读取

在 TDP MMU 慢路径中,KVM 取得 PFN 后获取 mmu_lock 读锁,确认本次结果没有失效,再逐级创建缺少的页表页,以原子操作安装叶子 SPTE(KVM 管理的页表项,这里对应 EPT 项)。若其他 vCPU 已建立映射或修改同一项,会检查结果或重试;成功后重新进入客户机,由处理器重试原来的访存。通常无需返回 QEMU,也不需要修改客户机页表。缺页入口、叶子项安装

本例最终读取 HPA 0x21000080。后续相同访问若命中有效 TLB / EPT 映射,就沿硬件路径完成;QEMU 不会再次逐项计算 HVA,KVM 也无需每次重新取得 PFN。按需建图把工作集中到首次访问和映射变化时。

四、映射结构:内存槽、缺页状态与 EPT 表项

1. 给示例 GPA 登记一段后备内存

将前面登记的 2 MiB 范围代入 ioctl 参数,可以得到下表。后续查找 GPA 0x00403080 时,KVM 使用的槽起点、页数与后备基址都来自这次登记。

用户态字段宽度与作用本例
slot32 位;低 16 位是槽 ID,高 16 位用于支持多地址空间的配置0
flags32 位;脏页记录、只读等属性0,普通可写 RAM
guest_phys_addr64 位;GPA 起始地址0x00400000
memory_size64 位;字节数,设为 0 表示删除槽0x00200000,2 MiB
userspace_addr64 位;QEMU 进程中的 HVA 起点0x7f0000000000

这些字段同时存在于 kvm_userspace_memory_region 和扩展的 kvm_userspace_memory_region2。后者另有 guest_memfd、guest_memfd_offset 等字段;其使用受相应标志与能力控制,普通 HVA 后备 RAM 不需要借助 guest_memfd 定位。QEMU 可以根据内核能力选择 REGION2 ioctl,这本身不表示虚拟机使用私有内存。ABI 定义

2. memslot 范围与逐页映射的衔接

内核对象或值关键内容参与的操作
kvm_memory_slotbase_gfn、npages、userspace_addr、flags、id按客户机页号查找 RAM 范围,计算 HVA,关联脏页记录
kvm_page_faultgfn、slot、pfn、write、map_writable、mmu_seq保存本次访存类型、后备页和失效序号,贯穿缺页处理
PFN宿主物理页号;4 KiB 页下,页基址为 PFN 左移 12 位由宿主 HVA 映射解析得到,成为叶子 EPT 项的物理地址部分
SPTEKVM 管理的页表项;本路径中对应 EPT 项,包含物理地址、权限、页级别及状态位非叶子项指向下级页表;叶子项把 GPA 页映射到宿主页

memslot 描述一段地址范围,SPTE 描述某一级硬件映射。一个槽可以对应很多 EPT 叶子项;相邻 HVA 连续也不保证宿主物理页连续。只读槽、宿主页是否可写和 KVM 为记录脏页设置的写保护,还会影响最终的写权限。

3. EPT 页表的实际布局

一个 4 KiB 页容纳 512 个 64 位表项,每一级从 GPA 中取 9 位选择下一项。以四级 EPT、4 KiB 叶子为例,依次使用 GPA 的位 47–39、38–30、29–21、20–12,最后 12 位是页内偏移。非叶子项保存下级页表的宿主物理地址,叶子项保存目标页的宿主物理地址;大页可在更高层终止遍历。页表项与层级

GPA 0x00403080,四级 EPT、4 KiB 叶子
  根表[0] → 下一级[0] → 下一级[2] → 叶子表[3]
                                               ↓
                                HPA 页基址 + 页内偏移 0x080

非叶子 EPT 项:下级页表 HPA + 访问权限等属性
叶子 EPT 项:  RAM 页 HPA    + 访问权限、内存类型等属性

五、访问分支:RAM 缺页、写保护与设备地址

同一次读取可能涉及不同层的故障:客户机内核处理自身页表的 #PF,KVM 处理 EPT 缺映射或权限问题,宿主 Linux 内存管理则在后备页尚未驻留时分配或调入页面。观察到 EPT violation 后,还需要结合 GPA 与槽属性选择下面的分支。

场景关键区别处理方向
客户机 #PF客户机自己的页表缺失或权限不允许普通 EPT 模式下由客户机异常机制处理;客户机内核决定建图、调页或报告非法访问
RAM 的 EPT 缺页GPA 在有效 RAM 槽内,但第二级映射缺失KVM 解析后备页、建立 EPT,再执行原访存
RAM 写保护已有映射,但当前 EPT 项不可写区分脏页跟踪、页跟踪及真实只读属性,按原因记录、恢复权限或进入模拟
GPA 未落入 RAM 槽可能是设备地址,也可能是未映射空洞走无槽及 MMIO 模拟路径,由内核设备或 QEMU 地址空间继续处理;不会自动分配一页 RAM
宿主后备页无法取得RAM 槽存在,但宿主内存解析出错根据错误类型重试、处理信号或报错,不能一律转换成普通 MMIO

例如迁移开启写保护式脏页跟踪后,客户机对已存在 RAM 页的首次写入会触发 EPT 处理,KVM 记录页被修改,再允许后续写入。KVM_MEM_READONLY 则声明槽的只读属性:x86 对它的写请求可交给用户态 MMIO 处理,不应像脏页记录那样随意恢复可写。脏页记录也可以结合硬件辅助机制,不能把所有记录模式都归结为逐页写退出。只读槽约定

无槽访问还可能缓存成特殊 MMIO SPTE,后续通过 EPT misconfiguration 等路径识别;“看到 EPT 退出”不足以判断是否需要 QEMU。对应实现见 kvm_handle_noslot_fault() 与 handle_ept_misconfig()。用户态往返见vCPU 退出处理,迁移脏页交接见Dirty Ring。

六、后备页变化:撤销旧映射,防止旧 PFN 回填

QEMU 的 HVA 可以保持不变,背后的宿主物理页却因回收、页迁移或映射变更而改变。如果 KVM 继续使用旧 SPTE,客户机就可能访问错误的物理页。清理 CPU 缓存的转换结果还不够:已有页表项必须失效,正在处理的缺页也不能把旧 PFN 重新填回去。

宿主内存管理通过 MMU notifier 通知 KVM 某个 HVA 范围即将失效。KVM 找到相交 memslot,把范围转换成 GFN,记录失效进行中,撤销对应叶子映射,并完成所需的远端 TLB 失效。失效结束时推进 mmu_invalidate_seq。范围失效与序号更新

缺页建图与宿主映射失效的竞争
示例中,缺页线程取得旧 PFN 后遇到宿主页替换。安装前的锁内检查发现失效序号变化,放弃旧结果并重新取页,阻止旧 SPTE 被回填。
示例中,缺页线程取得旧 PFN 后遇到宿主页替换。安装前的锁内检查发现失效序号变化,放弃旧结果并重新取页,阻止旧 SPTE 被回填。 打开原图
Mermaid 源码
sequenceDiagram
    accTitle: EPT 缺页与宿主物理页替换的并发处理
    accDescr: 缺页线程保存序号并取得旧 PFN。宿主内存管理通过 MMU notifier 撤销旧 EPT 项并推进序号。缺页线程持 MMU 读锁检查发现结果过期,释放旧页引用后重新查询,最终安装新 PFN。
    participant V as vCPU / KVM 缺页线程
    participant M as 宿主内存管理
    participant E as KVM EPT 与失效状态
    V->>E: 保存 mmu_seq = 7
    V->>M: HVA 0x7f0000003000 取页
    M-->>V: 旧 PFN = 0x21000
    M->>E: invalidate_range_start(HVA 范围)
    E->>E: MMU 写锁:记录失效范围,撤销 SPTE
    E->>E: 完成所需的远端 TLB 失效
    M->>M: 替换宿主后备页
    M->>E: invalidate_range_end
    E->>E: mmu_invalidate_seq = 8,结束失效
    V->>E: MMU 读锁:检查范围、序号与根
    E-->>V: 7 ≠ 8,结果过期
    V->>V: 释放旧页引用,重试
    V->>E: 保存 mmu_seq = 8
    V->>M: 再次查询相同 HVA
    M-->>V: 新 PFN = 0x31000
    V->>E: MMU 读锁内复核通过,原子安装新 SPTE
    V->>V: 重新进入客户机,重试原访存

缺页线程先保存 fault.mmu_seq,再解析 PFN;锁外预检查尽早避开正在失效的范围,锁内 is_page_fault_stale() 最后检查根是否过期、目标 GFN 是否正在失效,以及序号是否变化。发现冲突就释放本次取页结果并重试。锁外检查只用于减少争用,不能替代最后的锁内判断。检查顺序、序号与范围判定

memslot 本身的移动、删除也需要使旧映射失效;槽的生命周期通过专门的更新同步保护。由此,HVA → PFN 查询、EPT 页表修改与宿主内存生命周期形成同一套一致性约束,才能支持按需分配、回收与迁移。

七、源码调用链:解释访问、解析后备、提交映射

从 VMX 退出入口向下追踪时,可以把路径分为“解释访问、解析后备、提交映射”三段。它们分别回答客户机要做什么、宿主能提供哪一页,以及当前能否安全地把这一页交给硬件。

阶段源码入口与操作
读取硬件信息handle_ept_violation() 读取 GPA;__vmx_handle_ept_violation() 将读、写、取指和页表遍历信息转成 KVM 使用的标志。
选择处理分支kvm_mmu_page_fault() 区分 MMIO 缓存、正常缺页与写保护;需要模拟时进入指令模拟器。
取得后备页kvm_mmu_faultin_pfn() 保存失效序号,检查槽与地址属性,通过宿主内存管理取得 PFN 及实际可写性。
验证并提交kvm_tdp_mmu_page_fault() 完成锁内过期检查,调用建图函数,最后释放本次取页持有的引用。
安装页表层级kvm_tdp_mmu_map() 调整目标页级别,遍历并补充中间页表,再安装叶子项。
组合权限与地址make_spte() 将 PFN、宿主可写性、访问权限与跟踪策略组合成页表项,必要时记录脏页。

取页与提交之间还有一次有效性检查。以下摘录来自 kvm_tdp_mmu_page_fault() 的慢路径,保留了取页、加锁、检查、建图和释放的连续代码;前面的快速处理分支已省略:

r = kvm_mmu_faultin_pfn(vcpu, fault, ACC_ALL);
if (r != RET_PF_CONTINUE)
    return r;

r = RET_PF_RETRY;
read_lock(&vcpu->kvm->mmu_lock);

if (is_page_fault_stale(vcpu, fault))
    goto out_unlock;

r = kvm_tdp_mmu_map(vcpu, fault);

out_unlock:
kvm_mmu_finish_page_fault(vcpu, fault, r);
read_unlock(&vcpu->kvm->mmu_lock);
return r;

对本例,第一步把 fault.pfn 填为 0x21000,建图函数据此安装目标叶子项;如果宿主在两步之间替换了后备页,检查返回过期,结果保持 RET_PF_RETRY,旧 PFN 不会进入新映射。kvm_mmu_finish_page_fault() 处理本次持有的后备页引用。这段顺序把前面的成功访问与后备页变更场景接在一起。完整慢路径片段

快速缺页修复会尝试直接调整已有项,首次建图通常进入上述慢路径。返回 RET_PF_FIXED 表示映射已修复,RET_PF_RETRY 表示需要重试,RET_PF_EMULATE 才转向指令模拟;这些内核结果与返回用户态的 KVM_EXIT_MMIO 属于不同层次。

八、演进与取舍:影子页表、EPT 与并行 TDP MMU

1. 硬件分离第二级转换,减少客户机页表跟踪

早期影子页表把客户机页表和宿主映射合成为 GVA → HPA 的页表供硬件使用。客户机修改页表、切换地址空间时,KVM 需要维护合成结果,涉及页表写跟踪、同步和反向映射。EPT 把 GPA → HPA 单独交给硬件,客户机可以直接维护自己的 GVA → GPA 页表,减少软件跟踪客户机页表变化的工作。

2008 年,EPT 支持进入 KVM,随 Linux 2.6.26 发布。第二级页表也带来额外的遍历成本:客户机页表访问本身仍需经过 EPT,缓存未命中时尤其明显。TLB、页表遍历缓存以及满足对齐和后备条件的大页可以减少开销;大页又会与细粒度写保护、脏页记录和回收发生权衡。

2. 并行建图降低多 vCPU 首次访问的锁争用

2020 年,Linux 5.10 纳入新的 TDP MMU,包含独立的缺页建图路径,初期默认关闭。这是 KVM 管理 EPT / NPT 页表的软件实现演进:利用 GPA 索引的树结构简化管理,减少传统实现中维护反向映射等通用逻辑的负担。硬件 EPT 与这个软件实现不在同一时间引入。

2021 年,Linux 5.12 纳入并行缺页处理,让 TDP 缺页路径以读模式持有 MMU 锁。大量 vCPU 同时首次访问内存时,多个缺页处理可以并行建图,降低集中锁争用;对同一页表项的竞争仍需检测与重试。

当前代码中,RCU 保护页表遍历期间的对象生命周期,原子更新处理多个线程对同一 SPTE 的竞争;失效等操作仍需要相应同步。并行化也增加了重试、延迟释放及根生命周期管理的复杂度。页表遍历与并发安装

3. 访存成本的分段测量

性能现象与发生阶段排查方向收益与代价
首次读取等待宿主页分配或调入后备内存预分配、内存压力、NUMA 布局预分配可减少运行中等待,同时提前消耗资源;它不保证 EPT 全部预建
稳定运行仍有较多转换开销TLB 覆盖、客户机和宿主后备的大页条件大页减少表项及遍历,但降低写保护、回收等操作的粒度
迁移时首次写入频繁退出脏页跟踪方式、硬件辅助能力和写入分布优化跟踪机制与批处理,不能直接取消迁移所需的正确记录
宿主页回收后反复重建宿主内存压力、页面迁移、失效与缺页重试缓解后备页抖动;失效同步本身是内存安全的必要条件

评估时分别观察启动或首次触页耗时、稳定业务延迟、KVM MMU 退出与宿主缺页 / 内存压力,再比较同一负载的变化。客户机 #PF、EPT 缺页和宿主取页等待来源不同;只看一个“缺页数”,无法知道应该改客户机内存管理、KVM 建图还是宿主分配策略。

大页条件与迁移期间的粒度变化见KVM 大页映射的建立、拆分与恢复,其中逐步展示 2 MiB 叶子与 512 个 4 KiB 叶子的转换。

九、参考资料