KVM EPT页表

KVM在vcpu创建时创建和初始化MMU,

kvm_vm_ioctl
	kvm_vm_ioctl_create_vcpu
		kvm_arch_vcpu_create	
			kvm_mmu_create(vcpu)
					        vcpu->arch.mmu = &vcpu->arch.root_mmu;
       						vcpu->arch.walk_mmu = &vcpu->arch.root_mmu;
				__kvm_mmu_create
					        vcpu->arch.mmu->root_hpa = INVALID_PAGE;
       						vcpu->arch.mmu->root_pgd = 0;
			kvm_init_mmu
				init_kvm_tdp_mmu
					vcpu->arch.root_mmu->page_fault = kvm_tdp_page_fault;				     // EPT Voilation Handle的入口函数
					vcpu->arch.root_mmu->sync_page = nonpaging_sync_page;
					vcpu->arch.root_mmu->shadow_root_level = kvm_mmu_get_tdp_level(vcpu); // EPT页表的级数,系统默认为4
					vcpu->arch.root_mmu->direct_map = true; 							     // 直接映射使能,即使用EPT
					vcpu->arch.root_mmu->get_guest_pgd = get_cr3; 					     // 获取CR3的函数,get_cr3->kvm_read_cr3: vcpu->arch.cr3
 				        vcpu->arch.root_mmu->inject_page_fault = kvm_inject_page_fault;

进入vcpu之前的mmu设置,

kvm_vcpu_ioctl (kvm_vcpu_fops) {
    r = kvm_arch_vcpu_ioctl_run() {
        vcpu_run {
		    for(;;) {
               r = vcpu_enter_guest(vcpu) {
                 1, kvm_request_pending(vcpu)
						kvm_check_request & process 				//之前调用了kvm_make_request设置不同的request,在这里统一处理这些requests,例如中断注入:inject_pending_event(vcpu, &req_immediate_exit) -> kvm_x86_ops->set_irq(vcpu) = vmx_inject_irq(vcpu),通过写vmcs中的VM_ENTRY_INTR_INFO_FIELD字段(vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr))通知到guest,进入guest模式后,就立即处理中断 (https://www.binss.me/blog/qemu-note-of-interrupt/, https://blog.csdn.net/zgy666/article/details/105456569, https://blog.csdn.net/huang987246510/article/details/103397763, https://www.kernel.org/doc/html/latest/virt/kvm/vcpu-requests.html)
                 2, kvm_mmu_reload(vcpu)					        //guest的MMU初始化,设置vcpu->arch.mmu.root_hpa(指向EPT根页表的物理地址)
						kvm_mmu_load {
							mmu_alloc_roots {
								mmu_alloc_direct_roots {
									kvm_tdp_mmu_get_vcpu_root_hpa { //vcpu->kvm->arch.tdp_mmu_enabled为true时执行,即开启了EPT功能
										get_tdp_mmu_vcpu_root 
											1)kvm_mmu_get_root     //检查是否已经拥有了EPT根页表,并且页表的level等于vcpu->arch.mmu->shadow_root_level(EPT根页表所在的level,即Host Paging Structure中根目录的级别,通过kvm_mmu_get_tdp_level函数获得,如64位支持paging的系统可以支持level=4的页结构,
																 //关于EPT level 查看 commit 855feb6736
											2)alloc_tdp_mmu_page //找不到就分配新的页表kvm_mmu_page *root(内核用该结构表是一个页表页),其中root->spt 才是真正的页表页的地址,并给页表设置level(root->rool.level = vcpu->arch.mmu->shadow_root_level)
										return __pa(root->spt); 
									}
									vcpu->arch.mmu->root_hpa = root // 这里root就是上述代码获得的(kvm_mmu_page *)root->spt的物理地址,即真正页表页的物理地址
								}
							}
							kvm_mmu_sync_roots(vcpu)
							kvm_mmu_load_pgd(vcpu) {
								kvm_x86_ops.load_mmu_pgd(vcpu, root_hpa | vm_get_active_pcid(vcpu), ept_level) // 执行体系架构的load_mmu_pgd函数(intel: vmx_load_mmu_pgd), root_hpa = vcpu->arch.mmu->root_hpa,关于EPT level 查看 commit 855feb6736
									construct_eptp(vcpu, pgd, pgd_level) //返回EPTP,EPTP指向PML4的首地址( 在没有大页的情况下,一个gpa通过四级页表的寻址可得到spte获取相应的pfn,然后加上gpa最后12位的offset,得到hpa)
									vmcs_write64(EPT_POINTER, eptp)	  //EPTP写入VMCS的EPT_POINTER位置								
							}
							kvm_x86_ops.tlb_flush_current(vcpu)
						}
                  3, kvm_x86_ops.prepare_guest_switch(vcpu)
                  4, trace_kvm_entry(vcpu);
				  5, exit_fastpath = kvm_x86_ops.run(vcpu); 		  //体系架构相关的run操作(intel: vmx_vcpu_run())  RUN IN GUEST MODE
						vmx_vcpu_enter_exit						  
							 __vmx_vcpu_run(vmx, (unsigned long *)&vcpu->arch.regs,…)  // 进入 vmenter.S 后,就进入真正的入口 __vmx_vcpu_run,第二个参数 regs 就是由 KVM 保存的 guest 的通用寄存器
								call vmx_vmenter					// VM-Enter the current loaded VMCS。在调用 vmx_vmenter 进入 guest 之前,要做的就是保存 host 状态并加载由 KVM 保存的 guest 状态。首先将 host 寄存器压栈保存,然后从 vcpu->arch.regs 数组恢复 guest 寄存器。
																   // 另外就是 vmx->loaded_vmcs->lauched 变量,会决定 VM Entry 的方式是 vmresume 还是 vmlaunch,区别在于是否在同一物理 CPU 上运行。
								vmx_vmexit 						   // Handle a VMX VM-Exit。当 VM Exit 时,控制流会跳转到 vmx_vmexit,这是因为 VMCS 中的 host RIP 被设置到此处。(vmx_set_constant_host_state: vmcs_writel(HOST_RIP, (unsigned long)vmx_vmexit););
																   // 这里要做的就是在 VM Exit 后的第一时间填充覆盖 RSB,防御对 VM 的 Spectre-type 攻击。host 栈没变,之后的 ret 返回到 __vmx_vcpu_run 中继续执行,这里要做的就是保存 guest 状态并从栈中恢复 host 状态。
																   // 之后就返回继续执行 vmx_vcpu_run 的后半部分,根据 exit_reason 处理 VM Exit 等。(https://notes.caijiqhx.top/ucas/virtualization/vmcs/)
					  	trace_kvm_exit();						   //guest 退出时执行exit tracepoint
                  6, r = kvm_x86_ops.handle_exit(vcpu, exit_fastpath);//体系架构相关的handle_exit操作(intel: vmx_handle_exit),根据具体的退出原因进行处理,内核中处理或者退出到用户态处理 
						kvm_vmx_exit_handlers[exit_handler_index](vcpu); //根据exit_reason调用不同的vm_exit函数,函数返回1表示vm_exit被完全的处理了,可以返回guest继续;返回0表示需要用户态qemu/kvmtool继续处理
			   }
			   if (r < 0) break; 								   // 退回到用户态空间qemu/kvmtool进程中处理
			} 
		 }
	 }
	 trace_kvm_userspace_exit(vcpu->run->exit_reason, r);
}

