内存虚拟化的目的:
1.提供给虚拟机一个从0地址开始的连续物理内存空间;
2.在虚拟机之间有效隔离,调度以及共享内存资源。
客户机操作系统所维护的页表负责实现客户机虚拟地址(GVA)到客户机物理地址(GPA)的转化,而这个GPA地址是不能直接发送到系统总线上去 的。还需要实现GPA到宿主机虚拟地址(HVA),宿主机虚拟地址(HVA)到宿主机物理地址(HPA)的转化,总的转换关系如下:
GVA --> GPA --> HVA --> HPA
1. GVA到GPA的转化是由客户机的页表实现的;
2. GPA到HVA的转换关系是线性一一对应的。在创建一个虚拟机的时候回设置虚拟机的内存大小,内存的大小是在qemu中设置的。比如为虚拟机分配512M 的内存,首先qemu会调用mmap()函数申请一个512M的空间,返回一个userspace_addr,这个地址是宿主机系统分配的。另外qemu 还需要设置虚拟的客户机虚拟的起始地址guest_phys_addr和大小memory_size,如0~512M。这样 HVA = userspace_addr + (GPA - guest_phys_addr);
3. HVA到HPA的转换需要宿主机的页表来实现。其实就是Qemu进程的页表,因为是Qemu调用mmap函数申请的一段虚拟地址空间,当访问这段虚拟地址空间时会发生用户空间缺页异常,从而为Qemu进程的这段虚拟地址填补具体的页面。
客户机中的进程使用的页表是不能直接转载到MMU进行地址翻译的,因为它生成的地址是GPA。假如同一个宿主机中包含多个虚拟机,可能发生两个虚拟 机产生相同的GPA的现象,如果把这个GPA同时发送到系统总线中将产生无法预期的结果。不止多个虚拟机,GPA也有可能与宿主机页表产生的HPA相同, 这又遇到同样的问题。
针对这个问题,影子页表的出现实现了由GVA直接到HPA的地址翻译。另外,影子页表是这对于客户机中每一个进程的,也就是说客户机中的每一个进程的页表都存在一组影子页表与其相对应。
为什么是每个进程一组影子页表而不是每个虚拟机一个。在客户机进程运行时,载入物理CR3的是影子页目录的HPA,一个进程对一个GVA进程访问, 被MMU翻译成一个HPA,而另一进程也包含同样的GVA,如果同样使用该影子页表的话就会被MMU翻译到同一个HPA,这与进程间的地址是隔离的相冲 突,所以需要每个进程一个。
客户机页表的每一级都会占用客户机一个page的空间,对应到宿主机上就会占用相应的物理地址空间。而影子页表的每一级也会占用一个真实的物理页 面。客户机页表页与影子页表页通过哈希表相关联。但是客户机最后一级页表项和影子页表的最后一级页表项指向的是同一物理页面,只不过在客户机页表项中存放 的是该页面在客户机中的GPA,而影子页表项中存放的是该页面对应宿主机的HPA。
缺页异常的处理:
a) 一开始客户机页表项和影子页表项都为空;
b) 假如客户机中的程序对一个虚拟地址(GVA)进行读操作,那么客户机将使用GVA中的偏移量和CR3中保存的影子页表,进行一级级的查找,当查找到最后 一级页表项的时候页表项中的内容为空,无法找到下一级的物理页,产生page_fault。根据之前在VM-Excution域中Exception Bitmap异常位图中的设置,产生page_fault异常是触发VM-Exit。
c) 此时CPU退出到根模式执行VMM程序。VMM在“VM-EXIT INFORMATION FIELDS”中得到出错原因是EXIT_REASON_EXCEPTION_NMI,然后调用处理函数handle_exception(),然后读取 “VM-Exit Interruption-Information Field”得到是页异常导致。读取发生page_fault的虚拟地址GVA,然后调用kvm_mmu_page_fault()进行处理。
d) 最终page_fault 的处理函数是FNAME(page_fault)。首先调用FNAME(walk_addr),该函数使用发生page_fault是的GVA和客户机页 目录的基地址GPA,一级一级的找到最后一级页表项,然后看该页表项中的 P位是否为1,检查函数为FNAME(is_present_gpte)(pte)。 因为当前客户机和影子页表项都是空的,所以P位 是0,那么本函数跳到gotoerror 这个Lable处,函数返回0。
e) 这样就又回到FNAME(page_fault),FNAME(walk_addr)返回0,说明客户机页表有问题,这个问题是客户机的问题,需要客户 机自行处理。把在FNAME(walk_addr)函数中得到的出错信息注入到客户机中,然后FNAME(page_fault)返回,本次 page_fault处理结束。
f) 客户机调用客户机自己的page_fault处理函数,申请一个page,将page的GPA填充到客户机页表项中,同时将 P位置1,A和D位都是0。
然后继续之前的话题,客户机虚拟地址的访问。客户机仍使用GVA和物理CR3中保存的影子页表进行一级级的查找,因为刚才填充的客户机的页表项,而影子页表项没做处理,仍是空的,所以再次导致page_fault。
g) 这次进入page_fault处理函数FNAME(page_fault) 中仍然先执行客户机的页表行走函数FNAME(walk_addr),这次发现客户机页表项是有效的,因为之前设置过了,在行走函数 FNAME(walk_addr) 中还需要进行另一件事情就是 A位和 D位的设置,执行函数FNAME(update_accessed_dirty_bits)(vcpu, mmu, walker, write_fault)。
因为page_fault是读写操作造成的,所以无论是读还是写,在该函数中都会将 A位置1,而 D位 需要根据write_fault 标志来设置,如果是写操作导致的 page_fault,那么错误码的PFERR_WRITE_MASK 就会置位,从而该函数同时需要将客户机页表项的D位置1。
由于客户机的页表没有异常,所以FNAME(walk_addr) 返回1,那么接下来影子页表项为空的事情将继续由VMM来处理。
h) VMM遍历影子页表,当行走到最后一级页表项的时候,发现页表项为空,于是会将客户机物理页的gfn转化为pfn,将pfn填充到页表项中的addr部 分,并将P和A位置1。如果是一个写操作,而客户机的最后一级页表项中的 D位还没有置1,则还需要将页表项的R/W位清0,目的是将页面设置为只读,当发生写操作时捕捉 D位的修改。
客户机释放页面:
客户机释放一个页面后会将客户机中的页表项清空,同时执行INVLPG指令,将TLB中的该条映射失效,在VM运行控制域中可以进行设置,当客户机 执行INVLPG时产生VM-Exit。产生VM-Exit后VMM可以获取要失效的客户机虚拟地址,通过该虚拟地址和影子页表,可以找到对应的影子页表 项,找到后将该项清空。