阅读前,请先阅读前两篇文章
《tdp_page_fault 函数解析之level,gfn变量的含义》
感谢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在后面的文章后会进行讲解