首先感谢Intel OTC的 wufeng、OenHan、chenhe、ruanshuai给予的帮助和支持。其实我就是各位大牛的笔和嘴
问题综述
tdp_page_fault 函数是虚拟机发生 EPT voilation的处理函数,完成EPT表项的建立,最不好理解的就是gaddr变量、level变量、gfn变量的含义。本文就将根据主要部分,对各个变量的含义,作用,数据结构加以分析。在详细的分析代码之前,我们介绍一些背景知识,再对代码内容加以分析
一、地址空间
在虚拟化中,GUEST认为自己拥有从0开始的完整物理地址空间,叫做GUEST物理地址空间, GPA,GPA中含所有RAM、ROM或MMIO等,这些就是qemu中的address space;
将GUEST地址空间按照页面大小分割,每个单元叫做一个GUEST物理页框,每个页框有个编号,就像图书的页号一样,记为GFN。
与此同时HOST真正拥有物理地址空间(HPA),同样将其按照页框分割,每个页框的编号几位HFN或PFN。
当分配出一个真正的物理页面时,page的时候,page的起始地址可以用PFN表示;因为虚拟机真实的使用这个物理页面,GFN和PFN就一个转换关系,这就是影子页表(或EPT)完成的映射
二、影子页表逻辑结构
1. 在未开启大页的情况下,64位机器上,影子页表是4级结构,非叶子几点表的表项指向下一级页表基地址,如绿色所示;叶子节点表的表项指向一个真正的物理页面,如下图所示:
最开始的根表的级别是level = 4,其次是level = 3, 一直到level = 1的表,level1表的表项指向4K的真实物理页面(当然这里只是分配了HPA,还需要配合HOST上的PF分配真实物理页面);
level4表就是影子页表(EPT)的根表,其物理地址就被记录在VMCS的EPT Pointer中,每个VCPU都有一个EPT Pointer,也就是每个VCPU都有自己的MMU和一套页表。
gaddr就是发生EPT voilation的guest物理地址,GFN就是gaddr对应的页框号,转换公式如下 gfn = gaddr / PAGE_SIZE。gfn是1的倍数。位置关系如上图所示
2. 在开启大页的情况下,64位机器上,这里以2M大页,影子页表3级结构为例说明,如下图所示:
在这里,叶子表变成了level 2表;其他的同普通影子页表相同;
在2M大页的情况下,一个大页内可以包含512个小页。
在KVM的代码中通过KVM_PAGES_PER_HPAGE(level)宏可以获得,level代表了第几级页表是叶子节点页表。
/*
* level-1级叶子页表所管理的页面 是 level 级叶子页表所管理的页面大小的 2^9倍。
* KVM_HPAGE_GFN_SHIFT 表示以level级页表为叶子页表的情况下,所管理的页面是标准页面的多少倍
*/
#define KVM_HPAGE_GFN_SHIFT(x) (((x) - 1) * 9)
#define KVM_HPAGE_SHIFT(x) (PAGE_SHIFT + KVM_HPAGE_GFN_SHIFT(x)) /*基本页面大小是PAGE_SHIFT,KVM_HPAGE_GFN_SHIFT(x)是倍数关系*/
#define KVM_HPAGE_SIZE(x) (1UL << KVM_HPAGE_SHIFT(x)) /*当前所管理的页面大小 */
#define KVM_HPAGE_MASK(x) (~(KVM_HPAGE_SIZE(x) - 1))
#define KVM_PAGES_PER_HPAGE(x) (KVM_HPAGE_SIZE(x) / PAGE_SIZE) /*当前所管理的页面,是标准页面的多少倍*/
gaddr就是发生EPT voilation的guest物理地址, fn = gaddr / PAGE_SIZE,这里的FN是标准页面情况下,gaddr的页框号;如果一个大页面中含有512个标准页面的话,大页面的起始页框号就应该是512的整倍数,如0,512,1024等。对FN向下取元整就可以得到大页面的起始页帧号。如下图
举个例子,如果 fn = 513,则gfn = 512; 如果 fn = 511,gfn = 0;
转换为数学公式如下:
a = a & ~(b-1) ;就是A对B去元整,得到的是B的整倍数。 如 5 & ~(4-1) = 4; 5 & ~(8-1) = 0
这在我们的代码里面也是有体现的
gfn_t gfn = gpa >> PAGE_SHIFT;
......
level = mapping_level(vcpu, gfn);
gfn &= ~(KVM_PAGES_PER_HPAGE(level) - 1);
......
KVM_PAGES_PER_HPAGE(level) 按照上面的分析,就是大页是普通页面多少倍,也就是对其关系;那么这里计算出来的gfn就是按照大页倍数对齐后的起始gfn编号。
三,代码分析
tdp_page_fault的关键代码如下
static int tdp_page_fault(struct kvm_vcpu *vcpu, gva_t gpa, u32 error_code,
bool prefault)
{
gfn_t gfn = gpa >> PAGE_SHIFT;
......
force_pt_level = mapping_level_dirty_bitmap(vcpu, gfn);
if (likely(!force_pt_level)) {
level = mapping_level(vcpu, gfn);
gfn &= ~(KVM_PAGES_PER_HPAGE(level) - 1);
} else
level = PT_PAGE_TABLE_LEVEL;
if (fast_page_fault(vcpu, gpa, level, error_code))
return 0;
......
if (try_async_pf(vcpu, prefault, gfn, gpa, &pfn, write, &map_writable))
return 0;
......
/*调用__direct_map函数进行EPT映射*/
r = __direct_map(vcpu, gpa, write, map_writable,
level, gfn, pfn, prefault);
......
}
1. 首先根据gpa计算出标准页面情况下的gfn
2. 得到level,level就是影子页表中,level级页表作为叶子页表,其目录项指向页面(pfn)。
2. 根据gfn和gpa,在memslot中查找,得到qemu中分配页面的HVA,在通过__get_user_pages_fast得到这个HVA页面的pfn(当然这只是一个PFN,还需要PF完成真正页面的分配)
3. __direct_map 完成EPT页表的构造,并在最后一级页表项中将gfn同pfn映射起来
__direct_map 函数中关于gfn、level的内容将会在后面的文章进行介绍