__direct_map 函数解析之影子页表的构建

阅读前,请先阅读前两篇文章

《tdp_page_fault 函数解析之level,gfn变量的含义》

《KVM的vMMU相关数据结构及其影子页表关系分析》

感谢Intel OTC的 wufeng、OenHan、chenhe、ruanshua、社区的@heart2011给予的帮助和支持 微笑


本文分析__direct_map函数中关于构建影子页表的过程,着重关心gfn和level变量的含义


一、通过gaddr来索引shadow page table (EPT)

在64位的机器上,同样GPA也是使用64bit宽度,但是目前intel使用了其中的48bit作为物理地址使用。当进行EPT的索引的时候,就像native页表的方法一样,将GPA按照9、9、9、9、12的宽度进行分段,每一个段作为一级shadow page table的索引值。


index4用来索引level 4影子页表的中的SPTE,index3用来索引level3级影子页表中的SPTE,最后level 1级中的SPTE中的PFN加上offset就得到了最终的物理地址 HPA。

在KVM的代码中,通过下面的几个宏定义来计算索引index,注释的应该很清楚了

/*64bit物理地址的EPT页表索引计算宏定义*/
/*每级页表索引占用9bit*/
#define PT64_LEVEL_BITS 9

/*计算第level级索引的偏移量*/
#define PT64_LEVEL_SHIFT(level) \
		(PAGE_SHIFT + (level - 1) * PT64_LEVEL_BITS)

/*
 * 得到addr在level页表下的索引值。
 * >> PT64_LEVEL_SHIFT(level)       移动掉索引右边的部分
 * & ((1 << PT64_LEVEL_BITS) - 1)   屏蔽掉索引左边的部分,只取本级的9bit
 */
#define PT64_INDEX(address, level)\
	(((address) >> PT64_LEVEL_SHIFT(level)) & ((1 << PT64_LEVEL_BITS) - 1))


每级页表的页表项都会管理一定宽度的物理地址空间,比如level 1级页表的每个页表项就可以管理 1<< 12 大小的物理地址空间,这个宽度就是页大小

level 2 级页表的一个页表项管理地址空间的大小就是  1 << (12 + 9) ,即1 << (12 + (2  - 1) * 9) 

=>  1 << 12  + ( level - 1) * 9 

