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硬件自动完成。
通过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次内存访问,完成一次客户机的地址翻译
总结:
为了解决GVA-GPA-HPA的转换关系,在没有硬件辅助的时代,Hypervisor通过影子页表,很巧妙的将GVA-GPA映射到GVA-HPA, 功能虽然达成,但是在很多实际场景下,如进程频繁切换,内存频繁分配释放等,性能损耗会非常大。
EPT在硬件的帮助下,实现内存虚拟化简单直接,传统页表继续负责GVA-GPA, 而EPT负责GPA-HPA; 虽然内存访问延时可能会增加一些,但是大幅减少了因为页表更新带来的vmexit, 综合性价比提升巨大, 所以现代内存虚拟化,基本都被EPT统一了。
参考:
https://zhuanlan.zhihu.com/p/41467047