内存虚拟化:地址转换、影子页表与两级映射
一、客户机地址空间与宿主内存
内存虚拟化为每台客户机提供独立的物理地址空间,并将它映射到宿主的后备内存。两台虚拟机都可以从物理地址零开始使用内存,而不读写彼此的数据。客户机应用还有自己的虚拟地址,客户机内核通过页表把它们映射到自己认识的物理页;宿主再决定每个客户机物理页实际由哪块宿主内存承载。
内存虚拟化需要同时满足三个条件:客户机页表的含义保持不变,访问被限制在获准的宿主页内,宿主仍能分配、迁移和回收这些页面。本文以普通、可由宿主访问的 RAM 和 x86 KVM 为例,从一次读取推导影子页表与 EPT/NPT 的解决办法。
客户机内核维护自己的页表,QEMU 在宿主用户态准备 RAM 后备范围,KVM 在宿主内核维护第二级映射,处理器完成实际遍历。下文沿同一条 load 比较影子页表和 EPT,再跟踪宿主后备页变化时的映射重建。
源码版本:QEMU 10.2.0、Linux 6.18。
二、GVA、GPA、HVA 与 HPA 的对应关系
设客户机执行 load [0x400120],页大小为 4 KiB。客户机页表把虚拟页 0x400000 映射到客户机物理页 0x20000,偏移 0x120 保持不变,因此需要访问的客户机物理地址是 0x20120。
Mermaid 源码
flowchart TB
accTitle: 客户机地址转换与宿主映射管理
accDescr: 硬件访问从 GVA 经客户机页表到 GPA,再经 EPT/NPT 到 HPA。管理映射时 KVM 由内存槽定位 HVA,解析宿主页以建立二级页表,HVA 不在硬件的两级访问链上。
V["GVA 0x400120<br/>客户机应用地址"] -->|客户机页表| P["GPA 0x20120<br/>客户机物理地址"]
P -->|EPT / NPT| H["HPA 0x920120<br/>实际物理内存"]
P -. "缺少映射时:查内存槽" .-> U["HVA B + 0x20120<br/>QEMU RAM 后端地址"]
U -. "解析宿主页" .-> F["宿主页 0x920000<br/>页内偏移 0x120"]
F -. "建立二级映射" .-> H
| 名称 | 本例 | 作用 |
|---|---|---|
| GVA,客户机虚拟地址 | 0x400120 | 程序使用的地址,由客户机页表解释 |
| GPA,客户机物理地址 | 0x20120 | 客户机认为存在的 RAM 或设备地址 |
| HVA,宿主虚拟地址 | B + 0x20120 | QEMU 进程访问后备内存的位置 |
| HPA,宿主物理地址 | 0x920120 | 本次数据实际位于的宿主内存位置 |
QEMU 用内存槽向 KVM 登记一段 GPA 与 HVA 的对应关系。这里假定槽从 GPA 0 开始,映射到 QEMU 的基址 B;对应宿主页基址为 0x920000。KVM 建图时先按槽内偏移找到 HVA,再向 Linux 内存管理取得当前宿主页。另一个虚拟机的同一 GPA 可以落到不同 HPA。隔离依赖宿主控制这份映射和访问权限,客户机不能通过修改自己的页表取得任意宿主页。KVM 地址空间与后备内存
三、从软件合成映射到硬件两级转换
1. 影子页表把两次转换合成一次
传统页表硬件只负责虚拟地址到物理地址的一次转换。虚拟机监控器(VMM)可以读取客户机页表,再结合宿主的页面分配,把本例直接合成为 GVA 0x400000 → HPA 0x920000,形成影子页表供硬件使用。客户机仍维护自己看到的 GVA→GPA 关系,VMM 维护实际执行所用的合成结果。
现在客户机内核把同一虚拟页改为指向 GPA 0x30000,这页由 HPA 0xb60000 承载。客户机完成必要的 TLB 维护后,同一条 load 应读取 HPA 0xb60120。旧影子项仍指向 0x920000,因此 KVM 还要跟踪客户机页表变化,使依赖旧关系的影子项失效或更新,再维护转换缓存。影子页表可以延后或合并部分同步工作,相关状态仍需在重新使用时保持一致。影子页表同步与延后同步
2. EPT/NPT 分开客户机页表与宿主映射
Intel EPT(扩展页表)、AMD NPT(嵌套页表)增加第二级硬件转换。客户机页表仍把 GVA 变为 GPA,KVM 管理的第二级页表把 GPA 变为 HPA。在上面的改映射例子中,若 GPA 0x30000 的 EPT 项已经有效,客户机更新自己的页表并完成 TLB 维护后,硬件就会沿新 GPA 找到 HPA 0xb60000;KVM 无需再维护一份依赖 GVA 的合成结果。
Mermaid 源码
flowchart TB
accTitle: 影子页表与两级页表的分工
accDescr: 影子页表由软件合成客户机页表和宿主映射,供硬件从 GVA 转换到 HPA。EPT/NPT 则让硬件依次使用客户机页表和宿主二级页表,客户机页表页的读取也要经过第二级转换。
subgraph SHADOW["影子页表"]
G["客户机页表<br/>GVA → GPA"] -->|软件跟踪与合成| S["影子页表<br/>GVA → HPA"]
H["宿主后备关系<br/>GPA → HPA"] --> S
S --> A["硬件使用合成页表完成访问"]
end
subgraph TDP["EPT / NPT"]
V["GVA"] -->|客户机页表| P["GPA"]
P -->|宿主管理的二级页表| F["HPA"]
W["读取客户机页表页<br/>同样需要第二级转换"] -.-> P
end
classDef guest fill:#e4f1ec,stroke:#438575,color:#173e34;
classDef host fill:#eaf0f6,stroke:#8198ac,color:#263c4c;
classDef note fill:#fff4db,stroke:#b89a55,color:#59491e;
class G,V,P guest;
class H,S,A,F host;
class W note;
两种方案中,load 的结果都必须来自客户机当前指定的页。区别是维护工作的位置:影子模式需要追踪客户机映射与宿主后备两侧的变化;EPT/NPT 把客户机页表的常规维护留给客户机,KVM 主要维护第二级映射。宿主替换 HPA 时,两种方案仍须撤销旧映射,EPT 没有消除这一责任。
两级转换的新增成本来自硬件遍历:客户机页表本身也在 GPA 空间中,读取每一级表项都要经过 EPT/NPT;最后的数据访问同样需要第二级转换。TLB 保存最终转换,页表遍历缓存复用中间结果,因此一次 load 的实际访存开销取决于缓存命中情况。
这里有两类后续优化:大页让一个转换项覆盖更多地址,减少遍历与缓存压力;KVM 的并行页表管理则减少多个 vCPU 同时建图的锁争用。前者改变映射粒度,后者改变宿主的软件实现。它们都以地址和权限仍然正确为前提,具体条件放到大页与 EPT 专篇展开。
四、一次读取中的建图、访问与映射失效
1. 缺少 EPT 项时的映射建立
回到最初 GVA 0x400120 → GPA 0x20120 的映射。假设客户机页表有效,相关页表页已可访问,只有数据页 GPA 0x20000 的 EPT 项尚缺。硬件转换失败后退出,KVM 从内存槽找到 HVA B + 0x20000,取得宿主页 HPA 0x920000,检查权限并安装 EPT 项。客户机重试同一条 load,最终读取 0x920000 + 0x120。
取得宿主页时,Linux 可能还需分配或调入内存,这属于宿主缺页;客户机自己的页表无效则可能引发客户机 #PF,由客户机内核处理。普通 RAM 的 EPT 建图通常在 KVM 内完成,不必把每次访问交给 QEMU。
2. 宿主页变化与旧映射失效
后续访问可使用有效页表和 TLB。假设宿主将该页移到 HPA 0xa30000,HVA 可以不变,但旧 EPT 项和缓存必须撤销。Linux 通过 MMU notifier 这套内存变更通知机制告知 KVM 受影响的范围,KVM 使旧转换失效,并防止并发缺页把旧物理页重新写回。下一次重建后,同一 load 读取的是 0xa30120,内容随宿主页迁移得到保留。
3. MMIO 访问与脏页跟踪
GPA 若属于设备寄存器,就需要相应设备模型完成访问,不能照 RAM 路径随意分配页面。迁移期间还需记录客户机写过哪些 RAM 页:写保护可以让首次写入进入 KVM 处理,硬件也可提供辅助脏标记;bitmap 或 dirty ring 再把记录交给用户态。记录方式不同,目标都是让迁移器知道哪些内容需要重新发送。
五、转换、后备与跟踪应分别评价
| 性能现象 | 可用改进 | 代价与边界 |
|---|---|---|
| TLB 未命中和页表遍历多 | 大页、合适的地址布局 | 碎片、对齐与细粒度写保护可能限制大页 |
| 首次访存或回收后访问慢 | 内存预分配、合理 NUMA 放置 | 启动成本、提前占用和节点资源约束 |
| 多 vCPU 同时建图 | 并行页表管理,减少锁争用 | 失效同步、冲突重试和生命周期更复杂 |
| 迁移脏页持续增长 | 改进记录消费、传输或写入控制 | 更快记账无法补足传输带宽,也不能消除业务写入 |
影子页表用软件维护合成关系,EPT/NPT 用额外硬件转换换取更清晰的分工;两者最终都依赖宿主后备页的生命周期。评价时把转换未命中、缺页建图、真实内存访问和脏页跟踪分开,再观察吞吐、尾延迟与内存占用,才能判断优化发生在哪一层。
六、进一步阅读
- KVM 二级地址转换与缺页处理:完整跟踪一页 RAM 的映射与失效。
- KVM 大页映射:建立、拆分和恢复条件。
- KVM 脏页日志:位图与 Dirty Ring:记录的发布、消费和重置。
- QEMU 内存初始化与地址空间管理:后备分配、Q35 RAM 布局与 KVM 注册。
- vCPU 与 NUMA 内存放置:客户机拓扑、线程亲和性与实际页位置。
- 预拷贝内存迁移、virtio-mem:把映射管理放回迁移与容量调整过程。