=>  1ULL << (PAGE_SHIFT + (((level) - 1) * PT64_LEVEL_BITS)

所以 level级页表的页表项管理地址空间大小 = 1ULL << (PAGE_SHIFT + (((level) - 1) * PT64_LEVEL_BITS)

每个level级页表的页表项内所管理的地址都是按照大小对其的。

掩码就是用于对其的位都是0,其余位都是1。 地址 & MASK,得到的就是该地址的level级页表的页表项所管理地址地址空间的起始地址,该地址的大小是同

 level级页表的页表项管理地址空间大小对齐的


KVM代码中有相应的宏来计算MASK,用来对gaddr进行取整操作

#define PT64_BASE_ADDR_MASK (((1ULL << 52) - 1) & ~(u64)(PAGE_SIZE-1))
#define PT64_DIR_BASE_ADDR_MASK \
	(PT64_BASE_ADDR_MASK & ~((1ULL << (PAGE_SHIFT + PT64_LEVEL_BITS)) - 1))
#define PT64_LVL_ADDR_MASK(level) \
	(PT64_BASE_ADDR_MASK & ~((1ULL << (PAGE_SHIFT + (((level) - 1) \
						* PT64_LEVEL_BITS))) - 1))
#define PT64_LVL_OFFSET_MASK(level) \
	(PT64_BASE_ADDR_MASK & ((1ULL << (PAGE_SHIFT + (((level) - 1) \
						* PT64_LEVEL_BITS))) - 1))


二、影子页表迭代器

通过kvm_shadow_walk_iterator 结构,可以对影子页表进行迭代,其结构如下

/*
 * 遍历影子页表时所用的迭代器
 */
struct kvm_shadow_walk_iterator {
	u64 addr; 		/* GPA,寻找的GuestOS的物理地址(物理页帧),即(u64)gfn << PAGE_SHIF */
	hpa_t shadow_addr;	/* 当前EPT根页表项的物理基地址 */
	/**
	 * sptep不是下一级页表的基地址,而是当前页表中药使用的表项,而该表项中含有下一级表项的基地址,
	 * 更新的时候,需要向sptep中填入下一级表项物理地址或HPA物理地址
	 */
	u64 *sptep; 		/* gaddr在当前正在遍历的页表中的页表项指针 */
	int level; 			/* 当前所处的页表级别 */
	unsigned index; 	/* gaddr在当前level页表中的索引*/
};

当gaddr发生ept violation的时候,使用kvm_shadow_walk_iterator完成影子页表(EPT)的遍历,逐级查找gaddr所对应的页表项EPT,最终索引到叶子页表中对应的页表项SPTE,从而得到gaddr对应的页框的pfn。

对于64bit且没开大页的服务器来说,遍历是从level4页表开始的,一直遍历到level1级页表。level 4  ~ level 2成为非叶子页表,level1称为叶子页表。

当开启大页的时候,就像原来OS的原理一样,由于一个页表项管理范围的扩大,叶子页表的级别会变大,如2M页面,那么在level = 2的时候就是叶子页表,其中的每个页表项管理2M空间;同理 开启1G大页的时候,level = 3 的页表就是叶子页表。

如果遍历期间,发现没有中间某级的页表,那么就会分配下一级页表,将该页表的基地址填入当前页表项之中,当前遍历到的页表用level表示当前遍历的level级页表,通过level可以从gaddr中得到level级页表中所用的index索引号,然后通过index索引号就得到了页表项spte,将其地址保存到 sptep中,然后就可以进行页表项的填写与更新了

在初始化的时候,将gaddr记录在 addr 成员中, shadow_addr 指向EPT的根页表的地址上。
遍历的时候,shadow_addr 指向当前遍历到的level级别页表,当处理完当前页表后,shadow_addr就指向了下一级页表的基地址。

上述的操作被KVM封装成了几个函数,如下

/*
 * 判断pte是否是大页的最后一级,如果PTE中的SZ位置位,停止向下的遍历
 */
static int is_large_pte(u64 pte)
{
	return pte & PT_PAGE_SIZE_MASK;
}

/*
 * 判断pte是否是影子页表的叶子页表页表项,返回1代表是最后一级页表的页表项
 */
static int is_last_spte(u64 pte, int level)
{
    /*
     * level = 1 可定是最后一个级别了,那么spte肯定是叶子页表页表项了,返回1
     */
	if (level == PT_PAGE_TABLE_LEVEL)
		return 1;
    /*
     * 如果大页的最后一级,也就是页表项的SZ位置1,说明是最后一级,停止查找,返回1
     */
	if (is_large_pte(pte))
		return 1;
	return 0;
}



/*
 * 负责初始化struct kvm_shadow_walk_iterator结构,准备遍历EPT页表
 * @addr 是发生 ept violation的guest物理地址(调用者已经进行了页面对齐,因为EPT映射都是整页面进行的)
 * @vcpu 发生EPT violation的VCPU
 * @iterator 迭代器
 */
static void shadow_walk_init(struct kvm_shadow_walk_iterator *iterator,
			     struct kvm_vcpu *vcpu, u64 addr)
{
	/* 把要索引的地址赋给addr */
	iterator->addr = addr;
	/* 初始化时,指向EPT Pointer的基地址 */
	iterator->shadow_addr = vcpu->arch.mmu.root_hpa;
    /*
     * 初始化 指向根页表(level = 4级)
     * 注意这里iterator->level是mmu.shadow_root_level 而不是  role.level
     */
	iterator->level = vcpu->arch.mmu.shadow_root_level;

    /*
     * 如果HOST上EPT是4级,但是guest页表小于4级,说明GUEST可能是32bit paging,或其他的映射方式
     * 这种是非直接映射
     * 从下一级 level = 3 开始
     */
	if (iterator->level == PT64_ROOT_LEVEL &&
	    vcpu->arch.mmu.root_level < PT64_ROOT_LEVEL &&
	    !vcpu->arch.mmu.direct_map)
		--iterator->level;

    /*PAE的情况,PDPT中只有4个PDPTP*/
	if (iterator->level == PT32E_ROOT_LEVEL) {
		iterator->shadow_addr
			= vcpu->arch.mmu.pae_root[(addr >> 30) & 3];
		iterator->shadow_addr &= PT64_BASE_ADDR_MASK;
		--iterator->level;
		if (!iterator->shadow_addr)
			iterator->level = 0;
	}
}

/*
 * 检查当前页表是否还需要遍历当前页表,当level < 1的时候,已经遍历完最后一个级别就不需要遍历了
 */
static bool shadow_walk_okay(struct kvm_shadow_walk_iterator *iterator)
{
	/*
	 * 当level < 1的时候,已经遍历完最后一个级别就不需要遍历了
     */
	if (iterator->level < PT_PAGE_TABLE_LEVEL)
		return false;

	/*
	 * 得到addr在当前level级页表中表项的索引值
     */
	iterator->index = SHADOW_PT_INDEX(iterator->addr, iterator->level);

    /*
     * shadow_addr 指向当前level级页表的基地址,通过偏移index距离得到对应的页表项,记录spte的地址到sptep中
	 * sptep不是下一级页表的基地址,而是当前页表中药使用的表项,而该表项中含有下一级表项的基地址,更新的时候,需要向sptep中填入下一级表项物理地址或HPA物理地址
     */
	iterator->sptep	= ((u64 *)__va(iterator->shadow_addr)) + iterator->index;
	return true;
}

/*
 * 处理完了当前级别页表,取得下一级页表。
 * 函数进入的时候, spte的值当前级别页表中使用的页表项的值
 */
static void __shadow_walk_next(struct kvm_shadow_walk_iterator *iterator,
			       u64 spte)
{
    /*
     * 如果当前页表项已经是叶子页表页表项,直接处理level = 0,以便在shadow_walk_okay中退出
     */
	if (is_last_spte(spte, iterator->level)) {
		iterator->level = 0;
		return;
	}

    /*
     * 不是最后一级页表的页表项的话
     * 从SPTE中提取出下一级影子页表的基地址,记录到shadow_addr,相当于 i--
     * 因为到了下一级页表,页表级别也就减少1。
     */
	iterator->shadow_addr = spte & PT64_BASE_ADDR_MASK;
	--iterator->level;
}


/*
 * 参见__shadow_walk_next
 */
static void shadow_walk_next(struct kvm_shadow_walk_iterator *iterator)
{
	return __shadow_walk_next(iterator, *iterator->sptep);
}



上面的代码逻辑简单到爆,直接看注释吧。那么如何遍历呢?KVM代码提供了简单的接口抽象

#define for_each_shadow_entry(_vcpu, _addr, _walker)    \
	for (shadow_walk_init(&(_walker), _vcpu, _addr);	\
	     shadow_walk_okay(&(_walker));			\
	     shadow_walk_next(&(_walker)))


另外,对于SPTE的设定,kvm中也提供了一些辅助函数

代码中提供了辅助函数,可以帮助我们来建立EPT页表

mmu_set_spte函数:用来设置影子页表项,这样就可以将PFN或者下一级页表的基地址填到SPTE中,当然,其中处理了复杂的addr的内容,刷新TLB、将spte加入gfn对应的ramp中

link_shadow_page函数:将新分配出来的下一级影子页表页的地址填写本级对应的SPTE中



三、__direct_map函数建立影子页表的过程

/*
 * 建立EPT页表结构,负责将GPA逐层添加到EPT页表中
 * @vcpu:   发生EPT VIOLATION的VCPU
 * @gfn:    缺页GUEST物理地址的GFN
 * @level:  叶子页表所在的level
 * @pfn:    gfn对应的HOST物理页的页框号
 */
static int __direct_map(struct kvm_vcpu *vcpu, gpa_t v, int write,
			int map_writable, int level, gfn_t gfn, pfn_t pfn,
			bool prefault)
{
......
	/*
	 * 遍历所有页表中addr对应的页表项spte
	 */

	for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator) {
		/*
		 * 叶子节点,直接映射真正的物理页面pfn
		 */
		if (iterator.level == level) {
			/*
			 * 若找到最终level的EPT页表项,调用mmu_set_spte将GPA添加进去, 
			 * 若为各级中间level的页表项,调用__set_spte将下一级物理地址添加进去
			 */
			mmu_set_spte(vcpu, iterator.sptep, ACC_ALL,
				     write, &emulate, level, gfn, pfn,
				     prefault, map_writable);
......
			break;
		}
	        /*
	         * 非叶子页表页表项,表项不存在,分配下一级页表,并链接到当前的spte中
	         */
		if (!is_shadow_present_pte(*iterator.sptep)) {

            		/*将addr对齐到level级页表页表项所管理的地址空间的起始地址上*/
			base_addr &= PT64_LVL_ADDR_MASK(iterator.level); 
            		/*将对齐后的gaddr转换为页帧号*/
			pseudo_gfn = base_addr >> PAGE_SHIFT;
			/*分配一个EPT页表页,即kvm_mmu_page结构*/
			sp = kvm_mmu_get_page(vcpu, pseudo_gfn, iterator.addr,
					      iterator.level - 1,
					      1, ACC_ALL, iterator.sptep);

			/*将新分配到的下一级页表页的基地址填充到当前页表项SPTE中*/
			link_shadow_page(iterator.sptep, sp);

		}
	}
......
}

有了前面讲的知识,这段代码是很好理解的,直接看注释吧。

在遍历gaddr对应的页表项的时候,如果下一级页表不存在的话,就通过kvm_mmu_get_page函数分配得到页表,并填充到当前的spte中。

关于kvm_mmu_get_page在后面的文章后会进行讲解


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值