标签(空格分隔): KVMhtml
前一章介绍了CPU虚拟化的内容,这一章介绍一下KVM的内存虚拟化原理。能够说内存是除了CPU外最重要的组件,Guest最终使用的仍是宿主机的内存,因此内存虚拟化其实就是关于如何作Guest到宿主机物理内存之间的各类地址转换,如何转换会让转换效率更高呢,KVM经历了三代的内存虚拟化技术,大大加快了内存的访问速率。测试
在保护模式下,普通的应用进程使用的都是本身的虚拟地址空间,一个64位的机器上的每个进程均可以访问0到2^64的地址范围,实际上内存并无这么多,也不会给你这么多。对于进程而言,他拥有全部的内存,对内核而言,只分配了一小段内存给进程,待进程须要更多的进程的时候再分配给进程。
一般应用进程所使用的内存叫作虚拟地址,而内核所使用的是物理内存。内核负责为每一个进程维护虚拟地址到物理内存的转换关系映射。
首先,逻辑地址须要转换为线性地址,而后由线性地址转换为物理地址。优化
逻辑地址 ==> 线性地址 ==> 物理地址
逻辑地址和线性地址之间经过简单的偏移来完成。
ui
一个完整的逻辑地址 = [段选择符:段内偏移地址],查找GDT或者LDT(经过寄存器gdtr,ldtr)找到描述符,经过段选择符(selector)前13位在段描述符作index,找到Base地址,Base+offset就是线性地址。spa
为何要这么作?听说是Intel为了保证兼容性。线程
逻辑地址到线性地址的转换在虚拟化中没有太多的须要介绍的,这一层不存在实际的虚拟化操做,和传统方式同样,最重要的是线性地址到物理地址这一层的转换。3d
传统的线性地址到物理地址的转换由CPU的页式内存管理,页式内存管理。
页式内存管理负责将线性地址转换到物理地址,一个线性地址被分五段描述,第一段为基地址,经过与当前CR3寄存器(CR3寄存器每一个进程有一个,线程共享,当发生进程切换的时候,CR3被载入到对应的寄存器中,这也是各个进程的内存隔离的基础)作运算,获得页表的地址index,经过四次运算,最终获得一个大小为4K的页(有可能更大,好比设置了hugepages之后)。整个过程都是CPU完成,进程不须要参与其中,若是在查询中发现页已经存在,直接返回物理地址,若是页不存在,那么将产生一个缺页中断,内核负责处理缺页中断,并把页加载到页表中,中断返回后,CPU获取到页地址后继续进行运算。code
因为qemu-kvm进程在宿主机上做为一个普通进程,那对于Guest而言,须要的转换过程就是这样。htm
Guest虚拟内存地址(GVA) | Guest线性地址 | Guest物理地址(GPA) | Guest ------------------ | HV HV虚拟地址(HVA) | HV线性地址 | HV物理地址(HPA)
What's the fu*k ?这么多...
别着急,Guest虚拟地址到HV线性地址之间的转换和HV虚拟地址到线性地址的转换过程能够省略,这样看起来就更清晰一点。blog
Guest虚拟内存地址(GVA) | Guest物理地址(GPA) | Guest ------------------ | HV HV虚拟地址(HVA) | HV物理地址(HPA)
前面也说到KVM经过不断的改进转换过程,让KVM的内存虚拟化更加的高效,咱们从最初的软件虚拟化的方式介绍。
第一层转换,由GVA->GPA的转换和传统的转换关系同样,经过查找CR3而后进行页表查询,找到对应的GPA,GPA到HVA的关系由qemu-kvm负责维护,咱们在第二章KVM启动过程的demo里面就有介绍到怎样给KVM映射内存,经过mmap的方式把HV的内存映射给Guest。
struct kvm_userspace_memory_region region = { .slot = 0, .guest_phys_addr = 0x1000, .memory_size = 0x1000, .userspace_addr = (uint64_t)mem, };
能够看到,qemu-kvm的kvm_userspace_memory_region结构体描述了guest的物理地址起始位置和内存大小,而后描述了Guest的物理内存在HV的映射userspace_addr
,经过多个slot,能够把不连续的HV的虚拟地址空间映射给Guest的连续的物理地址空间。
软件模拟的虚拟化方式由qemu-kvm来负责维护GPA->HVA的转换,而后再通过一次HVA->HPA的方式,从过程上来看,这样的访问是很低效的,特别是在当GVA到GPA转换时候产生缺页中断,这时候产生一个异常Guest退出,HV捕获异常后计算出物理地址(分配新的内存给Guest),而后从新Entry。这个过程会可能致使频繁的Guest退出,且转换过程过长。因而KVM使用了一种叫作影子页表的技术。
影子页表的出现,就是为了减小地址转换带来的开销,直接把GVA转换到HVP的技术。在软件虚拟化的内存转换中,GVA到GPA的转换经过查询CR3寄存器来完成,CR3保存了Guest中的页表基地址,而后载入MMU来作地址转换。
在加入了影子页表的技术后,当访问到CR3寄存器的时候(多是因为Guest进程后致使的),KVM捕获到这个操做,CPU虚拟化章节 EXIT_REASON_CR_ACCESS,qemu-kvm经过载入特俗的CR3和影子页表来欺骗Guest这个就是真实的CR3,后面的操做就和传统的访问内存的方式一致,当须要访问物理内存的时候,只会通过一层的影子页表的转换。
影子页表由qemu-kvm进程维护,实际上就是一个Guest的页表到宿主机页表的映射,每一级的页表的hash值对应到qemu-kvm中影子页表的一个目录。在初次GVA->HPA的转换时候,影子页表没有创建,此时Guest产生缺页中断,和传统的转换过程同样,通过两次转换(VA->PA),而后影子页表记录GVA->GPA->HVA->HPA。这样产生GVA->GPA的直接关系,保存到影子页表中。
影子页表的引入,减小了GVA->HPA的转换过程,可是坏处在于qemu-kvm须要为Guest的每一个进程维护一个影子页表,这将带来很大的内存开销,同时影子页表的创建是很耗时的,若是Guest进程过多,将致使频繁的影子页表的导入与导出,虽然用了cache技术,可是仍是软件层面的,效率并非最好,因此Intel和AMD在此基础上提供了硬件虚拟化技术。
EPT(extended page table)能够看作一个硬件的影子页表,在Guest中经过增长EPT寄存器,当Guest产生了CR3和页表的访问的时候,因为对CR3中的页表地址的访问是GPA,当地址为空时候,也就是Page fault后,产生缺页异常,若是在软件模拟或者影子页表的虚拟化方式中,此时会有VM退出,qemu-kvm进程接管并获取到此异常。可是在EPT的虚拟化方式中,qemu-kvm忽略此异常,Guest并不退出,而是按照传统的缺页中断处理,在缺页中断处理的过程当中会产生EXIT_REASON_EPT_VIOLATION,Guest退出,qemu-kvm捕获到异常后,分配物理地址并创建GVA->HPA的映射,并保存到EPT中,将EPT载入到MMU,下次转换时候直接查询根据CR3查询EPT表来完成GVA->HPA的转换。之后的转换都由硬件直接完成,大大提升了效率,且不须要为每一个进程维护一套页表,减小了内存开销。
在笔者的测试中,Guest和HV的内存访问速率对比为3756MB/s对比4340MB/s。能够看到内存访问已经很接近宿主机的水平了。
KVM内存的虚拟化就是一个将虚拟机的虚拟内存转换为宿主机物理内存的过程,Guest使用的依然是宿主机的物理内存,只是在这个过程当中怎样减小转换带来的开销成为优化的主要点。 KVM通过软件模拟->影子页表->EPT的技术的进化,效率也愈来愈高。