内存虚拟化软件基础——KVM SPT

前言

  • kvm实现内存虚拟化,需要充分利用intel的硬件机制EPT。所谓EPT的硬件机制,就是开启EPT之后,当客户态的CPU访问物理地址(所谓的GPA,比如CR3或者页表项中存放的物理地址)时,会通过VMCS中的EPT pointer发起查找,在EPT页结构中查询对应的主机物理地址(HPA),访问过程中如果遇到某个页结构的条目不存在,会产生缺页异常。软件负责分配页表或者页结构。GPA到HPA地址转换如图:
    在这里插入图片描述
  • 具体转换过程如下图,EPT初始化时,软件只在VMCS中填入了EPT PML4 Table的物理地址,并未EPT PML4 Table分配了内存空间。当CPU访问的GPA落在PML4 Table其中一个条目包含的内存区间时,硬件需要根据这个条目中存放的主机物理地址去访问下一级的页结构EPT PDPT,但初始化时这张表是空的,条目肯定不存在,因此产生缺页异常,然后软件会负责分配EPT PDPT的内存并把地址填写到EPT PML4 Table的条目中。缺页异常处理完成后,硬件再次访问该条目,已然发现可以正常访问了。接着继续访问下一级的页结构直到最终找到HPA对应的主机物理页地址。这个过程中,中间层级的页结构表项被kvm称为非叶子页表项(nonleaf spte),最低一级的指向物理页的表项被kvm称为叶子页表项(leaf spte)。从上面的描述中可以确定三点:
  1. PML4 Table,PDPT,PDT,PT这些页结构,都是放在内存中的,它们由软件来分配管理。
  2. PML4 Table,PDPT,PDT,PT这些页结构的创建,都是软件在缺页异常的处理过程中完成的。
  3. 非叶子和叶子页表项的创建,都通过缺页异常触发。内核负责维护这些页结构。
    在这里插入图片描述
  • 本文主要介绍内核如何组织EPT的页结构。从形态上来看,每一级的页结构都包含若干页表项,每个页表项都指向一个内存页,最后一级的页表项指向真正的物理页,中间的指向一个存放页表的内存页。对于在EPT中存放页表的页结构,内核统一用一个概念来抽象,即SPT——Shadow Page Table。SPT中的每一个条目称为SPTE——Shadow Page Table Entry。为什么要用这么别扭的名字?因为这个数据结构不是为EPT页结构专门设计的,而是为影子页表设计的,影子页表中也需要维护一套页结构,EPT在实现上复用了这个结构。因此SPT可以在代码上兼容两种内存虚拟化加速的实现(影子页表和EPT),EPT在内核代码中还被称为tdp(两级页映射——two dimensional paging),用于区分传统的影子页表。

数据结构

SPT

  • 首先介绍EPT的页结构,就是前言中说的spt。从上图中可以知道,EPT有4级,最上面一级是Level-4(PML4 Table),然后依次时Level-3(PDPT),Level-2(PDT),Level-1(PT)。这些页结构,在kvm中都用kvm_mmu_page来表示,每张表的条目内容都存放下一级页表的物理地址。如下:
struct kvm_mmu_page {
	......
    gfn_t gfn;											/* 1 */
    union kvm_mmu_page_role role;						/* 2 */
    u64 *spt;											/* 3 */
	......
};
1. 这个页结构包含的虚机物理内存区域的起始页框号
2. 页结构在EPT页层级中的角色
3. 页结构的核心成员,指向一张物理页的基址,存放着512个页表项(sptes)
  • 我们知道,一个页结构就是一个4k大小的物理页,它的核心要素只有一个,就是物理页地址,在kvm_mmu_page中对应spt这个成员。那为什么一个页结构除了spt还有好些其它成员呢?这是因为管理需要,假设现在有两个4k大小的物理页,它们分别分配用来存放PML4 Table和PDPT这两个页结构,那硬件在查找HPA时应该先查找哪个表呢?显然应该先查PML4 Table,因为它的页表项包含PDPT的物理地址,这时候需要一个标签来表明页结构在EPT页表中的层级,用不同的角色来区分不同层级的页结构。这个标签kvm用kvm_mmu_page_role来表示,如下所示,所以一个页结构至少还应该包含这个role成员。除了层级不一样,其实每个页结构的页表项可能也有细微差别,比如指向物理页的页结构和指向spt的页结构。kvm_mmu_page可以看作是所有页结构的父类,不同层级的页结构继承这个父类的特性。