每一个level的页表页都是用如下结构表示,

struct kvm_mmu_page {
        struct list_head link;  // 所有的页表结构都链在一起,便于遍历
        struct hlist_node hash_link; // hash表,便于查找
        struct list_head lpage_disallowed_link;
        
        bool unsync;		    // 用在最后一级页表页,用于判断该页的页表项是否与guest的翻译同步(即是否所有pte都和guest的tlb一致)
        u8 mmu_valid_gen;       // 该页表页的generation number,用于和全局的kvm->arch.mmu_valid_gen进行比较,比它小表示该页是invalid的;将kvm->arch.mmu_valid_gen加1,那么当前所有的MMU页结构都变成了invalid,可以快速碾压失效该页表页所管理的guest物理地址空间,
							    // 而处理掉页结构的过程可以留给后面的过程(如内存不够时)再处理,这样就可以加快这个过程;当mmu_valid_gen值达到最大时,可以调用kvm_mmu_invalidate_zap_all_pages手动废弃掉所有的MMU页结构。
        bool mmio_cached;
        bool lpage_disallowed; /* Can't be replaced by an equiv large page */

        union kvm_mmu_page_role role; // role.level: 表示页结构在EPT页层级中的level,例如level为1是最底层的页表,每个条目指向一个物理页,level为4表示最顶层页表(即EPTP,地址存储在VMCS的EPT pointer);
							    // 表示页表页内的表项通过多少个level的逻辑关系进行管理,当没有开启HOST大页的时候,level=1,就是一个HOST物理页面中有512个管理表项: 1=4k sptes, 2=2M sptes, 3=1G sptes …
							    // 在非hugepage情况下,
							    // tdp_level =1 时,一个host页面(4k)中的表项就可以管理 512 个 guest 物理页框(一个页表项用8字节表示)
							    // tdp_level =2 时,一个host页面(4k)中的表项就可以管理 512*512 = 262144 个 guest 物理页框
							    // tdp_level =3 时,一个host页面(4k)中的表项就可以管理 512*512 * 512 = 134217728个 guest物理页框
							    // … …
							    // 由函数alloc_tdp_mmu_page设置
        gfn_t gfn; 				//guest的物理地址页帧号
        
