有了虚拟机,内存就变成了四类:
- 虚拟机里面的虚拟内存(Guest OS Virtual Memory,GVA),这是虚拟机里面的进程看到的内存空间;
- 虚拟机里面的物理内存(Guest OS Physical Memory,GPA),这是虚拟机里面的操作系统看到的内存,它认为这是物理内存;
- 物理机的虚拟内存(Host Virtual Memory,HVA),这是物理机上的 qemu 进程看到的内存空间;
- 物理机的物理内存(Host Physical Memory,HPA),这是物理机上的操作系统看到的内存。
由于 CPU 和内存是紧密结合的,因而内存虚拟化的初始化过程,和 CPU 虚拟化的初始化是一起完成的。
内存映射对于虚拟机来讲是一件非常麻烦的事情,从 GVA 到 GPA 到 HVA 到 HPA,性能很差,为了解决这个问题,有两种主要的思路。
第一种方式就是软件的方式,影子页表 (Shadow Page Table)。
内存映射要通过页表来管理,页表地址应该放在 cr3 寄存器里面。本来的过程是,客户机要通过 cr3 找到客户机的页表,实现从 GVA 到 GPA 的转换,然后在宿主机上,要通过 cr3 找到宿主机的页表,实现从 HVA 到 HPA 的转换。
为了实现客户机虚拟地址空间到宿主机物理地址空间的直接映射。客户机中每个进程都有自己的虚拟地址空间,所以 KVM 需要为客户机中的每个进程页表都要维护一套相应的影子页表。
在客户机访问内存时,使用的不是客户机的原来的页表,而是这个页表对应的影子页表,从而实现了从客户机虚拟地址到宿主机物理地址的直接转换。而且,在 TLB 和 CPU 缓存上缓存的是来自影子页表中客户机虚拟地址和宿主机物理地址之间的映射,也因此提高了缓存的效率。
但是影子页表的引入也意味着 KVM 需要为每个客户机的每个进程的页表都要维护一套相应的影子页表,内存占用比较大,而且客户机页表和和影子页表也需要进行实时同步。
第二种方式,就是硬件的方式,Intel 的 EPT(Extent Page Table,扩展页表)技术。
EPT 在原有客户机页表对客户机虚拟地址到客户机物理地址映射的基础上,又引入了 EPT 页表来实现客户机物理地址到宿主机物理地址的另一次映射。客户机运行时,客户机页表被载入 CR3,而 EPT 页表被载入专门的 EPT 页表指针寄存器 EPTP。
有了 EPT,在客户机物理地址到宿主机物理地址转换的过程中,缺页会产生 EPT 缺页异常。KVM 首先根据引起异常的客户机物理地址,映射到对应的宿主机虚拟地址,然后为此虚拟地址分配新的物理页,最后 KVM 再更新 EPT 页表,建立起引起异常的客户机物理地址到宿主机物理地址之间的映射。
KVM 只需为每个客户机维护一套 EPT 页表,也大大减少了内存的开销。
因为使用了 EPT 之后,客户机里面的页表映射,也即从 GVA 到 GPA 的转换,还是用传统的方式,和在内存管理那一章讲的没有什么区别。而 EPT 重点帮我们解决的就是从 GPA 到 HPA 的转换问题。因为要经过两次页表,所以 EPT 又称为 tdp(two dimentional paging)。
EPT 的页表结构也是分为四层,EPT Pointer (EPTP)指向 PML4 的首地址。
管理物理页面的 Page 结构和咱们讲内存管理那一章是一样的。EPT 页表也需要存放在一个页中,这些页要用 kvm_mmu_page 这个结构来管理。
虚拟机的内存管理也是需要用户态的 qemu 和内核态的 KVM 共同完成。为了加速内存映射,需要借助硬件的 EPT 技术。
在用户态 qemu 中,有一个结构 AddressSpace address_space_memory 来表示虚拟机的系统内存,这个内存可能包含多个内存区域 struct MemoryRegion,组成树形结构,指向由 mmap 分配的虚拟内存。
在 AddressSpace 结构中,有一个 struct KVMMemoryListener,当有新的内存区域添加的时候,会被通知调用 kvm_region_add 来通知内核。
在用户态 qemu 中,对于虚拟机有一个结构 struct KVMState 表示这个虚拟机,这个结构会指向一个数组的 struct KVMSlot 表示这个虚拟机的多个内存条,KVMSlot 中有一个 void *ram 指针指向 mmap 分配的那块虚拟内存。
kvm_region_add 是通过 ioctl 来通知内核 KVM 的,会给内核 KVM 发送一个 KVM_SET_USER_MEMORY_REGION 消息,表示用户态 qemu 添加了一个内存区域,内核 KVM 也应该添加一个相应的内存区域。
和用户态 qemu 对应的内核 KVM,对于虚拟机有一个结构 struct kvm 表示这个虚拟机,这个结构会指向一个数组的 struct kvm_memory_slot 表示这个虚拟机的多个内存条,kvm_memory_slot 中有起始页号,页面数目,表示这个虚拟机的物理内存空间。
虚拟机的物理内存空间里面的页面当然不是一开始就映射到物理页面的,只有当虚拟机的内存被访问的时候,也即 mmap 分配的虚拟内存空间被访问的时候,先查看 EPT 页表,是否已经映射过,如果已经映射过,则经过四级页表映射,就能访问到物理页面。
如果没有映射过,则虚拟机会通过 VM-Exit 指令回到宿主机模式,通过 handle_ept_violation 补充页表映射。先是通过 handle_mm_fault 为虚拟机的物理内存空间分配真正的物理页面,然后通过 __direct_map 添加 EPT 页表映射。
此文章为11月Day30学习笔记,内容来源于极客时间《趣谈Linux操作系统》,推荐该课程。