内核虚拟化:内存虚拟化

最近在阅读《深度探索Linux系统虚拟化:原理与实现》这本书来学习虚拟化方面的知识,通过读书笔记的方式记录学习过程中的重要知识点。这篇文章主要是关于内存虚拟化方面的介绍。

内存虚拟化

内存虚拟化

基础概念

GVA

img

在虚拟化中,虚拟机称为 Guest OS, 其运行在物理机的 VMX Non-Root 模式下。在 Guest OS 内部,当 Guest OS 进入保护模式之后,Guest OS 内部程序和系统可以直接访问的地址称为 GVA。GVA 全称 “Guest Vritual Address”,即虚拟机程序运行的虚拟地址。GVA 的逻辑含义与物理机的虚拟地址概念一样,都是通过页表的方式与物理内存相关联,只是在 Guest OS 内部,GVA 通过页表与 GPA 相关联, 这里的 GPA 就是 Guest OS 的物理地址。


GPA/GFN

img

在虚拟化中,虚拟机称为 Guest OS,其运行在物理机的 VMX Non-Root 模式下。在 VMX Non-Root 模式下,Guest OS 可以访问的物理内存对应的地址称为 GPA. GPA 全称 “Guest Physical Address”, 因此 GPA 就称为虚拟机的物理地址. 从 Guest OS 内部来看,GPA 构成的地址空间就是 Guest OS 的物理内存,而从 Host 角度来看,GPA 构成的地址空间则是 Host 端的虚拟地址空间.

Guest OS 将物理内存按 PAGE_SIZE 大小将物理内存划分成一个个独立的物理内存区域,然后从 0 地址到高地址的方式,为每个物理内存区块进行编号,这个号码称为 Guest OS 的物理页帧号,简称 “GFN”, GFN 与 GPA 的关系如下:

GFN = GPA >> PAGE_SIZE


HVA

img

在虚拟化中,Host OS 的虚拟地址称为 HVA. 虚拟机对于 Host OS 来说是一个个独立的 qemu-kvm 进程,Guest OS 内部运行在 VMX Non-root 模式,Guest OS 是无法感知到 Host 虚拟地址的存在,只能感知到 Guest OS 内部的物理地址存在; 相反 Host OS 也无法感知到 Guest OS 的物理地址存在,对 Host OS 来说都是虚拟地址。qemu-kvm 在创建 Guest OS 的时候,从 Host OS 上申请一段段虚拟地址空间作为 Guest OS 的物理内存空间,这里的 Host 虚拟内存可能充当 Guest OS 的物理内存、SMIO、E820 预留区等。


HPA/HFN/PFN

img

在虚拟化中,HPA 称为物理主机的物理地址。与通常概念的物理地址一样,这是物理机正真的物理内存提供的地址。物理内存提供了物理地址、物理页帧号和 struct page 等信息。Host OS 直接访问虚拟地址,硬件 MMU/TLB 等设备会自动查询页表,将一个 HVA 自动转换成 HPA; 如果页表不存在,那么 Host OS 可以通过缺页机制为 HVA 与某个 HGA 建立映射。

Host OS 按 PAGE_SIZE 的大小将物理内存划分为多个内存区域,并从 0 地址到高地址的循序为每个物理内存区域进行编号,这个号码称为页帧号,也称为 PFN, 或者 HFN. 三者之间的关系如下:

PFN = HPA >> PAGE_SIZE

虚拟内存条

kvm中定义了一个结构体kvm_memory_region描述申请创建的内存条信息:

/* for KVM_CREATE_MEMORY_REGION */
struct kvm_memory_region {
	__u32 slot;
	__u32 flags;
	__u64 guest_phys_addr;
	__u64 memory_size; /* bytes */
};

slot:表示kvm_memory_region描述的是第几个内存条
guest_phys_addr:表示这块内存在guest物理地址空间中的起始地址
memory_size:表示内存条大小

内存条描述信息:

image-20230905181851360

kvm在内核模块中定义的代表内存条实列的数据结构kvm_memory_slot:

struct kvm_memory_slot {
	gfn_t base_gfn;
	unsigned long npages;
	unsigned long *dirty_bitmap;
	struct page **phys_mem;
	u32 flags;
	...
};

base_gfn:使用页帧号描述内存条起始地址
phys_mem:记录属于内存条的所有页面

这种在创建内存条时就为整个内存条预先静态分配好了所有内存页面的方式,如果Guest用不到其中的部分页面,内存就白白浪费了。更为严重的是,这种方式不能利用虚拟内存交换机制,因此,虚拟机物理内存的大小,以及申请虚拟机的数量,都受物理机物理内存大小的限制。实际上,以软件方式模拟的虚拟机,完全可以利用宿主系统的虚拟内存机制,申请内存占用大于物理机物理内存的虚拟机。因此,后来由内核分配的方式演化为由用户空间分配,这样就可以利用虚拟内存机制,与普通应用程序使用虚拟内存机制无异。内存条相关的数据结构与之前相比多了userpace_addr:

