EPT是对虚拟机访问内存的硬件加速,其功能和影子页表一样,就是建立gpa到hpa的直接映射,访问EPT中已经建立好映射的gpa地址不触发vmexit,同时也只需要做一次pagewalk,但是第一次访问还是会vmexit,借着这个机会在退出处理函数中把对应的EPT表项建立好,这个退出名字叫EPT_VIOLATION,处理函数是handle_ept_violation。
handle_ept_violation函数里面最终调用了tdp_page_fault来为EPT建立新的entry,这个函数其实复用了影子页表的page_fault函数nonpaging_page_fault很多代码(我的理解是,EPT和影子页表都完成同样的功能只不过一个是硬件来查表一个是软件查表,但是表还是那张表),其中vcpu->arch.mmu.root_hpa保存了EPT表的基地址或者保存影子页表基地址。它调用__direct_map来完成entry的建立,后者使用了宏for_each_shadow_entry来遍历EPT表项,再调用mmu_set_spte来真正的完成表项的设置。上述宏for_each_shadow_entry长这样,它用到了root_hpa,以此为根基一层层的遍历addr(gpa)对应的entry:
#define for_each_shadow_entry(_vcpu, _addr, _walker) \
for (shadow_walk_init(&(_walker), _vcpu, _addr); \
shadow_walk_okay(&(_walker)); \
shadow_walk_next(&(_walker)))
mmu_set_spte建立entry的时候,要分情况,对RAM和MMIO要区别对待,比如对qemu模拟的设备mmio引发的violation,就需要特殊处理:
先判断不是RAM:is_rmap_spte,即目前没有被标记为present的spte;
然后set_spte》set_mmio_spte
》is_noslot_pfn这里判断该pfn是否在memslot里面,这是另外一个故事。qemu为虚拟机准备的RAM都在memslot里面,mmio一般不属于,除非是直通设备的情况。(注:针对直通设备,qemu调用assigned_dev_register_regions 这个函数处理直通设备的mmio空间和pio空间,它把直通设备的mmio空间通过sysfs文件做mmap到qemu地址空间,然把这一段hva地址做成一个ram类型的新的memory region给注册给虚拟机,让虚拟机把这一段地址当作内存来处理,于是这一段地址也在memslot范围内。)
》mark_mmio_spte,这里为该spte打上mask:mask |= shadow_mmio_mask | access | gfn << PAGE_SHIFT;
当下次访问这个gfn时,发生ept violation,handle_ept_violation函数调用:
handle_mmio_page_fault》handle_mmio_page_fault_common
if (quickly_check_mmio_pf(vcpu, addr, direct))
return RET_MMIO_PF_EMULATE;
spte = walk_shadow_page_get_mmio_spte(vcpu, addr);
if (is_mmio_spte(spte)) {
gfn_t gfn = get_mmio_spte_gfn(spte);
unsigned access = get_mmio_spte_access(spte);
if (!check_mmio_spte(vcpu->kvm, spte))
return RET_MMIO_PF_INVALID;
if (direct)
addr = 0;
trace_handle_mmio_page_fault(addr, gfn, access);
vcpu_cache_mmio_info(vcpu, addr, gfn, access);
return RET_MMIO_PF_EMULATE;
从is_mmio_spte 开始看起,首先判断是否是mmio,如果是则获取gfn,访问权限等信息,保存到vcpu结构的相关域中,最后返回,这个返回最终会引发重退出,回到qemu中模拟。开始的两行是一个优化,上一次查询结构保存在上面说的vcpu结构中,所以可以尝试一下,万一匹配了呢!而且前后访问同一个mmio的gfn的情况应该是很普遍的,因为配置设备寄存器往往是一系列的动作。
为了满足强迫症病人的需求,我们再看一下这个tdp_page_fault是从哪里来的:
handle_ept_violation》kvm_mmu_page_fault》vcpu->arch.mmu.page_fault,最后这个函数是不同架构在初始化的时候注册的,拿x86来说具体如下:
int kvm_mmu_create(struct kvm_vcpu *vcpu)
{
ASSERT(vcpu);
vcpu->arch.walk_mmu = &vcpu->arch.mmu;
vcpu->arch.mmu.root_hpa = INVALID_PAGE;
vcpu->arch.mmu.translate_gpa = translate_gpa;
vcpu->arch.nested_mmu.translate_gpa = translate_nested_gpa;
return alloc_mmu_pages(vcpu);
}
上述函数在kvm_arch_vcpu_init里面调用,这个是从qemu初始化vcpu发起的动作,具体的:
kvm_vm_ioctl_create_vcpu》kvm_arch_vcpu_create》kvm_x86_ops->vcpu_create==vmx_create_vcpu》kvm_vcpu_init》kvm_arch_vcpu_init
接下来,这个系统调用进一步对vcpu进行设置:(tdp two-dimentional paging)
kvm_vm_ioctl_create_vcpu 》kvm_arch_vcpu_setup》kvm_mmu_setup》init_kvm_mmu》init_kvm_tdp_mmu
static int init_kvm_tdp_mmu(struct kvm_vcpu *vcpu)
{
struct kvm_mmu *context = vcpu->arch.walk_mmu;
context->base_role.word = 0;
context->new_cr3 = nonpaging_new_cr3;
context->page_fault = tdp_page_fault;
上面其实是针对tdp_enabled==true场景来分析的,如果为false,即没有EPT等硬件的支持,那么要走另一条路:
static void init_kvm_mmu(struct kvm_vcpu *vcpu)
{
if (mmu_is_nested(vcpu))
return init_kvm_nested_mmu(vcpu);
else if (tdp_enabled)
return init_kvm_tdp_mmu(vcpu);
else
return init_kvm_softmmu(vcpu);
}
init_kvm_softmmu》kvm_init_shadow_mmu》nonpaging_init_context》context->page_fault = nonpaging_page_fault;
nonpaging_page_fault里面同样调用了handle_mmio_page_fault,也通过调用nonpaging_map,间接地调用了__direct_map,而tdp_page_fault就是糅合了这些调用,重写了tdp版本的page_fault处理函数。