x86内存虚拟化--影子页表(Shadow Page Table)和拓展页表(EPT)

阅读本篇博客需要知道操作系统中的虚拟内存TLB,x86页表转换,hypervisor/VMM概念,对x86 CPU虚拟化有一定了解。

本篇博客首先对虚拟内存的概念进行回顾 ,再介绍虚拟机里面的进程访问物理机上的内存的流程,最后对两种内存虚拟化机制–影子页表和拓展页表进行说明。

Recap

  • 物理地址和虚拟地址
  • MMU (Memory Management Unit):即内存管理单元,是计算机系统中的一个硬件组件,用于管理计算机的内存访问。可以实现虚实地址翻译、访问权限控制等。从虚实地址翻译的角度来说,MMU负责将虚拟地址(由CPU产生)转换为物理地址(RAM中实际存储数据的地址),相当于一个page table walker。
  • TLB (Translation Lookaside Buffer):它是计算机体系结构中的一种硬件高速缓存,用于加速虚拟地址到物理地址的转换过程。一般而言,MMU需要经过多级页表访问才能获得物理地址。而TLB中直接存储了最近被访问过的虚拟地址到物理地址的映射关系,允许CPU在某些情况下直接从TLB中获取映射,而无需每次都查询内存页表。和一般的cache一样,TLB的大小有限。

在操作系统启动页表后,虚实地址转化的流程如下:

virt_mem_flow

更具体的,在x86-64分页中,上图中的Page Table Walk的整个流程如下:

linux - are page tables under utilized in x86 systems - Super User

CR3是x86架构中的页目录基址寄存器,存储了一个指向页目录表(Page Directory Table)的指针。页目录表用于将虚拟地址映射到物理地址的页表。操作系统通过修改CR3寄存器中的值来切换不同的页目录表,从而实现不同进程之间的内存隔离。当CPU执行访问内存的指令时,它会将虚拟地址发送给内存管理单元(MMU),然后MMU会利用CR3寄存器中存储的页目录表的地址来查找对应的页表项。通过多级页表结构,MMU最终将虚拟地址转换为对应的物理地址,然后访问实际的内存数据。CR3和每一个页表项如PML4E, PDPE, PDE, PTE中存储的都是物理地址。

虚拟化场景下的访存

一些会出现在本篇博客里的缩写

  • HPA (Host Physical Address), HVA (Host Virtual Address)
  • GPA (Guest Physical Address), GVA (Guest Virtual Address)
  • SPT (Shadow Page table), EPT (Extended Page Table)

虚拟机中的进程访存,要实现GVA -> GPA -> HVA -> HPA的转化。即要经历以下步骤:

  1. 客户端虚拟地址经过guest中的页表转化为客户端的物理地址;
  2. 客户端的物理地址经过转换,变为宿主端的虛拟地址。VMM通常会用内部数据结构来记录这个映射。例如,kvm中有一个数据结构kvm_memory_slot会记录此映射关系。
  3. 宿主端的虛拟地址再经过host上的页表的转换变为真实可用的物理地址。

在上述操作的第2部中,每一次GPA->HVA的转化都需要通过vmexit(如果不清楚vmexit的概念,请先阅读本篇博客–x86 CPU虚拟化),从客户机退出到宿主才能完成。如果有四级页表,不考虑TLB,每一次虚实地址转换都需要4次vmexit,16次页表访问来完成。整个转换过程如下(该过程只是为了方便理解之后的内存虚拟化展示,没有系统会这样实现):

flowchart

我们假设,在Guest中所有页表相关的内存区域会被VMM设置为不可读不可写,那么 Guest每次访问页表时都会触发vmexit。在vmexit的处理中,VMM把转换GPA成HVA,再通过访问VMM对应的页表得到HPA,最后通过HPA指向的内存数据,得到下一级页表的guest物理地址/最后的guest物理地址,返回给guest。guest再次访问host返回的下一级页表的物理地址+对应的虚拟地址index的地址,触发vmexit。

这个过程中需要注意的是,CR3寄存器在Host和Guest中是两个不一样的值。在系统vmexit, vmentry的过程中,硬件会根据内存中VMCS结构中保存的host/guest CR3值更新CR3寄存器的值。

内存虚拟化

显而易见,从GVA -> GPA -> HVA -> HPA的转化路径太长,中间还会多次发生vmexit,严重影响性能。内存虚拟化想要做的事情就是希望可以直接将guest虚拟地址映射为host物理地址,也就是GVA->HPA的映射。