struct kvm_userspace_memory_region {
	...
	__u64 userspace_addr;
	...
};
struct kvm_userspace_memory_region {
	...
	unsigned long userspace_addr;
};

实模式下的guest寻址

实模式下的gpa到hva的转换:

image-20230906133432923

设置cpu处于虚拟8086模式

为了运行Guest内核头部的实模式代码,CPU切换到Guest模式后,首先需要切换到Virtual-8086模式。系统软件可以通过设置EFLAGS寄存器中的VM(Virtual-8086 Mode)标识位,使CPU运行在Virtual-8086模式,如图(eflags寄存器中virtual-8086 mode的标识位):

image-20230906133830461

在切入Guest模式前,KVM需要设置VMCS中的Guest的字段EFLAGS中的标识位VM。当切入Guest模式时,CPU装载VMCS中的Guest的字段EFLAGS到CPU寄存器EFLAGS,当CPU发现EFLAGS寄存器中的标识位VM置位后,触发CPU切换到Virtual-8086模式运行。

设置guest模式下的cr3寄存器

为了让在处于Guest模式的cr3寄存器指向KVM为Guest准备的页表,在切入Guest前,KVM模块需要设置VMCS中的Guest的cr3字段的值,使其指向KVM为Guest准备的、负责映射Guest物理地址到Host物理地址的专用页表的根页面的基址。初始,KVM为Guest准备的专用页表只需要准备一个根页面即可,然后在缺页异常时,缺页异常处理函数按需逐渐完成Guest物理地址到Host物理地址映射的建立。

对于运行于实模式的Guest来说,除了创建Guest后及首次切入Guest运行时,没有必要每次切入Guest时都设置Guest的cr3字段。运行于实模式的Guest,只需要一个页表完成Guest物理地址到Host物理地址的映射。相反,运行于保护模式下的Guest中的每个任务都有自己的页表,所以,Guest的VMCS的cr3字段需要根据任务的轮换,切换为正在运行的任务的页表。

缺页异常处理
1.为GPA分配空闲物理页面

当发生缺页异常后,在退出Guest模式之前,CPU首先将cr2寄存器中记录的引发缺页异常的地址记录到VMCS中的字段Exit qualification中。所以,异常处理函数handle_exception首先从VMCS的字段Exitqualification中读取缺页异常地址到变量cr2中,然后将这个地址传递给具体处理缺页异常的函数page_fault。

如同一台计算机可能有多个内存条一样,VMM也会为虚拟机分配多个内存条。对于虚拟机来说,其物理内存是由承载虚拟机的进程在其地址空间为虚拟机分配的一段一段的地址空间,每一段地址空间对虚拟机而言就相当于一个物理内存条,如下图所示。其中,每个内存条的gfn表示这个内存条在Guest的物理地址空间的起始页帧号,npages表示内存条的大小,每个内存条有个数组phys_mem记录支撑这个内存条的物理页面,比如phys_mem[0]记录的就是内存条的第1个物理页面。

虚拟内存条:

image-20230906135137174

2.建立页表映射

对于一个具体的GPA,根据每个内存条承载的Guest物理地址范围,就可以计算出GPA属于哪个内存条,进而为其分配这个内存条内的物理页面,

保护模式guest寻址

如果没有硬件虚拟化的支持,在切换到Guest时,cr3寄存器将指向Guest的页表。当Guest发出访存请求时,MMU将查询的是Guest的页表,最终发到总线上的将是GPA,不是真正的物理内存的地址。造成这一问题的根源是Guest和Host完全来自两个独立的“世界”,而物理上只有一个MMU单元,这个MMU被Guest的页表占用,Guest的页表中只是记录着GVA到GPA的映射,无法完成从GPA到HPA的映射。

一种可行的解决方案就是为每个Guest进程分别制作一张表,这张表中记录着GVA到HPA的映射关系。Guest模式下的cr3寄存器不再指向Guest的内部那张只能完成GVA到GPA映射的表,而是指向这张新的表。当MMU收到GVA时,通过遍历这张新的表,最终会将GVA翻译为HPA,从而将正确的物理地址送上地址总线。其中,有两个关键点:

  1. KVM需要构建从GVA映射到HPA的页表,而且这个页表需要根据Guest内部页表的信息更新,看起来这个表就像是Guest中页表的影子一样,如影随形。在实际进行地址映射时,因为cr3指向的是KVM构建的页表,所以生效的是这张表,其会将Guest内部的页表给遮挡(shadow)起来。所以,工程师们将KVM构建的这个页表称为影子页表。
  2. 保护模式的Guest有自己的页表,而且不只有一个页表,Guest中每个任务都会有自己的页表,这个页表随着任务的切换而进行切换。所以这就要求KVM也准备多个影子页表,每个Guest任务对应一个。而且,在Guest内部任务切换时,KVM需要洞悉这一切换时刻,切换对应的影子页表。

保护模式下建立影子页表映射的过程:

image-20230906140057864

设置cr3寄存器