union kvm_mmu_page_role {
    unsigned word; 
    struct {
        unsigned level:4;					/* 4 */
        unsigned direct:1;					/* 5 */
        unsigned invalid:1;					/* 6 */
		......
    };      
};
4. spt在页结构中的层级,比如level=1就是最底层,每个条目指向一个物理页,level=4最上层,它的地址被VMCS的EPT pointer记录
5. 表明页表项是否直接指向了物理页
6. spt是否无效,如果被设置成无效,就不应该被使用了,因此稍后它可能被销毁

MMU

  • MMU(Memory Manager Unit),在普通情况下代指用来实现线性地址到物理地址转换的内存管理单元,输入是HVA,输出是HPA。在kvm内存虚拟化中,它用来代指实现客户机物理地址到主机物理地址转换的内存管理单元。当客户机需要访问一个GPA是,它将GPA送入mmu,mmu返回一个对应的HPA。kvm mmu的输入是GPA,输出是HPA。kvm中mmu的数据结构如下:
struct kvm_mmu {
	......
    int (*page_fault)(struct kvm_vcpu *vcpu, gva_t gva, u32 err,	/* 1 */
              bool prefault);
    hpa_t root_hpa;													/* 2 */
    u8 root_level;													/* 3 */
    bool direct_map;												/* 4 */			
	......
1. mmu需要实现GPA到HPA的转换,不只基于x86平台的EPT实现,也可以基于AMD平台的NPT,或者影子页表,因此mmu定义一套地址转换的通
用操作,具体平台有自己的实现,对于x86平台上,page_fault的实现就是tdp_page_fault
2. mmu实现地址转换依赖的当然是spt,因此它的结构里面必须包含spt的入口,这样通过mmu可以找到ept页结构的入口点。root_hpa就是这个入
口点,它存放的是一个物理页地址,这个物理页的内容是spt,root_hpa也可以看做是指向根spt的指针。
3. 当客户机采用不同的分页模式时,页结构的层级页各有不同,root_level表示的是最顶层的页结构是第几级
4. 区分mmu是基于影子页表实现还是基于内存硬件机制实现,direct_map为true时表示基于硬件(EPT/NPT)实现
  • 对于intel提供的EPT页表机制,kvm抽象出kvm_mmu和kvm_mmu_page数据结构来描述,如下图所示,intel手册中EPT的页结构就是这里的一个SPT,页结构里面的每个表项就是这里的spte。
    在这里插入图片描述

创建SPT

  • 当虚机访问物理地址时,硬件会通过VMCS的EPTP开始查整个EPT页表,如果访问的物理内存不存在,产生缺页异常——EPT violation,退出到内核态后由handle_ept_violation处理缺页异常。内核根据引发缺页的客户机物理地址页框号gfn找到它所在的spte,如果spte不在内存中,说明它指向的下一级页表还没有创建,这时就会创建SPT。
  • 从虚机内存缺页到SPT创建,整个流程如下:
vmx_handle_exit
	handle_ept_violation
		kvm_mmu_page_fault
			vcpu->arch.mmu.page_fault	<=>	tdp_page_fault
				__direct_map			
static int __direct_map(struct kvm_vcpu *vcpu, gpa_t v, int write,
            int map_writable, int level, gfn_t gfn, kvm_pfn_t pfn,
            bool prefault)
{
	......
	for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator) {
        if (iterator.level == level) {									/* 1 */
            mmu_set_spte(vcpu, iterator.sptep, ACC_ALL,					/* 2 */
                     write, &emulate, level, gfn, pfn,
                     prefault, map_writable);
        }
        
        if (!is_shadow_present_pte(*iterator.sptep)) {					/* 3 */
            u64 base_addr = iterator.addr;

            base_addr &= PT64_LVL_ADDR_MASK(iterator.level);			/* 4 */
            pseudo_gfn = base_addr >> PAGE_SHIFT;						
            sp = kvm_mmu_get_page(vcpu, pseudo_gfn, iterator.addr,		/* 5 */
                          iterator.level - 1,
                          1, ACC_ALL, iterator.sptep);

            link_shadow_page(iterator.sptep, sp);						/* 6 */
        }
    }
	......
1. 遍历vcpu的mmu所管理spt页表,如果要查找的gfn所在的层级与spt页表层级相同,说明找到了gfn所在的页表
2. 找到页表之后,根据gfn计算其在spt页表的索引,就可以设置gfn对应的页表项了,这个流程在后面分析
3. 如果页表遍历的层级和gfn所在页表层级不同,并且它的下一级页表没有了,需要分配内存创建页表了
4. 根据当前页表迭代器的起始地址计算页框号
5. 查找内存页表,如果没有就创建
6. 将找到的内存页表地址写入上一级的页表项中
  • kvm_mmu_get_page函数中有创建页表的关键动作:
kvm_mmu_get_page
	kvm_mmu_alloc_page
		sp = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_header_cache)		/* 7 */
		sp->spt = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_cache)		/* 8 */	
