原文地址:http://blog.csdn.net/ariesjzj/article/details/8745035
内存虚拟化是虚拟机实现中的重要部分。在虚拟机中,虚拟出来的Guest OS和Host OS用的是相同的物理内存,却不能让它们相互影响到。具体地说,如果OS跑在裸机上(而非虚拟机上)的话,只要OS提供页表,MMU会在访存时自动做虚拟地址(Virtual address, VA)到物理地址(Physical address, PA)的转化。而跑在虚拟机上时,Guest OS经过地址转化看到的“物理地址”并不是真实物理内存上的地址,因此还需要将其转化为真实物理内存地址,称为机器地址(Machine address, MA)。也就是说Guest OS要访问VA需要经过VA=>PA=>MA的转化。注意在没有虚拟机的情况下,物理地址PA就是机器地址MA。如何在虚拟机中完成这样的地址转化呢?本文主要介绍以下几种主流方案。
1. VTLB(Virtual TLB)
首先介绍一下背景,TLB是由硬件实现的,里面存放的是虚拟地址到物理地址的映射关系。当系统要访问一个虚拟地址而TLB中有的话,就不用去页表里找了。这样可以提高效率,因为页表本身在内存中,走页表也是要访存的。只有当TLB中没有MMU才会从页表中找地址映射关系并把它放到TLB中,这样下次就可以不用访问页表了。
VTLB,顾名思义,就是在虚拟机的hypervisor中维护一份虚拟的“TLB”,而真实的cr3指向的正是这份虚拟TLB而非Guest OS的页表。VTLB中的“TLB”很容易让人误解,因为这份虚拟的TLB本身也是一份完整的页表,它拥有页表的层次结构,这和硬件TLB类似于hash表的结构是不同的。称之为虚拟TLB完全是因为其更新方式与TLB类似。我们知道,操作系统中当中更新了页表后需手动更新TLB,新的映射关系才能生效。比如本来在页表和TLB中虚拟地址0x11111111映射到物理地址0x22222222,现在Guest OS更新了页表,使虚拟地址0x11111111映射到物理地址0x33333333,如果没有执行相应的flush TLB指令(invlpg, write cr3等,注意x86不允许显式修改TLB内容,只能清掉其中的一项或多项),系统看到的仍是0x11111111到0x22222222的映射,因为TLB中的映射关系没有变(系统先看的是TLB中的映射,没有的话才看页表)。因此,操作系统中一旦更新了页表,一般就会跟着执行flush TLB指令(除了invlpg,load cr3和改变cr4的相关位等也会引起TLB flush)。因此虚拟机只要截获这些指令,然后将相应的地址转换关系更新到VTLB中即可。比如Guest OS要flush TLB中VA为0x11111111的映射,hypervisor需要做的是查找Guest OS的页表中0x11111111对应的物理地址PA,然后通过事先建立好的P2M表(PA=>MA)将之转化为MA,最后填入VTLB(也就是真实cr3指向的这张页表)。另外当page fault发生时,hypervisor也需要做类似工作。不同的是,如果page fault发生时在Guest OS页表中也找不到需要的映射时,hypervisor需要将这个page fault重新inject到Guest OS中,让Guest OS先填好自己的页表。然后再次发生page fault时,hypervisor再根据Guest OS页表填好VTLB。另外页表中需要对MMIO作额外处理,MMIO需要始终保持缺页状态,因为设备都是虚拟出来的,缺页才能在Guest OS访问设备时让hypervisor获得控制权。
VTLB的优点是实现简单,只要截获会引起TLB flush的相关指令即可。同时注意这些指令-invlpg, load cr3 和cr4同时又都是特权指令,这就意味着这种方案下,不用更改Guest OS,只要将Guest OS降到非特权级,它执行这些特权指令时就会因为权限不够被hypervisor截获,全虚拟化就可以实现。其缺点也是显而易见的,因为每次切换进程时都会write cr3,导致VTLB被清空,于是造成大量的的hidden page fault(即Guest OS的页表中有映射而VTLB中没有所造成的page fault)。而这正是下面方案所要解决的问题。
2. SPT(Shadow page table)
其实前面说的VTLB本质上也是一种Shadow的page table。但是Shadow page table一般特指有多个页表缓存的方案。比如hypervisor中存放了4组页表的缓存,用pgdir(即cr3值)作为它的key。举例来说,4组页表的pgdir地址分别为0x11111000, 0x22222000, 0x33333000, 0x44444000,那么这4个地址就是这4组页表的key。当write cr3被截获时,hypervisor就到这4组页表缓存去找,看它们的key中是否有即将要写入的cr3值。如果有,说明要载入的页表以前有缓存,直接拿出来用即可。如果没有,只能老老实实新创建一个。这里要注意的是如果Guest OS更新那些当前不在用的页表项(不属于cr3指向的页表),hypervisor也需要截获到并且更新到相应的页表缓存中。拿前面的例子来说,当前用的如果是0x11111000这份页表,这时Guest OS更新了0x22222000指向的这份页表中的某个页表项,相应的页表缓存应该得到更新。这就意味着只截TLB flush指令是不够的,还需要截获Guest OS用来更新页表的操作,幸运地是,这样的接口已经存在在Linux kernel中,称为pv_mmu_ops。在SPT中,我们需要截获其中的set_pte, set_pte_at, set_pmd, pte_update等操作。至于page fault handler方面,和VTLB是差不多的。
SPT的优点主要来自于性能上的提升。由于时间局部性,系统中经常会是几个进程之间回来切换,所以哪怕是4组页表缓存,其重用率也可达到80~90%。因此和VTLB相比,其性能可以大大提高。其缺点是由于要维护多份页表缓存,还是存在一定的额外开销,并且由于要存放这些缓存,内存上也会有些消耗。下面的PVMMU解决了这个问题。
Linux kernel中的Lguest采用了SPT。
3. PVMMU(aka. Direct paging)
PVMMU和前面两种方案的主要区别是这里真实cr3指向的不再是hypervisor中维护的页表,而是直接指向Guest OS的页表。不同的是Guest OS页表中放的不再是VA到PA的映射,而是直接从VA到MA的映射。这就需要我们截获Guest OS对页表的几乎所有访问。好在前面提到的pv_mmu_ops为我们提供了这样的接口,我们只要在其中加上我们自己的实现就行了。和SPT相比,我们不权截获页表更新的操作,还要截获页表读取的操作。在初始化时,我们需要建立两张表-P2M表和M2P表。前者完成PA到MA的转化,后者相反。当Guest OS需要创建页表项时(如调用make_pte),我们需要将pte中的PFN(Physical frame number)转为MFN(Machine frame number),而当Guest OS要读取页表项时(如调用pte_val),我们要作相反的转化,把MFN转化成PFN再给Guest OS。这样一来,对于Guest OS而言,就好像是在操作从VA到PA的页表一样。至于page fault,除了MMIO外,其它的情况基本可以直接丢给Guest OS处理,因为Guest OS的page fault handler中对页表的操作也是被截获掉的。
PVMMU的主要优点是效率高,因为它免除了Guest OS页表和Shadow页表间同步所引起的消耗。但它和SPT的共同缺点是需要修改Guest OS,即Guest OS知道自己被虚拟化了,我们称这种虚拟化方案为半虚拟化(Para-virtualization)。
Xen是这种PVMMU的主要使用者。
4. HAP(Hardware assisted paging)
在这种方案中,Guest完成VA到PA这第一层转化,硬件帮忙完成PA到MA这第二层转化。第二层转化对于Guest OS来说是透明的。Guest OS访存时做的事和在裸机上跑时一样,所以可以实现全虚拟化。这种特性Intel和AMD都有支持。Intel称之为Extended Page Tables (EPT),AMD称之为Nested Page Tables (NPT)。其优点是hypervisor省了很多活,缺点是需要硬件支持。
相关的实现代码可以参见Linux kernel中KVM的实现。