Page 65 - 《软件学报》2025年第9期
P. 65
3976 软件学报 2025 年第 36 卷第 9 期
类似于原生物理地址空间的功能, 虚拟机中的进程页表将客户机虚拟地址 (GVA) 映射到客户机物理地址
(GPA) [11,13,14,25] . 而从 hypervisor 的角度来看, 每个虚拟机都是一个在宿主机中运行的进程, 因此拥有一个对应的宿
主机进程页表, 用于维护宿主机虚拟地址 (HVA) 到宿主机物理地址 (HPA) 的映射 (例如, 在 QEMU/KVM 作为
hypervisor 的场景下, QEMU 的进程页表). 通常, GPA 和 HVA 是线性映射的, 因此 hypervisor 通常会直接维护从
GPA 到 HPA 的页表映射. 内存虚拟化在不同的硬件环境下通常采用不同的实现方案, 主要包括基于纯软件的影
子页表模型 [10] 和基于硬件辅助的嵌套页表模型 [2] . 表 2 总结并对比了在 RISC-V SV39 页表下的内存虚拟化技术,
包括本文实现的懒惰影子页表模型.
表 2 SV39 页表下的内存虚拟化解决方案对比
解决方案 TLB命中 TLB缺失后的访存次数 页表同步 硬件支持
快 快
原生场景 3 MMU
(HVA ⇒ HPA) (直接修改页表)
快 慢
影子页表模型 3 MMU
(GVA ⇒ HPA) (由hypervisor介入)
快 快
嵌套页表模型 15 MMU+H-extension
(GVA ⇒ HPA) (直接修改页表)
快 中
懒惰影子页表模型 3 MMU
(GVA ⇒ HPA) (批量修改, 快速路径)
嵌套页表模型是一种广泛应用的硬件辅助内存虚拟化技术. 当前 RISC-V 的虚拟化扩展已经支持了该方法.
当启用嵌套页表模型后, MMU 使用两个页表基地址寄存器来完成地址转换: 一个指向应用的进程页表 (vsatp), 另
一个指向每虚拟机的嵌套页表 (hgatp), 从而实现了从 GVA 到 GPA 再到 HPA 的地址翻译过程.
在理想情况下, 地址转换可以通过 TLB 命中直接从 GVA 转换为 HPA, 从而不会产生额外的开销. 然而, 在最
坏的情况下, 如果 TLB 未命中, 将触发一个二维的页表遍历, 这相比于原生地址翻译会显著增加开销, 因为每次访
问客户机进程页表都需要通过嵌套页表进行额外的遍历. 在 SV39 页表模型下, 地址翻译的内存引用次数将从原
生环境下的 3 次增加到虚拟化环境下的 15 次. 具体而言, 翻译 vsatp 寄存器中的基地址需要 3 次访问 (因为每个
GPA 都需要访问嵌套页表), 再加上 3 级虚拟机进程页表的每一级访问, 最终得到 HPA, 共计 3×4+3=15 次内存引
用. 当前, 现代商用处理器中的各种缓存机制, 如数据缓存 [26] 、MMU 缓存 [27,28] 以及翻译缓存 [2,3] , 可以减少 TLB 未
命中时所需的内存引用次数, 不过本文并不讨论它们的影响. 虽然在虚拟化环境下 TLB 未命中的开销比原生环境
更大, 但是嵌套页表模型允许虚拟机操作系统直接更新进程页表, 而无需 hypervisor 的干预.
影子页表模型是一种纯软件内存虚拟化技术, hypervisor 通过将虚拟机中的进程页表和嵌套页表合并按需创
建一个影子页表, 直接保存从 GVA 到 HPA 的完整翻译.
在理想情况下, 虚拟化地址转换可以通过命中 TLB 直接从 GVA 翻译到 HPA, 而不会产生额外的开销. 然而,
如果 TLB 未命中, MMU 只需要对影子页表执行一维的页表遍历. 此时, 页表基地址寄存器将直接指向影子页表,
所以页表遍历所需的内存引用次数与原生场景下页表遍历相同. 尽管 TLB 未命中的开销与原生执行相同, 但是由
于虚拟机操作系统不允许直接更新影子页表, 其对进程页表的操作无法直接影响到影子页表 [6] . 因此, 为了保证进
程页表和影子页表的一致性, 虚拟机每次对进程页表的更新都需要进行一次高开销的虚拟机退出以更新影子页表
的条目. 最常用的页表同步方法称为写保护 (write-protection). 具体而言, hypervisor 将虚拟机中的进程页表的页表
页在影子页表中都标记为只读, 这样在虚拟机中每次尝试对进程页表的修改都会触发一次写权限异常, 从而被
hypervisor 感知到并进行同步操作. 通过这种方法对影子页表的更新需要两次 VM-exit, 在传统架构下会消耗数千
个周期: 一次触发写权限异常进行页表同步, 另一次用于刷新 TLB [11] .
3 RISC-V 架构下的懒惰影子页表模型
鉴于全新的特权级模型和硬件特性, 本文在 RISC-V 架构下提出了懒惰影子页表模型, 以解决虚拟化技术在
内存虚拟化方面的性能瓶颈.

