1. 内存虚拟化

现代多任务操作系统设计,一般进程之间使用不同的虚拟地址空间相互隔离, 在实现上:

  • 操作系统负责维护进程页表,映射虚拟地址到物理地址的关系
  • CPU的内存管理单元(MMU)负责执行地址转换
  • CPU提供TLB(Translation lookaside buffer)缓存最近用到的转换结果,加速转换效率

虚拟化技术引入后,内存地址空间更加复杂了,客户机(Guest)和宿主机(Host)都有自己的地址空间:

  • GVA: Guest虚拟地址
  • GPA: Guest物理地址
  • HVA: Host虚拟地址
  • HPA: Host物理地址

显而易见,Guest负责GVA和GPA之间的转换;Host负责HVA和HPA之间的转换;而GPA和HPA之间的转换,就需要虚拟化层(Hypervisor)辅助了,这个过程一般被称为内存虚拟化。

2. 影子页表

早期的X86 CPU硬件辅助虚拟化能力很不完善,所以Hypervisor需要通过软件实现内存虚拟化。因此,Hypervisor为每个客户机每套页表额外再维护一套页表,通常也称为影子页表。

同时, Hypervisor截获客户机里面任何试图修改客户机页表或者刷新TLB的操作,将GVA到GPA的的修改,转变成GVA到GPA的修改。这些操作包括:

  • 写gCR3(Guest CR3)寄存器和原来一样的内容,一般用作刷新TLB
  • 写gCR3(Guest CR3)寄存器不同的物理地址,一般是发生了进程切换
  • 修改部分页表,这时候必须调用INVLPG指令失效对应的TLB

这样,Guest中的页表实际变成了虚拟页表,Hypervisor截获了Guest相关的修改操作并更新到影子页表,而真正装入物理MMU是影子页表; Guest中GVA和GPA之间的转换实际上变成了GVA与HPA的转换,TLB中缓存的也是GVA和HPA的映射,Guest内存访问没有额外的地址转换开销。

影子页表也带来了下面的主要缺点:

  • Hypervisor 需要为每个客户机的每个进程的页表都要维护一套相应的影子页表,这会带来较大内存上的额外开销;
  • 客户在读写CR3、执行INVLPG指令或客户页表不完整等情况下均会导致VM exit,这导致了内存虚拟化效率很低;
  • 客户机页表和和影子页表的同步也比较复杂;

3. Intel EPT技术

为了简化内存虚拟化的实现,以及提升内存虚拟化的性能,Intel推出了EPT(Enhanced Page Table)技术,即在原有的页表基础上新增了EPT页表实现另一次映射。这样,GVA-GPA-HPA两次地址转换都由CPU硬件自动完成。

vm 开启虚拟化 vmp虚拟化_vm 开启虚拟化


通过EPT的GVA和HPA大概翻译的过程:

(1) 处于非根模式的CPU加载guest进程的gCR3

(2) gCR3是GPA,cpu需要通过查询EPT页表来实现GPA->HPA

(3) 如果没有,CPU触发EPT Violation, 由Hypervisor截获处理

(4) 假设客户机有m级页表,宿主机EPT有n级,在TLB均miss的最坏情况下,会产生MxN次内存访问,完成一次客户机的地址翻译

vm 开启虚拟化 vmp虚拟化_虚拟地址_02


总结:

为了解决GVA-GPA-HPA的转换关系,在没有硬件辅助的时代,Hypervisor通过影子页表,很巧妙的将GVA-GPA映射到GVA-HPA, 功能虽然达成,但是在很多实际场景下,如进程频繁切换,内存频繁分配释放等,性能损耗会非常大。

EPT在硬件的帮助下,实现内存虚拟化简单直接,传统页表继续负责GVA-GPA, 而EPT负责GPA-HPA; 虽然内存访问延时可能会增加一些,但是大幅减少了因为页表更新带来的vmexit, 综合性价比提升巨大, 所以现代内存虚拟化,基本都被EPT统一了。

参考:
https://zhuanlan.zhihu.com/p/41467047