因为Guest自身的页表不能完成GVA到HPA的多层地址映射,因此,每当Guest设置cr3寄存器时,KVM都要截获这个操作,将cr3指向影子页表。这就需要处于Guest模式的CPU能够在Guest设置cr3寄存器时触发虚拟机退出,从而陷入KVM模块。后来,Intel在硬件层面支持了EPT,所以无须再截获Guest设置cr3寄存器的操作,因此,为了在启用EPT的情况下避免无谓的虚拟机退出,Intel在硬件层面提供了一个开关,虚拟化软件可以通过这个开关决定当Guest设置cr3寄存器时是否触发虚拟机退出。这个开关就是VMCS中的Processor-ased VM-xecution Controls的第15位CR3-oad exiting。

影子页表下的缺页异常处理

与实模式Guest的缺页异常不同,保护模式的Guest发生缺页异常时,控制cr2寄存器中存储的是GVA,而只有Guest知道GVA到GPA的映射,所以,缺页异常处理函数首先需要遍历Guest的页表,取出GVA对应的GPA。如果Guest尚未建立GVA到GPA的映射,则KVM向Guest注入缺页异常,Guest进行正常的缺页异常处理,完成GVA到GPA的映射。因为影子页表尚未完成映射关系的建立,当GVA再次到达MMU时,将再次触发影子页表的缺页异常。当然,这次影子页表的缺页异常处理函数可以从Guest的页表中获取GPA,然后KVM利用Host内核的内存管理机制,完成GPA到HPA的映射,最后完成影子页表的构建。整个过程如下图:

image-20230906140801024

EPT

在讨论影子页表的方案时我们看到,遍历页表这些原本应由MMU做的事,现在要由CPU来负责了。而且,每次影子页表发生缺页异常后,CPU都会从Guest模式切换到Host模式,然后还要切回去,甚至还不止一次切换。更为严重的是,为了保持Guest页表和影子页表的一致,任何Guest对页表的修改,都需要触发VM exit,KVM截获后同步影子页表的修改,让影子页表的实现异常复杂且低效。

增加了EPT后,Guest就可以透明地使用MMU处理GVA到GPA的映射了,所以当Guest发生缺页异常时,无须从Guest模式切换到Host模式了,减少了CPU切换上下文的开销。而且,Guest的页表和EPT页表分别维护,影子页表中需要同步的开销也消失了。再者,对于一个虚拟机而言,虽然从Guest的角度来看其中会有多个任务,因此需要维护多个页表,但是从宿主机的角度,一个虚拟机只是一个进程,因此维护一个EPT表即可,相对于影子页表,减少了内存占用。因为Guest内部切换进程时,不需要切换EPT,所以也减少了CPU在Guest模式和Host模式之间的切换。

开启ept后的缺页异常处理过程:

image-20230906141409817

当Guest内部发生缺页异常时,CPU不再切换到Host模式了,而是由Guest自身的缺页异常处理函数处理。当地址从GVA翻译到GPA后,GPA在硬件内部从MMU流转到了EPT。

如果EPT页表中存在GPA到HPA的映射,则EPA最终获取了GPA对应的HPA,将HPA送上地址总线。如果EPT中尚未建立GPA到HPA的映射,则CPU抛出EPT异常,CPU从Guest模式切换到Host模式,KVM中的EPT异常处理函数负责寻找空闲物理页面,建立EPT表中GPA到HPA的映射。

疑惑:最近在看kvmmmu中的内核源码时,看到了很多以tdp开头的函数,便有了疑惑虚拟化中的EPT页表和TDP页表是一回事吗?

EPT是Intel处理器中实现的一种技术,用于支持虚拟化内存管理。它允许虚拟机监视器(VMM,也称为hypervisor)通过二级分页机制来管理虚拟机的物理内存访问。EPT工作原理是,在传统的页表(将虚拟地址转换为物理地址)之上添加了一个额外的层级,这个额外的层级将虚拟机的物理地址(也称为宾客物理地址)映射到宿主机的物理地址。这使得虚拟机在尽可能接近硬件级别上进行内存隔离和管理成为可能。

TDP(Two-Dimensional Paging)通常被视为与EPT相同或非常相似的概念,尤其是在描述这种二级分页机制的上下文中。"Two-Dimensional Paging"这个术语强调了内存地址转换过程中的两个维度:一个是虚拟机内部的虚拟地址到虚拟机的物理地址的转换(通过虚拟机自己的页表完成),另一个是虚拟机的物理地址到宿主机物理地址的转换(通过EPT完成)。

虽然"EPT"和"TDP"在很多文献和讨论中可能被交替使用,它们本质上指的是相同的技术机制,即在虚拟化环境中通过硬件辅助的方式进行内存地址转换和管理。不过,"EPT"是Intel的术语,更具体地指向Intel实现的技术,而"TDP"则是一个更泛化的术语,用于描述这种两级页表机制的概念。

参考

  • http://www.biscuitos.cn/blog/Memory-Virtualization/
  • 《深度探索Linux系统虚拟化:原理与实现》
  • 22
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值