解决这个问题的目前有软件和硬件两种方式:

  • 影子页表(软件实现):hypervisor新建一套页表用于维护GVA->HPA的映射。
  • 拓展页表(硬件实现):hypervisor新建一套页表用于维护GPA->HPA的映射,硬件增加一套页表转化机制(相当于另一组MMU),直接读取该映射关系。

影子页表 (SPT)

影子页表其实就是VMM把Guest和Host中的页表合并成一个新的页表,来实现GVA->HPA映射。

由于写CR3寄存器是一条特权指令,所以guest操作系统在把当前进程的页表基址载入CR3时,VMM会截获这条指令,并将影子页表基地址写入VMCS对应的GUEST_CR3字段(如果不清楚VMCS的概念,请先阅读本篇博客–Basic concepts in Intel VT-x)。再次进入guest时,硬件会把影子页表的基地址载入CR3寄存器。也就是说,guest页表是个被架空的傀儡,CR3寄存器中真正的值指向影子页表的基地址。

Guest在访问虚拟地址时(GVA),由于CR3指向影子页表,影子页表中维护了GVA->HPA映射,MMU就可以直接读取GVA对应的HPA值,不需要VMM参与。

Host中的页表用于VMM内部的虚拟地址的物理地址的转换,即HVA->HPA的转换。

SPT

那么,我们应该如何维护影子页表中的表项呢?即影子页表中的指该怎么填写?这里我们也用一张图来说明影子页表的实现过程,为叙述方便,在这里我们不考虑TLB(即使添加TLB也只是再加一层判断而已),个人理解,如有错误欢迎指出。VMM会将guest Page Table本身使用的物理页面设为write protected。总之,影子页表实现非常复杂,例如需要考虑各种各样页表同步的问题,这个图中省略了很多细节。此外,影子页表的内存开销也很大,需要为每个客户机进程对应的页表都维护一个影子页表。

SPT_flow

拓展页表(EPT)

为了解决影子页表难实现并且内存开销大的问题,VT-x提供了EPT技术,直接在硬件上支持GVA->GPA->HPA的两次地址转换。在原有的CR3页表地址映射的基础上,EPT引入了EPT页表来实现另一次映射。这样GVA->GPA->HPA两次地址转换都由CPU硬件自动完成。

EPT

CPU 首先会查找Guest CR3 指向的L4页表。由于 Guest CR3 给出的是GPA,因此CPU需要通过 EPT 页表来实现 Guest CR3 GPA->HPA 的转换。CPU 首先会查看硬件的EPT TLB,如果没有对应的转换(图中未显示),CPU 会进一步查找EPT 页表,如果还没有,CPU 则拋出EPT Violation 异常由VMM来处理

获得L4 页表地址后,CPU 根据 GVA 和L4页表项的内容,来获取 L3 页表项的GPA。如果L4页表中 GVA 对应的表项显示为“缺页”,那么CPU 产生Page Fault, 直接交由Guest kernel处理。注意,这里不会产生 VM-Exit。获得L3 页表项的GPA后,CPU 同样要通过查询 EPT 页表来实现 L3 GPA->HPA 的转换,过程和上面一样。

同样的,CPU 会依次查找 L2、L1 页表,最后获得 GVA 对应的GPA,然后通过查询EFT 页表获得 HPA。从上面的过程可以看出,CPU 需要5次查询 EPT页表,每次查询都需要4 次内存访问,因此最坏情况下总共需要20次内存访问。EPT 硬件通过增大 EPTTLB来尽量减少内存访问。

SPT vs. EPT

  Shadow page table Extended page table
vmexit次数 较多 较少
访存速度 GVA->HPA直接映射,速度快 GVA->GPA->HPA两级映射,需要访问两张页表,速度慢
开发难度 VMM需要实现页表同步,权限模拟等,开发维护难度大 硬件辅助guest OS通知VMM,有特殊寄存器辅助开发,开发难度小

Reference

  1. 《系统虚拟化:原理与实现》5.5内存虚拟化
  2. Hypervisor From Scratch – Part 4: Address Translation Using Extended Page Table (EPT)

  3. 一文看懂影子页表和拓展页表
  4. CPU & Memory Virtualization Part 5
  5. Virtual Machines Lecture 3 Memory Virtualization
  6. Linux虚拟化:KVM影子页表



Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Solana Core Concepts: Accounts, Programs, and PDAs
  • Python3 Concurrency: asyncio module/async/await
  • Python Cheat Sheet
  • MIT6.824 Lab2A Raft Leader Election
  • Linux中的top command