7. 从cache中分配kvm_mmu_page的内存
8. 分配kvm_mmu_page结构关键成员,页表spt
  • 页表分配完成后,需要将页表的地址填写到上级页表的页表项,以PDE举例,要填充的内容主要是物理地址和低12 bit的标志位:
    在这里插入图片描述
link_shadow_page(iterator.sptep, sp)
    spte = __pa(sp->spt) | shadow_present_mask | PT_WRITABLE_MASK |		/* 9 */
           	shadow_user_mask | shadow_x_mask | shadow_me_mask;
           	mmu_spte_set(sptep, spte)									/* 10 */
           		__set_spte(sptep, new_spte)
           			WRITE_ONCE(ssptep->spte_low, sspte.spte_low)
9. 将页表的物理地址取出,这里能够看到,SPT处于中间层级的页表项记录的就是主机内存的物理地址,使用全局默认标识设置处于中间层级表项的低12 bit的部分标志。
10.填充表项。
  • 以上是填充SPT页结构中间层级的页表项过程,对于最下一级指向物理页的页表项的填充,我们在下一节介绍。

设置SPT

  • 在缺页流程处理中,当发现gfn所在区间的页表不存在,就分配,然后继续遍历,直到最终找到gfn对应的页表项,之后会做两件事情,一是填充页表项,二是填充反向映射rmap内容。填充页表项可以建立GPA到HPA的映射,填充rmap数组内容可以建立gfn到对应页表项的映射。

填充SPTE

  • 填充最下一级的页表项和中间层级的页表项类似。流程如下:
mmu_set_spte
	set_spte
		mmu_spte_update
			mmu_spte_update_no_track
				mmu_spte_set
					__set_spte

增加rmap

  • 增加rmap的主要目的,就是建立gfn到页表项的映射,当页表项被填充以后,我们就建立了GPA到HPA的映射。CPU在Guest态写GPA地址时,就可以查找建立起来的页表,转换为物理地址。但当主机内存吃紧,需要将部分活动频率不高的物理页swap到磁盘时,如果这是物理页被虚机使用,需要根据页面的gfn号找到对应的页表项地址,然后更新页表内容,将present字段设置为不存在。
  • 这个rmap数组保存在kvm的slot数据结构中,如下:
struct kvm_memory_slot {
	......
	struct kvm_arch_memory_slot arch;
	......
};
struct kvm_arch_memory_slot {
	struct kvm_rmap_head *rmap[KVM_NR_PAGE_SIZES];
	......
}
struct kvm_rmap_head {
	unsigned long val;
};

在这里插入图片描述

mmu_set_spte
	rmap_add
		sp = sptep_to_sp(spte);
		kvm_mmu_page_set_gfn(sp, spte - sp->spt, gfn);
		rmap_head = gfn_to_rmap(vcpu->kvm, gfn, sp);
		pte_list_add(vcpu, spte, rmap_head);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值