        u64 *spt; 			    //页结构,指向一个物理页的基址,存放着512个页表项(sptes);该页表页所处的level由 role.level 决定
        /* hold the gfn of each spte inside spt */
        gfn_t *gfns;
        int root_count;         // 用在第4级页表,标识有多少EPTP指向该级页表页,root_count只对eptp指向的mmu page有意义,其他mmu page都是0;  他的值表示该页表页被多少个vcpu作为根页结构;当为非0值时,该页表页不可以被回收(kvm_mmu_zap_oldest_mmu_pages)
							    // A counter keeping track of how many hardware registers (guest cr3 or pdptrs) are now pointing at the page. While this counter is nonzero, the page cannot be destroyed. See role.invalid.
							    // get_tdp_mmu_vcpu_root函数中申请新页表页时,设置初始值为1
        unsigned int unsync_children;
        struct kvm_rmap_head parent_ptes; /* rmap pointers to parent sptes */
        DECLARE_BITMAP(unsync_child_bitmap, 512);

        /* Number of writes since the last time traversal visited this page.  */
        atomic_t write_flooding_count;// 在写保护模式下,对于任何一个页的写都会导致KVM进行一次emulation。对于叶子节点(真正指向数据页的节点),可以使用unsync状态来保护频繁的写操作不会导致大量的emulation,但是对于非叶子节点(paging structure节点)则不行。
							    // 对于非叶子节点的写emulation会修改该域,如果写emulation非常频繁,KVM会unmap该页以避免过多的写emulation。
        
        bool tdp_mmu_page; 	    //表示是否为EPT页表页(在alloc_tdp_mmu_page中设置)  
};  

cr3保存了guest的页表基址地址,指向L4页表。初始情况下,guest cr3指向的guest物理页面为空页面。当使用影子页表时,每当guest切换进程设置cr3寄存器时,KVM需要捕获每一次guest设置cr3的操作(前提时KVM需要事先设置好VMCS的cr3 load-exiting位),导致VM_EXIT,KVM捕获此设置操作(VM_EXIT即EXIT_REASON_CR_ACCESS),然后KVM执行 handle_cr 设置 cr3 使其指向影子页表(每个 guest 进程一个影子页表,实现 GVA 到 HPA 的直接转换),

kvm_register_readl(vcpu, reg);
    kvm_x86_ops.cache_reg(vcpu, reg); or vcpu->arch.regs[reg]; 
kvm_set_cr3(vcpu, val) // vcpu->arch.cr3 = cr3;

但在使能EPT后,guest CR3不需要再指向影子页表,而是回到最初的样子,指向guest进程的页表。guest设置cr3时,KVM也不需要再捕捉guest设置cr3的操作,不需要陷入KVM。这样当guest访问页表导致缺页异常时,KVM不处理guest页表缺页的机制,不会导致VM Exit,由guest的缺页异常处理函数负责分配一个guest物理页面(GPA),将该页面物理地址回填,建立guest页表结构。但当guest需要读写页表中的内容时,将会使用EPT。GPA分为4部分,前9位指向EPT PML4(PML4基址在EPTP中),第二个9位指向EPT PDPT条目(PDPT的基址来自EPT PML4E),第三个9位指向EPT PD条目(PD的基址来自EPT  PDPTE),第四个9位指向EPT PT表中的一个条目(PT的基址来自EPT PDE),现在EPT PTE指向相应页面的HOST物理地址。步骤大致如下,

1,由于guest cr3给出的是GPA,CPU需要查EPT页表得到GPA后才可以做读写操作;
2,如果EPT页表中不存在cr3 GPA地址对应的查找项,则guest产生EPT Violation异后常由KVM来处理,KVM为其创建SPTE页表(KVM根据GPA查找mem slot,得到对应的HVA,进而得到HPA后填充到页表中);
3,guest得到了cr3的HPA后,根据GVA的第一个9位在cr3的基址(指向PML4页表)上偏移后找到L3页表(PDPT)的GPA;
4,如果PML4页表中不存在GVA第一个9位指向的表项,则为缺页,那么guest CPU产生Page Fault,直接交由Guest Kernel处理,注意这里不会产生VM-Exit;
5,获得L3页表项的GPA后,CPU同样查询EPT页表,过程和上面一样;
6,L2,L1页表的访问也是如此,直至找到最终于与GPA对应的HPA。

各级 EPT 页表项的内容如下,

references

Hypervisor From Scratch – Part 4: Address Translation Using Extended Page Table (EPT) - Sina & Shahriar's Blog

内存虚拟化软件基础——KVM SPT_享乐主的博客-CSDN博客

KVM建立虚拟机页表过程 - 代码先锋网 pt example

KVM实现客户机内存 - 代码天地

KVM MMU EPT内存管理

5. EPT机制 | 码农家园

http://ningfxkvm.blogspot.com/2015/11/kvmmmupage.html

Hypervisor From Scratch – Part 4: Address Translation Using Extended Page Table (EPT) - Sina & Shahriar's Blog

intel手册https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3c-part-3-manual.pdf

Documentation/virt/kvm/mmu.rst  


版权声明:本文为yiyeguzhou100原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。