5. EPT机制


  • 1. 背景
  • 2. EPT地址转换机制
  • 3. MMU初始化
    • 3.1. 相关变量初始化: kvm模块初始化时
      • 3.1.1. 整体流程
      • 3.1.2. 代码分析
    • 3.2. MMU相关操作函数初始化: vcpu创建时
      • 3.2.1. 整体流程
      • 3.2.2. MMU的创建
      • 3.2.3. MMU的初始化: vcpu->arch.mmu
  • 4. SPT(影子页表/EPT页表)逻辑结构
    • 4.1. 未开启大页
    • 4.2. 开启大页
  • 5. 通过gaddr来索引shadow page table (EPT)
  • 6. 影子页表迭代器
  • 7. SPTE设定的辅助函数
  • 8. 虚拟机EPT根页表载入和CR3初始化
    • 8.1. 整体流程
    • 8.2. 代码分析
  • 9. EPT构建过程
    • 9.1. EPT页表的建立流程
      • 9.1.1. 整体流程
      • 9.1.2. EPT异常处理入口
      • 9.1.3. kvm_tdp_page_fault(): 建立页表项
        • 9.1.3.1. fast_page_fault(): 快速处理
        • 9.1.3.2. try_async_pf(): GPA到HPA的转换
          • 9.1.3.2.1. __gfn_to_hva_many(): 虚拟机页框号(gfn)得到qemu中分配的页面的HVA
          • 9.1.3.2.2. hva_to_pfn(): 主机虚拟地址(HVA)得到主机物理页框号(HPA)
        • 9.1.3.3. __direct_map(): 构建页表核心
          • 9.1.3.3.1. kvm_mmu_get_page: 创建页表页
            • 9.1.3.3.1.1. kvm_mmu_alloc_page(): 申请一个存放页表页
          • 9.1.3.3.2. link_shadow_page(): 将下级影子页表页与页表项关联起来
        • 9.1.3.4. kvm_tdp_page_fault 小结
  • 10. kvm_mmu_free_page(): 回收
  • 11. EPT vm-entry处理
    • 11.1. KVM_REQ_MMU_RELOAD
    • 11.2. KVM_REQ_MMU_SYNC
    • 11.3. KVM_REQ_TLB_FLUSH
  • 12. EPT VM-exit 处理
    • 12.1. CR3寄存器
    • 12.2. handle_ept_violation
  • 13. 逆向映射机制
    • 13.1. 两种逆向映射
    • 13.2. rmap逆向映射: gfn对应的spte
    • 13.3. rmap的用处
    • 13.4. rmap相关数据结构
    • 13.5. 初始化阶段: 注册各个slot时
    • 13.6. 建立阶段: 填充EPT
      • 13.6.1. gfn_to_rmap(): 获取gfn对应的rmap地址
      • 13.6.2. pte_list_add(): 设置rmap
  • 14. 参考

1. 背景

KVM并不负责物理页面的分配,而是qemu分配后对应的地址传递过来,然后KVM维护EPT。也就是说,在qemu进程建立页表后,EPT才会建立

在虚拟化环境下,intel CPU在处理器级别加入了对内存虚拟化的支持。即扩展页表EPT,而AMD也有类似的成为NPT。在此之前,内存虚拟化使用的一个重要技术为影子页表。

虚拟化环境下,虚拟机使用的是客户机虚拟地址GVA,而其本身页表机制只能把客户机的虚拟地址转换成客户机的物理地址也就是完成GVA->GPA的转换,但是GPA并不是被用来真正的访存,所以需要想办法把客户机的物理地址GPA转换成宿主机的物理地址HPA

在KVM机制下,客户系统运行在CPU的非根模式,透明的完成地址翻译,即对客户机而言,一条客户机虚拟地址经MMU翻译为客户机物理地址而返回。但实际过程则稍微复杂,因为每一条客户机物理地址也都是真实存在于物理内存上的,而虚拟机所在的地址空间并不是真实的内存物理地址空间,所以客户域下的GPA需要再经过某种地址翻译机制完成到HPA的转化,才能够取得真实物理内存单元中的内容。

KVM提供了两种地址翻译的机制,基于软件模拟的影子页表机制,以及基于硬件辅助的扩展页表机制(Intel的EPT,AMD的NPT)。

影子页表采用的是一步到位式,即完成客户机虚拟地址GVA宿主机物理地址HPA的转换,由VMM为每个客户机进程维护。本节对于影子页表不做过多描述,重点在于EPT。

内容第一部分根据intel手册分析EPT地址转换机制;第二部分借助于KVM源代码分析EPT构建过程。

2. EPT地址转换机制

具体见<系统虚拟化/处理器虚拟化技术>, 当然最权威是Intel手册

当一个逻辑CPU处于非根模式下运行客户机代码时,使用的地址是客户机虚拟地址,而访问这个虚拟地址时,同样会发生地址的转换,这里的转换还没有设计到VMM层,和正常的系统一样,这里依然是采用CR3作为基址,利用客户机页表进行地址转换,只是到这里虽然已经转换成物理地址,但是由于是客户机物理地址,不等同于宿主机的物理地址,所以并不能直接访问,需要借助于第二次的转换,也就是EPT的转换。注意EPT的维护有VMM维护,其转换过程由硬件完成,所以其比影子页表有更高的效率。

我们假设已经获取到了客户机的物理地址,下面分析下如何利用一个客户机的物理地址,通过EPT进行寻址。

2020-03-17-16-14-05.png

注意不管是32位客户机还是64位客户机,这里统一按照64位物理地址来寻址。EPT页表是4级页表,页表的大小仍然是一个页即4KB,但是一个表项是8个字节,所以一张表只能容纳512个表项,需要9位来定位具体的表项。客户机的物理地址使用低48位来完成这一工作。从上图可以看到,一个48位的客户机物理地址被分为5部分,前4部分按9位划分,最后12位作为页内偏移。当处于非根模式下的CPU使用客户机操作一个客户机虚拟地址时,首先使用客户机页表进行地址转换,得到客户机物理地址,然后CPU根据此物理地址查询EPT,在VMCS结构中有一个EPTP的指针,其中的12-51位指向EPT页表的一级目录即PML4 Table.这样根据客户机物理地址的首个9位就可以定位一个PML4 entry,一个PML4 entry理论上可以控制512GB的区域,这里不是重点,我们不在多说。PML4 entry的格式如下:

2020-03-17-16-14-17.png

1、其实这里我们只需要知道PML4 entry的12-51位记录下一级页表的地址,而这40位肯定是用不完的,根据CPU的架构,采取不同的位数,具体如下:

在Intel中使用MAXPHYADDR来表示最大的物理地址,我们可以通过CPUID的指令来获得处理支持的最大物理地址,然而这已经不在此次的讨论范围之内,我们需要知道的只是:

  • 当MAXPHYADDR 为36位,在Intel平台的桌面处理器上普遍实现了36位的最高物理地址值,也就是我们普通的个人计算机,可寻址64G空间;
  • 当MAXPHYADDR 为40位,在Inter的服务器产品和AMD 的平台上普遍实现40位的最高物理地址,可寻址达1TB;
  • 当MAXPHYADDR为52位,这是x64体系结构描述最高实现值,目前尚未有处理器实现。

而对下级表的物理地址的存储4K页面寻址遵循如下规则:

① 当MAXPHYADDR为52位时,上一级table entry的12~51位提供下一级table物理基地址的高40位,低12位补零,达到基地址在4K边界对齐;

② 当MAXPHYADDR为40位时,上一级table entry的12~39位提供下一级table物理基地址的高28位,此时40~51是保留位,必须置0,低12位补零,达到基地址在4K边界对齐;

③ 当MAXPHYADDR为36位时,上一级table entry的12~35位提供下一级table物理基地址的高24位,此时36~51是保留位,必须置0,低12位补零,达到基地址在4K边界对齐。

而MAXPHYADDR为36位正是普通32位机的PAE模式。

2、就这么定位为下一级的页表EPT Page-Directory-Pointer-Table ,根据客户物理地址的30-38位定位此页表中的一个表项EPT Page-Directory-Pointer-Table entry。注意这里如果该表项的第7位为1,该表项指向一个1G字节的page.为0,则指向下一级页表。下面我们只考虑的是指向页表的情况。

3、然后根据表项中的12-51位,继续往下定位到第三级页表EPT Page-Directory-Pointer-Table,在根据客户物理地址的21-29位来定位到一个EPT Page-Directory-Pointer-Table Entry。如果此entry的第7位为1,则表示该entry指向一个2M的page,为0就指向下一级页表。

4、根据entry的12-51位定位第四级页表EPT Page-Directory ,然后根据客户物理地址的12-20位定位一个PDE。

PDE的12-51位指向一个4K物理页面,最后根据客户物理地址的最低12位作为偏移,定位到具体的物理地址。

与影子页表的构成略有不同(影子页表项存储的是GVA->HPA的映射),基于硬件辅助的地址翻译采用二维的地址翻译结构(“two-dimensional”),如以下两图所示。客户系统维护自身的客户页表,即完成GVA->GPA的映射,而具体的每一条客户页表项、客户页目录项都是真实存储在物理内存中的,所以需要完成GPA->HPA的映射,定位到宿主物理地址空间,以获得物理内存单元中的值。EPT/NPT页表就负责维护GPA->HPA的映射,并且EPT/NPT页表是由处理器的MMU直接读取的,可以高效的实现地址翻译。总结之,所谓“二维”的地址翻译结构,即客户系统维护自己的页表,透明地进行地址翻译;VMM负责将客户机请求的GPA映射到宿主机的物理地址,到真实的内存单元中取值。

2020-04-12-14-25-32.png

一条完整的地址翻译流程为,处于非根模式的CPU加载客户进程的gCR3,由于gCR3是一条GPA,CPU需要通过查询EPT/NPT页表来实gCR3 GPA->HPA的转换。CPU MMU首先查询硬件的TLB,如果没有GPA到HPA的映射,在cache中查询EPT/NPT,若cache中未缓存,逐层向下层存储查询,最终获得gCR3所映射的物理地址单元内容,作为下一级客户页表的索引基址。如果还没有,CPU抛出EPT Violation,由VMM截获处理。根据GVA获得偏移,获得一条地址用于索引下一级页表,该地址为GPA,再由VCPU的MMU查询EPT/NPT,如此往复,最终获得客户机请求的客户页内容。假设客户机有m级页表,宿主机EPT/NPT有n级,在TLB均miss的最坏情况下,会产生m*n次内存访问,完成一次客户机的地址翻译。

2020-04-12-14-25-53.png

虽然影子页表与EPT/NPT的机制差距甚大,一个用于建立GVA->HPA的影子页表,另一个用于建立GPA->EPT的硬件寻址页表,但是在KVM层建立页表的过程却十分相似,所以在KVM中共用了建立页表的这部分代码。至于究竟建立的是什么页表,init_kvm_mmu()会根据EPT支持选项是否开启,选择使用哪种方式建立页表。

3. MMU初始化

3.1. 相关变量初始化: kvm模块初始化时

3.1.1. 整体流程

1
2
3
4
5
6
7
8
vmx_init()                               // 初始化入口
 ├─ kvm_init(KVM_GET_API_VERSION)        // 初始化KVM框架
 |   ├─ kvm_arch_init()                  // 架构相关初始化
 |   |   ├─ kvm_mmu_module_init()         // mmu模块初始化
 |   |   ├─ kvm_mmu_set_mask_ptes()       // shadow pte mask设置
 |   ├─ kvm_arch_hardware_setup()        //
 |   |   ├─ kvm_x86_ops->hardware_setup() //
 |   |   |  ├─ kvm_configure_mmu()        // 硬件判断和全局变量

3.1.2. 代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
int kvm_mmu_module_init(void)
{
        int ret = -ENOMEM;

        if (nx_huge_pages == -1)
                __set_nx_huge_pages(get_nx_auto_mode());

        /*
         * MMU roles use union aliasing which is, generally speaking, an
         * undefined behavior. However, we supposedly know how compilers behave
         * and the current status quo is unlikely to change. Guardians below are
         * supposed to let us know if the assumption becomes false.
         */
        BUILD_BUG_ON(sizeof(union kvm_mmu_page_role) != sizeof(u32));
        BUILD_BUG_ON(sizeof(union kvm_mmu_extended_role) != sizeof(u32));
        BUILD_BUG_ON(sizeof(union kvm_mmu_role) != sizeof(u64));

        kvm_mmu_reset_all_pte_masks();
        // mmio
        kvm_set_mmio_spte_mask();
        // 建立缓存, 用于反向映射
        pte_list_desc_cache = kmem_cache_create("pte_list_desc",
                                            sizeof(struct pte_list_desc),
                                            0, SLAB_ACCOUNT, NULL);
        if (!pte_list_desc_cache)
                goto out;
        // 建立缓存, 用于分配 struct kvm_mmu_page
        mmu_page_header_cache = kmem_cache_create("kvm_mmu_page_header",
                                                  sizeof(struct kvm_mmu_page),
                                                  0, SLAB_ACCOUNT, NULL);
        if (!mmu_page_header_cache)
                goto out;

        if (percpu_counter_init(&kvm_total_used_mmu_pages, 0, GFP_KERNEL))
                goto out;
        // 当系统内存回收时的回调函数
        ret = register_shrinker(&mmu_shrinker);
        if (ret)
                goto out;

        return 0;

out:
        mmu_destroy_caches();
        return ret;
}

hardware_setup时确认全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static __init int hardware_setup(void)
{
        ......
        if (!cpu_has_vmx_ept() ||
                !cpu_has_vmx_ept_4levels() ||
                !cpu_has_vmx_ept_mt_wb() ||
                !cpu_has_vmx_invept_global())
                enable_ept = 0;
        if (!enable_ept)
                // ept没有启用, 0
                ept_lpage_level = 0;
        else if (cpu_has_vmx_ept_1g_page())
                // 支持1G大页, 3
                ept_lpage_level = PT_PDPE_LEVEL;
        else if (cpu_has_vmx_ept_2m_page())
                // 支持2M大页, 2
                ept_lpage_level = PT_DIRECTORY_LEVEL;
        else
                // 4K页面, 1
                ept_lpage_level = PT_PAGE_TABLE_LEVEL;
        kvm_configure_mmu(enable_ept, ept_lpage_level);
        ......
}

void kvm_configure_mmu(bool enable_tdp, int tdp_page_level)
{
        tdp_enabled = enable_tdp;

        /*
         * max_page_level reflects the capabilities of KVM's MMU irrespective
         * of kernel support, e.g. KVM may be capable of using 1GB pages when
         * the kernel is not.  But, KVM never creates a page size greater than
         * what is used by the kernel for any given HVA, i.e. the kernel's
         * capabilities are ultimately consulted by kvm_mmu_hugepage_adjust().
         */
        if (tdp_enabled)
                // 1G大页, 3
                // 2M大页, 2
                // 4K页面, 1
                max_page_level = tdp_page_level;
        else if (boot_cpu_has(X86_FEATURE_GBPAGES))
                max_page_level = PT_PDPE_LEVEL;
        else
                max_page_level = PT_DIRECTORY_LEVEL;
}
EXPORT_SYMBOL_GPL(kvm_configure_mmu);

3.2. MMU相关操作函数初始化: vcpu创建时

3.2.1. 整体流程

KVM在vcpu创建时创建和初始化MMU,所以说KVM的MMU每个VCPU独有的(但是有一些是共享的内容,后面会说到)。

1
2
3
4
5
6
7
8
kvm_vm_ioctl()  // 虚拟机vm的ioctl入口
 ├─ kvm_vm_ioctl_create_vcpu()       // 创建vcpu的ioctl
 |   ├─ kvm_arch_vcpu_create()   // 初始化kvm_vcpu_arch
 |   |   |─ kvm_mmu_create(vcpu);   // 创建mmu
 |   |   |   ├─ alloc_mmu_pages(vcpu, &vcpu->arch.guest_mmu);  // 分配内存
 |   |   |   └─ alloc_mmu_pages(vcpu, &vcpu->arch.root_mmu);   // 分配内存
 |   |   |─ kvm_init_mmu(vcpu, false);   // 初始化mmu
 |   |   |   ├─ init_kvm_tdp_mmu(vcpu);  // 设置回调函数

3.2.2. MMU的创建

MMU的创建在kvm_vm_ioctl() --> kvm_vm_ioctl_create_vcpu() --> kvm_arch_vcpu_create() --> kvm_mmu_create()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int kvm_mmu_create(struct kvm_vcpu *vcpu)
{
        uint i;
        int ret;

        vcpu->arch.mmu = &vcpu->arch.root_mmu;
        vcpu->arch.walk_mmu = &vcpu->arch.root_mmu;
        // 存储paging structure中根目录的地址(如EPT中的EPTP), 这是HPA
        vcpu->arch.root_mmu.root_hpa = INVALID_PAGE;
        // 虚拟机本身页表的cr3地址, GPA
        vcpu->arch.root_mmu.root_cr3 = 0;
        vcpu->arch.root_mmu.translate_gpa = translate_gpa;
        for (i = 0; i < KVM_MMU_NUM_PREV_ROOTS; i++)
                vcpu->arch.root_mmu.prev_roots[i] = KVM_MMU_ROOT_INFO_INVALID;

        vcpu->arch.guest_mmu.root_hpa = INVALID_PAGE;
        vcpu->arch.guest_mmu.root_cr3 = 0;
        vcpu->arch.guest_mmu.translate_gpa = translate_gpa;
        for (i = 0; i < KVM_MMU_NUM_PREV_ROOTS; i++)
                vcpu->arch.guest_mmu.prev_roots[i] = KVM_MMU_ROOT_INFO_INVALID;

        vcpu->arch.nested_mmu.translate_gpa = translate_nested_gpa;
        // guest_mmu是嵌套的情况下, L1虚拟机mmu
        ret = alloc_mmu_pages(vcpu, &vcpu->arch.guest_mmu);
        if (ret)
                return ret;
        // 非嵌套情况下的虚拟机MMU
        ret = alloc_mmu_pages(vcpu, &vcpu->arch.root_mmu);
        if (ret)
                goto fail_allocate_root;

        return ret;
 fail_allocate_root:
        free_mmu_pages(&vcpu->arch.guest_mmu);
        return ret;
}

static int alloc_mmu_pages(struct kvm_vcpu *vcpu, struct kvm_mmu *mmu)
{
        struct page *page;
        int i;

        /*
         * When using PAE paging, the four PDPTEs are treated as 'root' pages,
         * while the PDP table is a per-vCPU construct that's allocated at MMU
         * creation.  When emulating 32-bit mode, cr3 is only 32 bits even on
         * x86_64.  Therefore we need to allocate the PDP table in the first
         * 4GB of memory, which happens to fit the DMA32 zone.  Except for
         * SVM's 32-bit NPT support, TDP paging doesn't use PAE paging and can
         * skip allocating the PDP table.
         */
        if (tdp_enabled && kvm_x86_ops->get_tdp_level(vcpu) > PT32E_ROOT_LEVEL)
                return 0;
        // 分配内存
        page = alloc_page(GFP_KERNEL_ACCOUNT | __GFP_DMA32);
        if (!page)
                return -ENOMEM;
        // 后面会用到
        mmu->pae_root = page_address(page);
        for (i = 0; i < 4; ++i)
                mmu->pae_root[i] = INVALID_PAGE;

        return 0;
}

该函数指定了arch.walk_mmu就是arch.mmu的地址,在KVM MMU相关的代码中经常会把arch.walk_mmuarch.mmu混用,在这里指定了他们其实是一回事。

3.2.3. MMU的初始化: vcpu->arch.mmu

kvm_vm_ioctl() --> kvm_vm_ioctl_create_vcpu() --> kvm_arch_vcpu_create() --> kvm_init_mmu():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// arch/x86/kvm/mmu/mmu.c
static int kvm_init_mmu(struct kvm_vcpu *vcpu)
{
    if (mmu_is_nested(vcpu))
        // 嵌套虚拟化
        return init_kvm_nested_mmu(vcpu);
    else if (tdp_enabled) // 是否支持EPT
        /*
        * EPT(Extended page table,Intel x86硬件提供的内存虚拟化技术)相关初始化
        * 主要是设置一些函数指针,其中比较重要的如缺页异常处理函数
        */
        return init_kvm_tdp_mmu(vcpu);
    else
        // 影子页表(软件实现内存虚拟化技术)相关初始化
        return init_kvm_softmmu(vcpu);
}

在支持EPT情况下, 调用init_kvm_tdp_mmu初始化MMU.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static void init_kvm_tdp_mmu(struct kvm_vcpu *vcpu)
{
        // vcpu->arch.mmu
        struct kvm_mmu *context = vcpu->arch.mmu;
        union kvm_mmu_role new_role =
                kvm_calc_tdp_mmu_root_page_role(vcpu, false);

        if (new_role.as_u64 == context->mmu_role.as_u64)
                return;

        context->mmu_role.as_u64 = new_role.as_u64;
        // page_fault 函数被初始化为 kvm_tdp_page_fault
        context->page_fault = kvm_tdp_page_fault;
        context->sync_page = nonpaging_sync_page;
        context->invlpg = nonpaging_invlpg;
        context->update_pte = nonpaging_update_pte;
        context->shadow_root_level = kvm_x86_ops->get_tdp_level(vcpu);
        context->direct_map = true;
        context->get_guest_pgd = get_cr3;
        context->get_pdptr = kvm_pdptr_read;
        context->inject_page_fault = kvm_inject_page_fault;

        if (!is_paging(vcpu)) {
                context->nx = false;
                context->gva_to_gpa = nonpaging_gva_to_gpa;
                // 没开启分页,
                context->root_level = 0;
        } else if (is_long_mode(vcpu)) {
                context->nx = is_nx(vcpu);
                // 64位: 5级页表/4级页表
                context->root_level = is_la57_mode(vcpu) ?
                                PT64_ROOT_5LEVEL : PT64_ROOT_4LEVEL;
                reset_rsvds_bits_mask(vcpu, context);
                context->gva_to_gpa = paging64_gva_to_gpa;
        } else if (is_pae(vcpu)) {
                context->nx = is_nx(vcpu);
                // 32位开启pae, 值为3: 3级页表
                context->root_level = PT32E_ROOT_LEVEL;
                reset_rsvds_bits_mask(vcpu, context);
                context->gva_to_gpa = paging64_gva_to_gpa;
        } else {
                context->nx = false;
                // 32位非pae, 值为2: 2级页表
                context->root_level = PT32_ROOT_LEVEL;
                reset_rsvds_bits_mask(vcpu, context);
                context->gva_to_gpa = paging32_gva_to_gpa;
        }

        update_permission_bitmask(vcpu, context, false);
        update_pkru_bitmask(vcpu, context, false);
        update_last_nonleaf_level(vcpu, context);
        reset_tdp_shadow_zero_bits_mask(vcpu, context);
}

所谓初始化就是填充 vcpu->arch.mmu 结构体,里面有很多回调函数都会用到

4. SPT(影子页表/EPT页表)逻辑结构

KVM在还没有EPT硬件支持的时候,采用的是影子页表(shadow page table)机制,为了和之前的代码兼容,在当前的实现中,EPT机制是在影子页表机制代码的基础上实现的,所以EPT里面的pte和之前一样被叫做 shadow pte

4.1. 未开启大页

未开启大页的情况下,64位机器上,影子页表4级结构,

  • 非叶子节点表表项指向下一级页表基地址,如绿色所示;
  • 叶子节点表表项指向一个真正的物理页面,如下图所示:

2020-03-30-15-43-27.png

最开始的根表级别level = 4,其次是level = 3, 一直到level = 1的表,level1表表项指向4K的真实物理页面(当然这里只是分配了HPA,还需要配合HOST上的PF分配真实物理页面);

level4表就是影子页表(EPT)的根表,其物理地址就被记录在VMCSEPT Pointer中,每个VCPU都有一个EPT Pointer,也就是每个VCPU!!! 都有自己的MMU!!!一套页表!!!

gaddr就是发生EPT voilationguest物理地址GFN就是gaddr对应的页框号,转换公式如下 gfn = gaddr / PAGE_SIZE。gfn是1的倍数。位置关系如上图所示

4.2. 开启大页

在开启大页的情况下,64位机器上,这里以2M大页,影子页表3级结构为例说明,如下图所示:

2020-03-30-15-47-18.png

在这里,叶子表变成了level 2表;其他的同普通影子页表相同;

2M大页的情况下,一个大页内可以包含512个小页。(一个2M大页是2^11KB大小, 一个标准页4K<2^2>, 所以 2^9 个)

一个大页包含的小页的数目, 在KVM的代码中通过KVM_PAGES_PER_HPAGE(level)宏可以获得,level代表了第几级页表!!!叶子节点页表!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// arch/x86/include/asm/kvm_host.h
/* KVM Hugepage definitions for x86 */
enum {
        // 4K页面, PT(页表)是最后一级表, 里面是页表项, 即指向页面, 1
        PT_PAGE_TABLE_LEVEL   = 1,
        // 2M大页, PDT(页目录表)是最后一级表, 里面是页目录项, 即指向页面, 2
        PT_DIRECTORY_LEVEL    = 2,
        // 1G大页, PDPT(页目录指针表)是最后一级表, 里面是页目录指针项, 即指向页面, 3
        PT_PDPE_LEVEL         = 3,
        /* set max level to the biggest one */
        PT_MAX_HUGEPAGE_LEVEL = PT_PDPE_LEVEL,
};
// 3
#define KVM_NR_PAGE_SIZES       (PT_MAX_HUGEPAGE_LEVEL - \
                                 PT_PAGE_TABLE_LEVEL + 1)

/*
 * level-1 级叶子页表所管理的页面是 level级叶子页表所管理的页面大小的 2^9倍。
 * KVM_HPAGE_GFN_SHIFT 表示以level级页表为叶子页表的情况下,所管理的页面是标准页面的多少倍
 */
#define KVM_HPAGE_GFN_SHIFT(x)  (((x) - 1) * 9)
/*基本页面大小是 PAGE_SHIFT,KVM_HPAGE_GFN_SHIFT(x)是倍数关系*/
#define KVM_HPAGE_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 voilationguest物理地址fn = gaddr / PAGE_SIZE,这里的FN标准页面情况下,gaddr的页框号;如果一个大页面中含有512个标准页面的话,大页面起始页框号就应该是512的整倍数,如0512, 1024等。对FN向下取元整!!! 就可以得到大页面的起始页帧号。如下图

2020-03-30-16-05-39.png

举个例子,如果 fn = 513,则gfn = 512; 如果 fn = 511gfn = 0;

转换为数学公式如下:

1
a = a & ~(b-1) ;

就是A对B去元整,得到的是B的整倍数。 如 5 & ~(4-1) = 4; 5 & ~(8-1) = 0

这在我们的代码里面也是有体现的

1
2
3
4
5
    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编号起始pfn是该页表页所管理GUEST物理地址空间的起始gfn,而不是发生缺页地址所在页面的gfn缺页gfn被管理的GEUST物理地址空间中的一个值.

假设level=2, 如果 gfn = 513,则base_gfn = 512; 如果 gfn = 511,base_gfn = 0;

2020-04-05-17-02-34.png

5. 通过gaddr来索引shadow page table (EPT)

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

2020-04-03-16-24-16.png

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*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 大小的物理地址空间,这个宽度就是页大小, 4KB

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

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进行取整操作

1
2
3
4
5
6
7
8
9
#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))

6. 影子页表迭代器

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

1
2
3
4
5
6
7
8
9
struct kvm_shadow_walk_iterator {
    u64 addr;//寻找的GuestOS的物理地址,即(u64)gfn << PAGE_SHIFT  
    hpa_t shadow_addr;// 指向下一个要找的EPT页表基地址
    // sptep不是下一级页表的基地址,而是当前页表中要使用的表项,而该表项中含有下一级表项的基地址
    // 更新的时候,需要向sptep中填入下一级表项物理地址或HPA物理地址
    u64 *sptep;//指向当前页表项地址, 表项值为0代表下一级为空
    int level;//当前查找所处的页表级别
    unsigned index;//对应于gaddr的表项在当前页表的索引
};

当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封装成了几个函数,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/*
 * 判断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代码提供了简单的接口抽象

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

2020-03-30-17-16-04.png

三个参数的含义

  • _vcpu:对应kvm_vcpu结构体的指针。
  • _addr:如果是ETP的话,是GuestOS的物理地址
  • _walker:游标

说白了其实就是一个for循环,只不过循环的三个部分由三个函数组成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static void shadow_walk_init(struct kvm_shadow_walk_iterator *iterator,
                             struct kvm_vcpu *vcpu, u64 addr)
{
        shadow_walk_init_using_root(iterator, vcpu, vcpu->arch.mmu->root_hpa,
                                    addr);
}

static void shadow_walk_init_using_root(struct kvm_shadow_walk_iterator *iterator,
                                        struct kvm_vcpu *vcpu, hpa_t root,
                                        u64 addr)
{
        // 要查找的虚拟机物理地址
        iterator->addr = addr;
        // 影子页表level4页表页物理地址, EPT下就是VMCS的EPT pointer
        iterator->shadow_addr = root;
        // 影子页表级数, EPT下是4
        iterator->level = vcpu->arch.mmu->shadow_root_level;

        if (iterator->level == PT64_ROOT_4LEVEL &&
            vcpu->arch.mmu->root_level < PT64_ROOT_4LEVEL &&
            !vcpu->arch.mmu->direct_map)
                --iterator->level;

        if (iterator->level == PT32E_ROOT_LEVEL) {
                /*
                 * prev_root is currently only used for 64-bit hosts. So only
                 * the active root_hpa is valid here.
                 */
                BUG_ON(root != vcpu->arch.mmu->root_hpa);

                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;
        }
}

初始化函数,即CPU拿到一个GPA,构建页表的第一步,构建最初始的kvm_shadow_walk_iterator

每一级的遍历通过一个kvm_shadow_walk_iterator进行,其中各个字段的意义已经注明,我们只需要明白初始状态

  • iterator.shadow_addr= vcpu->arch.mmu->root_hpa, 影子页表level4页表页物理地址,EPT情况下,该值就是VMCS的EPT_pointer, 指向EPT页表基地址,下一个要查找的页表就是当前vCPU的根页表目录
  • addr是GPA的客户物理地址, 要查找的虚拟机物理地址
  • level = vcpu->arch.mmu->shadow_root_level, 影子页表的级数,EPT情况下这个是4是当前iterator所处的级别,其值会随着一层一层的遍历递减。

然后看循环条件

1
2
3
4
5
6
7
8
9
10
11
static bool shadow_walk_okay(struct kvm_shadow_walk_iterator *iterator)
{
        // < 1
        if (iterator->level < PT_PAGE_TABLE_LEVEL)
                return false;
        // 虚拟机物理地址在相应level页表中的索引
        iterator->index = SHADOW_PT_INDEX(iterator->addr, iterator->level);
        // 获取GuestOS物理地址在EPT页表对应级别的页表项地址, 表项中会指向下一级页表的地址
        iterator->sptep = ((u64 *)__va(iterator->shadow_addr)) + iterator->index;
        return true;
}

循环的条件比较简单,就是判断是否循环到了最后一级的页表,即iterator.level < 1, 如果iterator.level递减到0,则本次页表构建过程也完毕了。

如果iterator还没有到最后一级,则需要设置iterator的index, 这是要寻找的addr本级页表中对应的页表项索引,然后设置iterator->sptep,指向下一级的页表页

处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void shadow_walk_next(struct kvm_shadow_walk_iterator *iterator)
{
        __shadow_walk_next(iterator, *iterator->sptep);
}

static void __shadow_walk_next(struct kvm_shadow_walk_iterator *iterator,
                               u64 spte)
{
        if (is_last_spte(spte, iterator->level)) {
                iterator->level = 0;
                return;
        }
        // 这个是下一级页表的物理地址
        iterator->shadow_addr = spte & PT64_BASE_ADDR_MASK;
        --iterator->level;
}

正如前面所说,此过程是往后移动iterator的过程,当level为PT_PAGE_TABLE_LEVEL1的时候,此时应该设置对应的页表指向对应的页了,而不需要再次往后遍历,所以,就直接return,否则,需要往后移动iterator,设置iteratorshadow_addr和level。

7. SPTE设定的辅助函数

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

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

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

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

8. 虚拟机EPT根页表载入和CR3初始化

8.1. 整体流程

虚拟机每一次运行,执行vcpu_enter_guest这个函数,载入ept页表

1
2
3
4
5
6
7
8
9
vcpu_enter_guest()
  kvm_mmu_reload(vcpu)
    kvm_mmu_load(vcpu);
      mmu_topup_memory_caches(vcpu); // 从缓存中分配vcpu->arch.mmu_pte_list_desc_cache 和 vcpu->arch.mmu_page_header_cache
      mmu_alloc_roots(vcpu); // 初始化根目录的页面
      kvm_mmu_sync_roots(vcpu);
      kvm_mmu_load_pgd(vcpu); // 加载pgd, 即cr3
        kvm_x86_ops->load_mmu_pgd(vcpu, vcpu->arch.mmu->root_hpa); // 调用 vmx_load_mmu_pgd()
      kvm_x86_ops->tlb_flush(vcpu, true);

8.2. 代码分析

调用kvm_mmu_load函数,设置客户CR3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int kvm_mmu_load(struct kvm_vcpu *vcpu)
{
        int r;
        // 从缓存中分配vcpu->arch.mmu_pte_list_desc_cache 和 vcpu->arch.mmu_page_header_cache
        r = mmu_topup_memory_caches(vcpu);
        if (r)
                goto out;
        r = mmu_alloc_roots(vcpu);
        kvm_mmu_sync_roots(vcpu);
        if (r)
                goto out;
        kvm_mmu_load_pgd(vcpu);
        kvm_x86_ops->tlb_flush(vcpu, true);
out:
        return r;
}
1
2
3
4
5
6
7
static int mmu_alloc_roots(struct kvm_vcpu *vcpu)
{
        if (vcpu->arch.mmu->direct_map)
                return mmu_alloc_direct_roots(vcpu);
        else
                return mmu_alloc_shadow_roots(vcpu);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static int mmu_alloc_direct_roots(struct kvm_vcpu *vcpu)
{
        struct kvm_mmu_page *sp;
        unsigned i;
        // 根据当前vcpu的分页模式建立 ept顶层页表的管理结构
        if (vcpu->arch.mmu->shadow_root_level >= PT64_ROOT_4LEVEL) {
                spin_lock(&vcpu->kvm->mmu_lock);
                if(make_mmu_pages_available(vcpu) < 0) {
                        spin_unlock(&vcpu->kvm->mmu_lock);
                        return -ENOSPC;
                }
                //为PML4T的地址
                sp = kvm_mmu_get_page(vcpu, 0, 0,
                                vcpu->arch.mmu->shadow_root_level, 1, ACC_ALL);
                ++sp->root_count;
                spin_unlock(&vcpu->kvm->mmu_lock);
                // root_hpa是影子页表level4页表页物理地址, 即EPTP
                // sp->spt指向影子页表页的地址
                vcpu->arch.mmu->root_hpa = __pa(sp->spt);
        } else if (vcpu->arch.mmu->shadow_root_level == PT32E_ROOT_LEVEL) {
                for (i = 0; i < 4; ++i) {
                        hpa_t root = vcpu->arch.mmu->pae_root[i];

                        MMU_WARN_ON(VALID_PAGE(root));
                        spin_lock(&vcpu->kvm->mmu_lock);
                        if (make_mmu_pages_available(vcpu) < 0) {
                                spin_unlock(&vcpu->kvm->mmu_lock);
                                return -ENOSPC;
                        }
                        // 32地址bit[31:30]只需要4项,所以在这里都分配出来,可以减少以后vm-exit次数
                        sp = kvm_mmu_get_page(vcpu, i << (30 - PAGE_SHIFT),
                                        i << 30, PT32_ROOT_LEVEL, 1, ACC_ALL);
                        root = __pa(sp->spt);
                        ++sp->root_count;
                        spin_unlock(&vcpu->kvm->mmu_lock);
                        vcpu->arch.mmu->pae_root[i] = root | PT_PRESENT_MASK;
                }
                vcpu->arch.mmu->root_hpa = __pa(vcpu->arch.mmu->pae_root);
        } else
                BUG();

        /* root_cr3 is ignored for direct MMUs. */
        vcpu->arch.mmu->root_cr3 = 0;

        return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void vmx_load_mmu_pgd(struct kvm_vcpu *vcpu, unsigned long cr3)
{
        struct kvm *kvm = vcpu->kvm;
        bool update_guest_cr3 = true;
        unsigned long guest_cr3;
        u64 eptp;
        // 初始状态, 传入的是 INVALID_PAGE
        guest_cr3 = cr3;
        // 开启ept
        if (enable_ept) {
                eptp = construct_eptp(vcpu, cr3);
                vmcs_write64(EPT_POINTER, eptp);

                if (kvm_x86_ops->tlb_remote_flush) {
                        spin_lock(&to_kvm_vmx(kvm)->ept_pointer_lock);
                        to_vmx(vcpu)->ept_pointer = eptp;
                        to_kvm_vmx(kvm)->ept_pointers_match
                                = EPT_POINTERS_CHECK;
                        spin_unlock(&to_kvm_vmx(kvm)->ept_pointer_lock);
                }

                /* Loading vmcs02.GUEST_CR3 is handled by nested VM-Enter. */
                // 嵌套情况下
                if (is_guest_mode(vcpu))
                        update_guest_cr3 = false;
                else if (!enable_unrestricted_guest && !is_paging(vcpu))
                        guest_cr3 = to_kvm_vmx(kvm)->ept_identity_map_addr;
                else if (test_bit(VCPU_EXREG_CR3, (ulong *)&vcpu->arch.regs_avail))
                        guest_cr3 = vcpu->arch.cr3;
                else /* vmcs01.GUEST_CR3 is already up-to-date. */
                        update_guest_cr3 = false;
                ept_load_pdptrs(vcpu);
        }
        // 写VMCS, 设置虚拟机的CR3寄存器
        if (update_guest_cr3)
                vmcs_writel(GUEST_CR3, guest_cr3);
}

9. EPT构建过程

与宿主机页表建立过程相似,EPT页表结构也是通过对缺页异常的处理完成的。Guset处在非根模式下运行,加载新的客户进程时,将VMCS客户域CR3的值加载到CR3寄存器(保存的是GPA),非根模式下的CPU根据EPT页表寻址该GPA->HPA的映射,EPT页表的基址在VMCS执行域的EPTP字段中保存。初始情况下客户进程页表该进程的父进程共享一套页表结构(COW机制),Guest CR3指向的客户物理页面为空页面,客户页表缺页,KVM采用不处理客户页表缺页的机制,虚拟机不退出,由客户机缺页异常处理函数负责分配一个客户物理页面,将该页面物理地址回填,建立客户页表结构完成该映射的过程需要将GPA翻译到HPA,此时该进程相应的EPT页表为空,产生EPT_VIOLATION异常,虚拟机退出到根模式下执行,由KVM捕获该异常,建立该GPA宿主物理地址HPA的映射,完成一套EPT页表的建立,中断返回,切换到非根模式继续运行。

VCPU的mmu查询下一级客户页表,根据GVA的偏移产生一条新的GPA,客户机寻址该GPA对应页面,产生客户缺页,不发生VM_Exit,由客户系统的缺页处理函数捕获该异常,从客户物理内存中选择一个空闲页,将该客户物理地址GPA回填给客户页表,此时该GPA对应的EPT页表项不存在,发生EPT_VIOLATION,切换到根模式下,由KVM负责建立该GPA->HPA映射,再切换回非根模式,如此往复,直到非根模式下GVA最后的偏移建立最后一级客户页表,分配GPA,缺页异常退出到根模式建立最后一套EPT页表。至此,一条GVA对应在真实物理内存单元中的内容,便可通过这一套二维页表结构获得。

下面借助于KVM源代码分析下EPT的构建过程,其构建模式和普通页表一样,属于中断触发式

初始页表是空的,只有在访问未命中的时候引发缺页中断,然后缺页处理程序构建页表

9.1. EPT页表的建立流程

9.1.1. 整体流程

  1. 初始情况下:Guest CR3指向的Guest物理页面为空页面;
  2. Guest页表缺页异常,KVM采用不处理Guest页表缺页的机制,不会导致VM Exit,由Guest的缺页异常处理函数负责分配一个Guest物理页面(GPA),将该页面物理地址回填,建立Guest页表结构
  3. 完成该映射的过程需要将GPA翻译到HPA,此时该进程相应的EPT页表为空,产生EPT_VIOLATION,虚拟机退出到根模式下执行,由KVM捕获该异常,建立该GPA到HOST物理地址HPA的映射,完成一套EPT页表的建立,中断返回,切换到非根模式继续运行。
  4. VCPU的mmu查询下一级Guest页表,根据GVA的偏移产生一条新的GPA,Guest寻址该GPA对应页面,产生Guest缺页不发生VM_Exit,由Guest系统的缺页处理函数捕获该异常,从Guest物理内存中选择一个空闲页,将该Guest物理地址GPA回填给Guest页表;
  5. 此时该GPA对应的EPT页表项不存在,发生EPT_VIOLATION,切换到根模式下,由KVM负责建立该GPA->HPA映射,再切换回非根模式;
  6. 如此往复,直到非根模式下GVA最后的偏移建立最后一级Guest页表,分配GPA,缺页异常退出到根模式建立最后一套EPT页表。
  7. 至此,一条GVA对应在真实物理内存单元中的内容,便可通过这一套二维页表结构获得。

注: EPTPEPT表项地址都是宿主机物理地址, 即HPA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
vmx_handle_exit()  // vmxexit的入口
 ├─ kvm_vmx_exit_handlers[exit_reason](vcpu);      // 调用对应函数
 |   ├─ handle_ept_violation()   // ept violation异常的处理
 |   |   |─ exit_qualification = vmcs_readl(EXIT_QUALIFICATION);   // 读取exit_qualification字段
 |   |   |─ gpa = vmcs_read64(GUEST_PHYSICAL_ADDRESS);   // 读取虚拟机的物理地址
 |   |   |─ error_code = XXXX;   // 拼凑error
 |   |   |─ kvm_mmu_page_fault(vcpu, gpa, error_code, NULL, 0);   // 处理page fault异常
 |   |   |   ├─ kvm_mmu_do_page_fault(vcpu, cr2_or_gpa, lower_32_bits(error_code), false); // 处理内存访问异常, mmio的见io部分
 |   |   |   |   └─ vcpu->arch.mmu->page_fault(vcpu, cr2_or_gpa, err, prefault);   // EPT下会调用kvm_tdp_page_fault
 |   |   |   |       └─ direct_page_fault(vcpu, gpa, error_code, prefault, max_level, true);   // EPT下会调用kvm_tdp_page_fault
 |   |   |   |           ├─ gfn = gpa >> PAGE_SHIFT;   // 虚拟机物理地址右移12位得到虚拟机物理页框号, 这是标准页面下的 GFN
 |   |   |   |           ├─ mmu_topup_memory_caches(vcpu);   // 分配缓存池
 |   |   |   |           ├─ fast_page_fault();   // 快速page fault处理
 |   |   |   |           ├─ try_async_pf();   // 根据gfn, 在memslots中查找, 得到pfn, 主机物理页框号, HPA
 |   |   |   |           |   ├─ slot = kvm_vcpu_gfn_to_memslot(vcpu, gfn);   // 得到该gfn(虚拟机页框号)对应的kvm_memory_slot
 |   |   |   |           |   └─ *pfn = __gfn_to_pfn_memslot();   // 得到该gfn(虚拟机页框号)对应的pfn(主机物理页框号), 即GPA到HPA的转换
 |   |   |   |           |        ├─ addr = __gfn_to_hva_many();   // 得到该gfn(虚拟机页框号)对应的qemu中分配的页面的HVA
 |   |   |   |           |        |   └─ return __gfn_to_hva_memslot();
 |   |   |   |           |        |       └─ return slot->userspace_addr + (gfn - slot->base_gfn) * PAGE_SIZE; // 这是gfn(虚拟机页框号)转换成主机虚拟地址(hva)
 |   |   |   |           |        └─ return hva_to_pfn(); // 得到这个主机虚拟地址(HVA)的主机物理页框号(HPA), 当然这只是一个PFN, 还需要PF完成真正页面的分配, 如果HVA对应的地址并不在内存中,还需要HOST自己处理缺页中断
 |   |   |   |           ├─ handle_abnormal_pfn();   // 处理反常的物理页
 |   |   |   |           └─ __direct_map();   // 完成EPT页表的构造,并在最后一级页表项中将gfn同pfn映射起来
 |   |   |   |               ├─ level = kvm_mmu_hugepage_adjust(vcpu, gfn, max_level, &pfn);   // 获取到该gfn对应的level, 基于vcpu->kvm->mm(qemu的mm_struct)
 |   |   |   |               ├─ for_each_shadow_entry(vcpu, gpa, it);   // 遍历EPT页表
 |   |   |   |               ├─ it.level == level: break;   //  如果页表的level等于请求的level, 表明是该entry引起的violation(说明到了叶子节点), 跳出
 |   |   |   |               ├─ sp = kvm_mmu_get_page();   // 对于非叶子节点, 如果该entry页表项值为0, 表明下一级页表页不存在, 则分配当前entry的下一级(level-1, 指向的页表)页表的kvm_mmu_page(sp)
 |   |   |   |               ├─ link_shadow_page(vcpu, it.sptep, sp);   // 非叶子结点, 下一级页表页不存在时, 将分配的下一级页表页链接到当前entry, 这是HPA
 |   |   |   |           |        ├─ mmu_spte_set(sptep, spte);   // 将sp处理后成为spte, 将其添加到当前entry(sptep), HPA
 |   |   |   |           |        └─ mmu_page_add_parent_pte(vcpu, sp, sptep); // 当前页表页(sp)会被多个上级页表项引用, 将所有上级页表项的parent_spte添加到当前页表页的patent_ptes链表中
 |   |   |   |           |   └─ mmu_set_spte();   // 设置最后一级页表项(即表项指向真正的页面), sptep指向pfn, HPA
 |   |   |   └─ x86_emulate_instruction(vcpu, cr2_or_gpa, emulation_type, insn, insn_len);   //

9.1.2. EPT异常处理入口

初始状态EPT页表为空,当客户机运行时,其使用的GVA转化成GPA后,还需要CPU根据GPA查找EPT,从而定位具体的HPA,但是由于此时EPT为空,所以会引发缺页中断,发生VM-exit, 此时CPU进入到根模式,运行VMM(这里指KVM),进入vmx_handle_exit函数, 在KVM中定义了一个异常处理数组来处理对应的VM-exit

1
2
3
4
5
6
static int (*kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = {
    ......
        [EXIT_REASON_EPT_VIOLATION]           = handle_ept_violation,
        [EXIT_REASON_EPT_MISCONFIG]           = handle_ept_misconfig,
    ......
};

Intel EPT相关的VMEXIT有两个:

  • EPT MisconfigurationEPT pte配置错误,具体情况参考Intel Manual 3C, 28.2.3.1 EPT Misconfigurations
  • EPT Violation:当guest VM访存出发到EPT相关的部分,在不产生EPT Misconfiguration的前提下,可能会产生EPT Violation,具体情况参考Intel Manual 3C, 28.2.3.2 EPT Violations

所以在发生EPT violation的时候,根据EXIT_REASON_EPT_VIOLATION走到handle_ept_violation函数, KVM中会执行handle_ept_violation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// arch/x86/kvm/vmx/vmx.c
static int handle_ept_violation(struct kvm_vcpu *vcpu)
{
        unsigned long exit_qualification;
        gpa_t gpa;
        u64 error_code;
        // 读取exit明细信息
        exit_qualification = vmcs_readl(EXIT_QUALIFICATION);

        /*
         * EPT violation happened while executing iret from NMI,
         * "blocked by NMI" bit has to be set before next VM entry.
         * There are errata that may cause this bit to not be set:
         * AAK134, BY25.
         */
        if (!(to_vmx(vcpu)->idt_vectoring_info & VECTORING_INFO_VALID_MASK) &&
                        enable_vnmi &&
                        (exit_qualification & INTR_INFO_UNBLOCK_NMI))
                vmcs_set_bits(GUEST_INTERRUPTIBILITY_INFO, GUEST_INTR_STATE_NMI);
        // 获取正在执行的虚拟机物理地址, GPA, 这就是产生异常的GPA
        gpa = vmcs_read64(GUEST_PHYSICAL_ADDRESS);
        trace_kvm_page_fault(gpa, exit_qualification);

        /* Is it a read fault? */
        error_code = (exit_qualification & EPT_VIOLATION_ACC_READ)
                     ? PFERR_USER_MASK : 0;
        /* Is it a write fault? */
        error_code |= (exit_qualification & EPT_VIOLATION_ACC_WRITE)
                      ? PFERR_WRITE_MASK : 0;
        /* Is it a fetch fault? */
        error_code |= (exit_qualification & EPT_VIOLATION_ACC_INSTR)
                      ? PFERR_FETCH_MASK : 0;
        /* ept page table entry is present? */
        error_code |= (exit_qualification &
                       (EPT_VIOLATION_READABLE | EPT_VIOLATION_WRITABLE |
                        EPT_VIOLATION_EXECUTABLE))
                      ? PFERR_PRESENT_MASK : 0;

        error_code |= (exit_qualification & 0x100) != 0 ?
               PFERR_GUEST_FINAL_MASK : PFERR_GUEST_PAGE_MASK;
        // exit明细信息
        vcpu->arch.exit_qualification = exit_qualification;
        return kvm_mmu_page_fault(vcpu, gpa, error_code, NULL, 0);
}

而该函数并没有做具体的工作,只是获取一下发生VM-exit的时候的一些状态信息如发生此VM-exit的时候正在执行的客户物理地址GPA退出原因等,然后作为参数继续往下传递,调用kvm_mmu_page_fault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// arch/x86/kvm/mmu/mmu.c
/*
 * Return values of handle_mmio_page_fault and mmu.page_fault:
 * RET_PF_RETRY: let CPU fault again on the address.
 * RET_PF_EMULATE: mmio page fault, emulate the instruction directly.
 *
 * For handle_mmio_page_fault only:
 * RET_PF_INVALID: the spte is invalid, let the real page fault path update it.
 */
enum {
        RET_PF_RETRY = 0,
        RET_PF_EMULATE = 1,
        RET_PF_INVALID = 2,
};

int kvm_mmu_page_fault(struct kvm_vcpu *vcpu, gpa_t cr2_or_gpa, u64 error_code,
                       void *insn, int insn_len)
{
        int r, emulation_type = EMULTYPE_PF;
        bool direct = vcpu->arch.mmu->direct_map;

        if (WARN_ON(!VALID_PAGE(vcpu->arch.mmu->root_hpa)))
                return RET_PF_RETRY;

        r = RET_PF_INVALID;
        // mmio 引起的, 其实从上面的路径走不到这里, 而是从 handle_ept_misconfig()过来
        // 关于 mmio , 见io部分
        if (unlikely(error_code & PFERR_RSVD_MASK)) {
                // mmio pagefault的处理
                r = handle_mmio_page_fault(vcpu, cr2_or_gpa, direct);
                // mmio page fault, 直接进行指令模拟
                if (r == RET_PF_EMULATE)
                        goto emulate;
        }
        // 处理内存访问异常
        if (r == RET_PF_INVALID) {
                // spte(影子页表项/EPT页表项)无效
                r = kvm_mmu_do_page_fault(vcpu, cr2_or_gpa,
                                          lower_32_bits(error_code), false);
                WARN_ON(r == RET_PF_INVALID);
        }
        // 再次fault
        if (r == RET_PF_RETRY)
                return 1;
        if (r < 0)
                return r;

        /*
         * Before emulating the instruction, check if the error code
         * was due to a RO violation while translating the guest page.
         * This can occur when using nested virtualization with nested
         * paging in both guests. If true, we simply unprotect the page
         * and resume the guest.
         */
        if (vcpu->arch.mmu->direct_map &&
            (error_code & PFERR_NESTED_GUEST_PAGE) == PFERR_NESTED_GUEST_PAGE) {
                kvm_mmu_unprotect_page(vcpu->kvm, gpa_to_gfn(cr2_or_gpa));
                return 1;
        }

        /*
         * vcpu->arch.mmu.page_fault returned RET_PF_EMULATE, but we can still
         * optimistically try to just unprotect the page and let the processor
         * re-execute the instruction that caused the page fault.  Do not allow
         * retrying MMIO emulation, as it's not only pointless but could also
         * cause us to enter an infinite loop because the processor will keep
         * faulting on the non-existent MMIO address.  Retrying an instruction
         * from a nested guest is also pointless and dangerous as we are only
         * explicitly shadowing L1's page tables, i.e. unprotecting something
         * for L1 isn't going to magically fix whatever issue cause L2 to fail.
         */
        if (!mmio_info_in_cache(vcpu, cr2_or_gpa, direct) && !is_guest_mode(vcpu))
                emulation_type |= EMULTYPE_ALLOW_RETRY_PF;
emulate:
        /*
         * On AMD platforms, under certain conditions insn_len may be zero on #NPF.
         * This can happen if a guest gets a page-fault on data access but the HW
         * table walker is not able to read the instruction page (e.g instruction
         * page is not present in memory). In those cases we simply restart the
         * guest, with the exception of AMD Erratum 1096 which is unrecoverable.
         */
        if (unlikely(insn && !insn_len)) {
                if (!kvm_x86_ops->need_emulation_on_page_fault(vcpu))
                        return 1;
        }

        return x86_emulate_instruction(vcpu, cr2_or_gpa, emulation_type, insn,
                                       insn_len);
}
EXPORT_SYMBOL_GPL(kvm_mmu_page_fault);

首先就判断本次exit是否是MMIO引起的,如果是,则调用handle_mmio_page_fault函数处理MMIO pagefault,具体为何这么判断可参考intel手册, mmio的fault处理见io部分.

针对SPTE invalid(shadow/EPT 页表项无效), 调用kvm_mmu_do_page_fault

1
2
3
4
5
6
7
8
9
static inline int kvm_mmu_do_page_fault(struct kvm_vcpu *vcpu, gpa_t cr2_or_gpa,
                                        u32 err, bool prefault)
{
#ifdef CONFIG_RETPOLINE
        if (likely(vcpu->arch.mmu->page_fault == kvm_tdp_page_fault))
                return kvm_tdp_page_fault(vcpu, cr2_or_gpa, err, prefault);
#endif
        return vcpu->arch.mmu->page_fault(vcpu, cr2_or_gpa, err, prefault);
}

这里就会调用上面初始化的kvm_tdp_page_fault(), 该函数用于完成EPT表项的建立.

9.1.3. kvm_tdp_page_fault(): 建立页表项

GFNPFN就一个转换关系,这就是影子页表(或EPT)完成的映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// arch/x86/kvm/mmu/mmu.c
int kvm_tdp_page_fault(struct kvm_vcpu *vcpu, gpa_t gpa, u32 error_code,
                       bool prefault)
{
        int max_level;
        // 从 3 开始, 大于1, 也就是3和2, 代表1G大页和2M大页
        for (max_level = PT_MAX_HUGEPAGE_LEVEL;
             max_level > PT_PAGE_TABLE_LEVEL;
             max_level--) {
                // 一个大页是一个普通页面多少倍大小
                // 1G大页是2^18倍; 2M大页是512倍
                int page_num = KVM_PAGES_PER_HPAGE(max_level);
                // gpa按照大页倍数对齐后的起始gfn编号
                gfn_t base = (gpa >> PAGE_SHIFT) & ~(page_num - 1);
                // 检查gfn(即base)范围一致性, 从而确定最大级别
                // 得到的max_level是叶子页表, 也就是其项指向页面的那一级??
                if (kvm_mtrr_check_gfn_range_consistency(vcpu, base, page_num))
                        break;
        }

        return direct_page_fault(vcpu, gpa, error_code, prefault,
                                 max_level, true);
}

调用direct_page_fault()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static int direct_page_fault(struct kvm_vcpu *vcpu, gpa_t gpa, u32 error_code,
                             bool prefault, int max_level, bool is_tdp)
{
        bool write = error_code & PFERR_WRITE_MASK;
        bool exec = error_code & PFERR_FETCH_MASK;
        bool lpage_disallowed = exec && is_nx_huge_page_enabled();
        bool map_writable;
        // 虚拟机物理地址右移12位得到虚拟机物理页框号(相对于虚拟机而言)
        // 标准页面情况下的 gfn
        gfn_t gfn = gpa >> PAGE_SHIFT;
        unsigned long mmu_seq;
        kvm_pfn_t pfn;
        int r;

        if (page_fault_handle_page_track(vcpu, error_code, gfn))
                return RET_PF_EMULATE;
        // 分配缓存池
        r = mmu_topup_memory_caches(vcpu);
        if (r)
                return r;
        // 大页不开, 那就是4K页面, max_level是最后一级(叶子页表, 也就是其项指向页面的那一级), 1
        if (lpage_disallowed)
                max_level = PT_PAGE_TABLE_LEVEL;
        // 快速处理 violation
        // 只有当GFN对应的物理页存在且violation是由读写操作引起的,才可以使用快速处理,因为这样不用加MMU-lock.
        if (fast_page_fault(vcpu, gpa, error_code))
                return RET_PF_RETRY;

        mmu_seq = vcpu->kvm->mmu_notifier_seq;
        smp_rmb();
        // 根据虚拟机物理页框号gfn和虚拟机物理地址gpa得到qemu中分配的页面的HVA, 然后得到这个HVA页面的pfn, 当然这只是个pfn, 还需要PF完成真正页面的分配
        // 得到pfn, 主机物理页框号
        if (try_async_pf(vcpu, prefault, gfn, gpa, &pfn, write, &map_writable))
                return RET_PF_RETRY;
        // 处理反常的物理页框
        if (handle_abnormal_pfn(vcpu, is_tdp ? 0 : gpa, gfn, pfn, ACC_ALL, &r))
                return r;

        r = RET_PF_RETRY;
        spin_lock(&vcpu->kvm->mmu_lock);
        if (mmu_notifier_retry(vcpu->kvm, mmu_seq))
                goto out_unlock;
        if (make_mmu_pages_available(vcpu) < 0)
                goto out_unlock;
        // 建立EPT页表结构
        r = __direct_map(vcpu, gpa, write, map_writable, max_level, pfn,
                         prefault, is_tdp && lpage_disallowed);

out_unlock:
        spin_unlock(&vcpu->kvm->mmu_lock);
        kvm_release_pfn_clean(pfn);
        return r;
}

调用mmu_topup_memory_caches函数进行缓存池的分配,官方的解释是为了避免在运行时分配空间失败,这里提前分配浩足额的空间,便于运行时使用。该部分内容最后单独详解。

接着调用了fast_page_fault尝试快速处理violation,只有当GFN对应的物理页存在且violation是由读写操作引起的,才可以使用快速处理,因为这样不用加MMU-lock.

假设这里不能快速处理,那么到后面就调用try_async_pf函数根据GFN获取对应的PFN,这个过程具体来说需要首先获取GFN对应的slot,转化成HVA,接着就是正常的HOST地址翻译的过程了,如果HVA对应的地址并不在内存中,还需要HOST自己处理缺页中断

9.1.3.1. fast_page_fault(): 快速处理

pass

9.1.3.2. try_async_pf(): GPA到HPA的转换

KVM并不负责物理页面的分配,而是qemu分配后对应的地址传递过来,然后KVM维护EPT。也就是说,在qemu进程建立页表后,EPT才会建立

1
2
3
4
5
6
7
try_async_pf();   // 根据gfn, 在memslots中查找, 得到pfn, 主机物理页框号, HPA
 ├─ slot = kvm_vcpu_gfn_to_memslot(vcpu, gfn);   // 得到该gfn(虚拟机页框号)对应的kvm_memory_slot
 └─ *pfn = __gfn_to_pfn_memslot();   // 得到该gfn(虚拟机页框号)对应的pfn(主机物理页框号), 即GPA到HPA的转换
      ├─ addr = __gfn_to_hva_many();   // 得到该gfn(虚拟机页框号)对应的qemu中分配的页面的HVA
      |   └─ return __gfn_to_hva_memslot();
      |       └─ return slot->userspace_addr + (gfn - slot->base_gfn) * PAGE_SIZE; // 这是gfn(虚拟机页框号)转换成主机虚拟地址(hva)
      └─ return hva_to_pfn(); // 得到这个主机虚拟地址(HVA)的主机物理页框号(HPA), 当然这只是一个PFN, 还需要PF完成真正页面的分配, 如果HVA对应的地址并不在内存中,还需要HOST自己处理缺页中断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static bool try_async_pf(struct kvm_vcpu *vcpu, bool prefault, gfn_t gfn,
                         gpa_t cr2_or_gpa, kvm_pfn_t *pfn, bool write,
                         bool *writable)
{
        struct kvm_memory_slot *slot;
        bool async;

        /*
         * Don't expose private memslots to L2.
         */
        if (is_guest_mode(vcpu) && !kvm_is_visible_gfn(vcpu->kvm, gfn)) {
                *pfn = KVM_PFN_NOSLOT;
                return false;
        }

        slot = kvm_vcpu_gfn_to_memslot(vcpu, gfn);
        async = false;
        *pfn = __gfn_to_pfn_memslot(slot, gfn, false, &async, write, writable);
        if (!async)
                return false; /* *pfn has correct page already */

        if (!prefault && kvm_can_do_async_pf(vcpu)) {
                trace_kvm_try_async_get_page(cr2_or_gpa, gfn);
                if (kvm_find_async_pf_gfn(vcpu, gfn)) {
                        trace_kvm_async_pf_doublefault(cr2_or_gpa, gfn);
                        kvm_make_request(KVM_REQ_APF_HALT, vcpu);
                        return true;
                } else if (kvm_arch_setup_async_pf(vcpu, cr2_or_gpa, gfn))
                        return true;
        }

        *pfn = __gfn_to_pfn_memslot(slot, gfn, false, NULL, write, writable);
        return false;
}

二者均调用了__gfn_to_pfn_memslot(struct kvm_memory_slot *slot, gfn_t gfn, bool atomic, bool *async,bool write_fault, bool *writable)函数,区别在于第四个参数bool *async,前者不为NULL,而后者为NULL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
kvm_pfn_t __gfn_to_pfn_memslot(struct kvm_memory_slot *slot, gfn_t gfn,
                               bool atomic, bool *async, bool write_fault,
                               bool *writable)
{
        unsigned long addr = __gfn_to_hva_many(slot, gfn, NULL, write_fault);

        if (addr == KVM_HVA_ERR_RO_BAD) {
                if (writable)
                        *writable = false;
                return KVM_PFN_ERR_RO_FAULT;
        }

        if (kvm_is_error_hva(addr)) {
                if (writable)
                        *writable = false;
                return KVM_PFN_NOSLOT;
        }

        /* Do not map writable pfn in the readonly memslot. */
        if (writable && memslot_is_readonly(slot)) {
                *writable = false;
                writable = NULL;
        }

        return hva_to_pfn(addr, atomic, async, write_fault,
                          writable);
}
EXPORT_SYMBOL_GPL(__gfn_to_pfn_memslot);

GPA到HPA的转化分两步完成,分别通过gfn_to_hvahva_to_pfn两个函数完成。

  • gfn_to_hva: 首先确定gpa对应的gfn映射到哪一个kvm_memory_slot,通过kvm_memory_slot做一个地址映射(实际就是做一个线性的地址偏移,偏移大小为(gfn - slot->base_gfn) * PAGE_SIZE),这样就得到了由gfn到hva的映射,实际获得的是GPA的客户物理页号到宿主虚拟地址的映射。
  • hva_to_pfn: 利用获得的gfn到hva的映射,完成宿主机上的虚拟地址(该虚拟地址为gfn对应的虚拟地址)到物理地址的转换,进而获得宿主机物理页框号pfn。此转换可能涉及宿主机物理页缺页!!!,需要请求分配该页!!!
9.1.3.2.1. __gfn_to_hva_many(): 虚拟机页框号(gfn)得到qemu中分配的页面的HVA
9.1.3.2.2. hva_to_pfn(): 主机虚拟地址(HVA)得到主机物理页框号(HPA)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*
 * Pin guest page in memory and return its pfn.
 * @addr: host virtual address which maps memory to the guest
 * @atomic: whether this function can sleep
 * @async: whether this function need to wait IO complete if the
 *         host page is not in the memory
 * @write_fault: whether we should get a writable host page
 * @writable: whether it allows to map a writable host page for !@write_fault
 *
 * The function will map a writable host page for these two cases:
 * 1): @write_fault = true
 * 2): @write_fault = false && @writable, @writable will tell the caller
 *     whether the mapping is writable.
 */
static kvm_pfn_t hva_to_pfn(unsigned long addr, bool atomic, bool *async,
                        bool write_fault, bool *writable)
{
        struct vm_area_struct *vma;
        kvm_pfn_t pfn = 0;
        int npages, r;

        /* we can do it either atomically or asynchronously, not both */
        /*这里二者不能同时为真*/
        BUG_ON(atomic && async);
        /*主要实现逻辑*/
        // 查tlb缓存
        if (hva_to_pfn_fast(addr, write_fault, writable, &pfn))
                return pfn;

        if (atomic)
                return KVM_PFN_ERR_FAULT;
        /*如果前面没有成功,则调用 hva_to_pfn_slow */
        //快表未命中,查内存页表
        npages = hva_to_pfn_slow(addr, async, write_fault, writable, &pfn);
        if (npages == 1)
                return pfn;

        down_read(&current->mm->mmap_sem);
        if (npages == -EHWPOISON ||
              (!async && check_user_page_hwpoison(addr))) {
                pfn = KVM_PFN_ERR_HWPOISON;
                goto exit;
        }

retry:
        vma = find_vma_interp(current->mm, addr, addr + 1);

        if (vma == NULL)
                pfn = KVM_PFN_ERR_FAULT;
        else if (vma->vm_flags & (VM_IO | VM_PFNMAP)) {
                r = hva_to_pfn_remapped(vma, addr, async, write_fault, writable, &pfn);
                if (r == -EAGAIN)
                        goto retry;
                if (r < 0)
                        pfn = KVM_PFN_ERR_FAULT;
        } else {
                if (async && vma_is_valid(vma, write_fault))
                        *async = true;
                pfn = KVM_PFN_ERR_FAULT;
        }
exit:
        up_read(&current->mm->mmap_sem);
        return pfn;
}

在本函数中涉及到两个重要函数hva_to_pfn_fasthva_to_pfn_slow,首选是前者,在前者失败后,调用后者。

hva_to_pfn_fast核心是调用了__get_user_pages_fast函数,而hva_to_pfn_slow函数的主体其实是get_user_pages_fast函数,可以看到这里两个函数就差了一个前缀,前者默认页表项已经存在,直接通过遍历页表得到对应的页框;而后者不做这种假设,如果有页表项没有建立,还需要建立页表项,物理页面没有分配就需要分配物理页面。考虑到这里是KVM,在开始EPT violation时候虚拟地址肯定没有分配具体的物理地址,所以这里调用后者的可能性比较大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/*
 * The fast path to get the writable pfn which will be stored in @pfn,
 * true indicates success, otherwise false is returned.  It's also the
 * only part that runs if we can in atomic context.
 */
static bool hva_to_pfn_fast(unsigned long addr, bool write_fault,
                            bool *writable, kvm_pfn_t *pfn)
{
        struct page *page[1];
        int npages;

        /*
         * Fast pin a writable pfn only if it is a write fault request
         * or the caller allows to map a writable pfn for a read fault
         * request.
         */
        if (!(write_fault || writable))
                return false;

        npages = __get_user_pages_fast(addr, 1, 1, page);
        if (npages == 1) {
                *pfn = page_to_pfn(page[0]);

                if (writable)
                        *writable = true;
                return true;
        }

        return false;
}

/*
 * The slow path to get the pfn of the specified host virtual address,
 * 1 indicates success, -errno is returned if error is detected.
 */
static int hva_to_pfn_slow(unsigned long addr, bool *async, bool write_fault,
                           bool *writable, kvm_pfn_t *pfn)
{
        unsigned int flags = FOLL_HWPOISON;
        struct page *page;
        int npages = 0;

        might_sleep();

        if (writable)
                *writable = write_fault;

        if (write_fault)
                flags |= FOLL_WRITE;
        if (async)
                flags |= FOLL_NOWAIT;

        npages = get_user_pages_unlocked(addr, 1, &page, flags);
        if (npages != 1)
                return npages;

        /* map read fault as writable if possible */
        if (unlikely(!write_fault) && writable) {
                struct page *wpage;

                if (__get_user_pages_fast(addr, 1, 1, &wpage) == 1) {
                        *writable = true;
                        put_page(page);
                        page = wpage;
                }
        }
        *pfn = page_to_pfn(page);
        return npages;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
 * Like get_user_pages_fast() except it's IRQ-safe in that it won't fall back to
 * the regular GUP.
 * Note a difference with get_user_pages_fast: this always returns the
 * number of pages pinned, 0 if no pages were pinned.
 *
 * If the architecture does not support this function, simply return with no
 * pages pinned.
 */
int __get_user_pages_fast(unsigned long start, int nr_pages, int write,
                          struct page **pages)
{
        unsigned long len, end;
        unsigned long flags;
        int nr = 0;

        start = untagged_addr(start) & PAGE_MASK;
        len = (unsigned long) nr_pages << PAGE_SHIFT;
        end = start + len;

        if (end <= start)
                return 0;
        if (unlikely(!access_ok((void __user *)start, len)))
                return 0;

        /*
         * Disable interrupts.  We use the nested form as we can already have
         * interrupts disabled by get_futex_key.
         *
         * With interrupts disabled, we block page table pages from being
         * freed from under us. See struct mmu_table_batch comments in
         * include/asm-generic/tlb.h for more details.
         *
         * We do not adopt an rcu_read_lock(.) here as we also want to
         * block IPIs that come from THPs splitting.
         */

        if (IS_ENABLED(CONFIG_HAVE_FAST_GUP) &&
            gup_fast_permitted(start, end)) {
                local_irq_save(flags);
                gup_pgd_range(start, end, write ? FOLL_WRITE : 0, pages, &nr);
                local_irq_restore(flags);
        }

        return nr;
}

函数开始获取虚拟页框号结束地址,在咱们分析的情况下,一般这里就是一个页面的大小。然后调用local_irq_disable禁止本地中断,开始遍历当前进程的页表。pgdp是在页目录表中的偏移+一级页表基址。进入while循环,获取二级表的基址,next在这里基本就是end了,因为前面申请的仅仅是一个页面的长度。可以看到这里如果表项内容为空,则goto到了slow,即要为其建立表项。这里暂且略过。先假设其存在,继续调用gup_pud_range函数。在x86架构下,使用的二级页表而在64位下使用四级页表。64位暂且不考虑,所以中间两层处理其实就是走个过场。这里直接把pgdp指针转成了pudp即pud_t类型的指针,接下来还是进行同样的工作,只不过接下来调用的是gup_pmd_range函数,该函数取出表项的内容,往下一级延伸,重点看其调用的gup_pte_range函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
static int gup_pte_range(pmd_t pmd, unsigned long addr, unsigned long end,
                         unsigned int flags, struct page **pages, int *nr)
{
        struct dev_pagemap *pgmap = NULL;
        int nr_start = *nr, ret = 0;
        pte_t *ptep, *ptem;

        ptem = ptep = pte_offset_map(&pmd, addr);
        do {
                pte_t pte = gup_get_pte(ptep);
                struct page *head, *page;

                /*
                 * Similar to the PMD case below, NUMA hinting must take slow
                 * path using the pte_protnone check.
                 */
                if (pte_protnone(pte))
                        goto pte_unmap;

                if (!pte_access_permitted(pte, flags & FOLL_WRITE))
                        goto pte_unmap;

                if (pte_devmap(pte)) {
                        if (unlikely(flags & FOLL_LONGTERM))
                                goto pte_unmap;

                        pgmap = get_dev_pagemap(pte_pfn(pte), pgmap);
                        if (unlikely(!pgmap)) {
                                undo_dev_pagemap(nr, nr_start, pages);
                                goto pte_unmap;
                        }
                } else if (pte_special(pte))
                        goto pte_unmap;

                VM_BUG_ON(!pfn_valid(pte_pfn(pte)));
                page = pte_page(pte);

                head = try_get_compound_head(page, 1);
                if (!head)
                        goto pte_unmap;

                if (unlikely(pte_val(pte) != pte_val(*ptep))) {
                        put_page(head);
                        goto pte_unmap;
                }

                VM_BUG_ON_PAGE(compound_head(page) != head, page);

                SetPageReferenced(page);
                pages[*nr] = page;
                (*nr)++;

        } while (ptep++, addr += PAGE_SIZE, addr != end);

        ret = 1;

pte_unmap:
        if (pgmap)
                put_dev_pagemap(pgmap);
        pte_unmap(ptem);
        return ret;
}

这里就根据pmd和虚拟地址的二级偏移,定位到二级页表项的地址ptep,在循环中,就取出ptep的内容,不出意外就是物理页面的地址及pfn,后面调用了page = pte_page(pte);实质是把pfn转成了page结构,然后设置参数中的page数组。没有错误就返回1. 上面就是整个页表遍历的过程。如果失败了,就为其维护页表分配物理页面,注意这里如果当初申请的是多个页面,就一并处理了,而不是一个页面一个页面的处理。实现的主体是get_user_pages函数,该函数是__get_user_pages函数的封装,__get_user_pages比较长,我们这里就不在介绍,感兴趣的朋友可以参考具体的代码或者其他资料。

9.1.3.3. __direct_map(): 构建页表核心

__direct_map()是构建页表的核心函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*
 * 建立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 gpa, int write,
                        int map_writable, int max_level, kvm_pfn_t pfn,
                        bool prefault, bool account_disallowed_nx_lpage)
{
        struct kvm_shadow_walk_iterator it;
        struct kvm_mmu_page *sp;
        int level, ret;
        // 虚拟机物理页帧号(这是全局唯一的, 以4K为一个页面)
        gfn_t gfn = gpa >> PAGE_SHIFT;
        gfn_t base_gfn = gfn;

        if (WARN_ON(!VALID_PAGE(vcpu->arch.mmu->root_hpa)))
                return RET_PF_RETRY;
        // 对level和gfn、pfn做出调整
        // level是gfn转换成hva, 然后得到这个hva映射的level(根据vcpu->kvm->mm, 这是qemu用户态进程的mm_struct)
        // 也就是请求的地址使用的level
        level = kvm_mmu_hugepage_adjust(vcpu, gfn, max_level, &pfn);

        trace_kvm_mmu_spte_requested(gpa, level, pfn);
        // 遍历所有页表中addr对应的页表项spte, 上面有详细说明
        for_each_shadow_entry(vcpu, gpa, it) {
                /*
                 * We cannot overwrite existing page tables with an NX
                 * large page, as the leaf could be executable.
                 */
                disallowed_hugepage_adjust(it, gfn, &pfn, &level);
                // 那么这里计算出来的gfn就是按照大页倍数对齐后的起始gfn编号, 具体见前面
                // 跳出循环时候, 这个就是最后一级页表项(即直接指向页面的那个)对应的页面起始gfn编号
                base_gfn = gfn & ~(KVM_PAGES_PER_HPAGE(it.level) - 1);
                // entry的level和请求的level相等, 说明该entry引起的violation,
                // 即该entry对应的下级页或者页表不在内存中,或者直接为NULL。
                // 这是整个循环跳出条件, 说明到了叶子节点
                /*
         * 叶子节点,直接映射真正的物理页面pfn
         */
                if (it.level == level)
                        break;
               
                /*
             * 非叶子页表页表项,表项不存在,分配下一级页表,并链接到当前的spte中
             */
                drop_large_spte(vcpu, it.sptep);
                /* 判断当前entry指向的页表是否存在,也就是缺页, 不存在的话需要建立 */
                if (!is_shadow_present_pte(*it.sptep)) {
                        // 当前entry指向的页表不存在
                        // 获取一个页, 得到的是下一级别 kvm_mmu_page(level-1)
                        sp = kvm_mmu_get_page(vcpu, base_gfn, it.addr,
                                              it.level - 1, true, ACC_ALL);
                        // 将新分配出来的下一级影子页表页的地址填写该entry对应的SPTE(it.sptep)中
                        link_shadow_page(vcpu, it.sptep, sp);
                        if (account_disallowed_nx_lpage)
                                account_huge_nx_page(vcpu->kvm, sp);
                }
        }
        // 叶子节点, 设置pte,page地址(pfn, HPA)写入到pte
        /*
        * 若找到最终level的EPT页表项,调用mmu_set_spte将GPA添加进去,
        * 若为各级中间level的页表项,调用__set_spte将下一级物理地址添加进去
        */
        ret = mmu_set_spte(vcpu, it.sptep, ACC_ALL,
                           write, level, base_gfn, pfn, prefault,
                           map_writable);
        direct_pte_prefetch(vcpu, it.sptep);
        ++vcpu->stat.pf_fixed;
        return ret;
}

先调用 kvm_mmu_hugepage_adjust() 对level和gfn、pfn做出调整, 里面会调用gfn_to_memslot_dirty_bitmap函数判断当前gfn对应的slot是否可用,当然绝大多数情况下是可用的。为什么要进行这样的判断呢?在if内部可以看到是获取level,如果当前GPN对应的slot可用,我们就可以获取分配slot的pagesize,然后得到最低级的level,比如如果是2M的页,那么level就为2,为4K的页level就为1.

base_gfn得到的是按照大页对齐后的虚拟机物理页帧号, 确保对应层级的偏移部分为0,如level=1,则baseaddr的低12位就清零, 也就是说如果是2M页面(level=2), 则应该是512的倍数(2M页面包含512个4K标准页); 1G页面应该是2^18的倍数. 也就是说base_gfn是该页表页所管理GUEST物理地址空间的起始gfn,而不是发生缺页地址所在页面的gfn。缺页gfn是被管理的GEUST物理地址空间中的一个值. 假设level=2, 如果 gfn = 513,则base_gfn = 512; 如果 gfn = 511,base_gfn = 0;

2020-04-05-17-02-34.png

for_each_shadow_entry,用于根据GFN遍历EPT页表的对应项,这点上面有详细解释。

循环中首先判断entry的level请求的level是否相等,

  • level相等说明到了叶子节点, 即该entry处引起的violation,即该entry对应下级页不在内存中,或者直接为NULL。 则会跳出,直接调用mmu_set_sptemmu_set_spte调用 set_spte设置spte, 这里设置了PFN, 属于HPA。

  • level不相等就说明是非叶子结点, 对非叶子节点, 判断当前页表项spte是否为0(即该页表项spte对应的下一级的页表项不存在), 就调用 kvm_mmu_get_page 获得一页新的mmu page,之后调用 link_shadow_page 调用 mmu_set_spte将其加入到EPT paging structure中(即设置当前页表项spte指向这个页表项页kvm_mmu_page, 这个是HPA); 将本次的parent_spte加入到反向链表中。如果存在直接向后遍历.

整个处理流程就是这样,根据GPA逐层查找EPT,最终level相等!!! (叶子节点, 直接映射真正的物理页面pfn)的时候,就根据最后一层的索引定位一个PTE该PTE应该指向的就是GFN对应的PFN,那么这时候set spte就可以了。最好的情况就是最后一级页表中的entry指向的物理页被换出外磁盘,这样只需要处理一次EPT violation,而如果在初始全部为空的状态下访问,每一级的页表都需要重新构建,则需要处理四次EPTviolation,发生4次VM-exit

9.1.3.3.1. kvm_mmu_get_page: 创建页表页

kvm_mmu_get_page函数是创建mmu页结构的函数,在该函数主要流程如下:

  1. 设置role
  2. 通过role和gfn在反向映射表中查找kvm mmu page,如果存在之前创建过的page,则返回该page
  3. 调用kvm_mmu_alloc_page创建新的struct kvm_mmu_page
  4. 调用hlist_add_head将该页加入到kvm->arch.mmu_page_hash哈希表中
  5. 调用init_shadow_page_table初始化对应的spt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
static struct kvm_mmu_page *kvm_mmu_get_page(struct kvm_vcpu *vcpu,
                                             gfn_t gfn,
                                             gva_t gaddr,
                                             unsigned level,
                                             int direct,
                                             unsigned int access)
{
        union kvm_mmu_page_role role;
        unsigned quadrant;
        struct kvm_mmu_page *sp;
        bool need_sync = false;
        bool flush = false;
        int collisions = 0;
        LIST_HEAD(invalid_list);
        // 设置该mmu page的属性
        /*设置角色,重要的是level,代表本页表页在影子页表中的级别*/
        role = vcpu->arch.mmu->mmu_role.base;
        role.level = level;
        role.direct = direct;
        if (role.direct)
                role.gpte_is_8_bytes = true;
        role.access = access;
        /* quadrant 对应页表项的索引,来自于GPA */
        if (!vcpu->arch.mmu->direct_map
            && vcpu->arch.mmu->root_level <= PT32_ROOT_LEVEL) {
                quadrant = gaddr >> (PAGE_SHIFT + (PT64_PT_BITS * level));
                quadrant &= (1 << ((PT32_PT_BITS - PT64_PT_BITS) * level)) - 1;
                role.quadrant = quadrant;
        }
        /* 根据gfn遍历KVM维护的可用的mmu_page_hash哈希链表 */
        /*
         * 查找页表页是否已经被其他上级的SPTE分配过了,如果是的话,就找到了,返回
         *
         */
        for_each_valid_sp(vcpu->kvm, sp, gfn) {
                if (sp->gfn != gfn) {
                        collisions++;
                        continue;
                }

                if (!need_sync && sp->unsync)
                        need_sync = true;

                if (sp->role.word != role.word)
                        continue;

                if (sp->unsync) {
                        /* The page is good, but __kvm_sync_page might still end
                         * up zapping it.  If so, break in order to rebuild it.
                         */
                        if (!__kvm_sync_page(vcpu, sp, &invalid_list))
                                break;

                        WARN_ON(!list_empty(&invalid_list));
                        kvm_make_request(KVM_REQ_TLB_FLUSH, vcpu);
                }
                // 记录该页表页中有多少个spte是unsync状态的
                if (sp->unsync_children)
                        kvm_make_request(KVM_REQ_MMU_SYNC, vcpu);

                __clear_sp_write_flooding_count(sp);
                trace_kvm_mmu_get_page(sp, false);
                // 如果能找到, 直接返回
                goto out;
        }
        /*如果根据页框号没有遍历到合适的page,就需要重新创建一个页*/
        ++vcpu->kvm->stat.mmu_cache_miss;
        // 分配
        sp = kvm_mmu_alloc_page(vcpu, direct);
        /* 设置所管理GUEST物理地址空间的起始gfn和level角色 */
        sp->gfn = gfn;
        sp->role = role;
        /* 以所管理GUEST物理地址空间的起始gfn为key,加入到hash表arch.mmu_page_hash中 */
        hlist_add_head(&sp->hash_link,
                &vcpu->kvm->arch.mmu_page_hash[kvm_page_table_hashfn(gfn)]);
        if (!direct) {
                /*
                 * we should do write protection before syncing pages
                 * otherwise the content of the synced shadow page may
                 * be inconsistent with guest page table.
                 */
                account_shadowed(vcpu->kvm, sp);
                if (level == PT_PAGE_TABLE_LEVEL &&
                      rmap_write_protect(vcpu, gfn))
                        kvm_flush_remote_tlbs_with_address(vcpu->kvm, gfn, 1);

                if (level > PT_PAGE_TABLE_LEVEL && need_sync)
                        flush |= kvm_sync_pages(vcpu, gfn, &invalid_list);
        }
        /*初始化清空页表页中所有的SPTE为0*/
        clear_page(sp->spt);
        trace_kvm_mmu_get_page(sp, true);

        kvm_mmu_flush_or_zap(vcpu, &invalid_list, false, flush);
out:
        if (collisions > vcpu->kvm->stat.max_mmu_page_hash_collisions)
                vcpu->kvm->stat.max_mmu_page_hash_collisions = collisions;
        return sp;
}

一个kvm_mmu_page对应于一个kvm_mmu_page_rolekvm_mmu_page_role记录对应page的各种属性

下面for_each_valid_sp是一个遍历链表的宏定义,KVM为了根据GFN查找对应的kvm_mmu_page,用一个HASH数组记录所有kvm_mmu_page每一个表项都是一个链表头,即根据起始GFN获取到的HASH值相同的,位于一个链表中。这也是HASH表处理冲突常见方法。

如果sp有unsync的子项,标记KVM_REQ_MMU_SYNC,下次vm-entry时处理,将指向sp的页框页标记为unsync状态。

如果在对应链表中找到一个合适的页,就直接利用该页,否则需要调用kvm_mmu_alloc_page函数重新申请一个页,主要是申请一个kvm_mmu_page结构和一个存放表项page,这就用到了之前我们说过的三种缓存,这里只用到了两个,分别是mmu_page_header_cachemmu_page_cache

这样分配好kvm_mmu_page结构后, 会用其管理GUEST物理地址空间起始GFN!!! (见上面)为key计算一个hash值,并上到哈希表vcpu->kvm->arch.mmu_page_hash[]上,以便可以快速的根据gfn找到管理该物理地址空间的页表页(即上面的for_each_valid_sp);作为一个节点加入到全局的HASH链表中,最后返回sp.

有个问题: 一个缺页gfn,既在level3页表页的管理空间内,也在level2页表页的管理空间内,那么gfn应该是哪个页表页的起始gfn呢?答案很简单,是level2的起始gfn。因为在__direct_map函数中,是从level4逐级往下处理的,当处理level3页表页的spte的时候,gfn为level3页表页管理GUEST物理地址空间的起始gfn,但发现已经有了指向level2页表页的基地址了,就不会调用kvm_mmu_get_page了;下一次循环的时候,gfn就被变更为level2页表页管理GUEST物理地址空间的起始gfn了,而spte中没有指向level1页表页基地址的指针,才会调用kvm_mmu_get_page,这时候传入的gfnlevel2页表页管理GUEST物理地址空间的起始gfn

9.1.3.3.1.1. kvm_mmu_alloc_page(): 申请一个存放页表页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static struct kvm_mmu_page *kvm_mmu_alloc_page(struct kvm_vcpu *vcpu, int direct)
{
        struct kvm_mmu_page *sp;

        sp = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_header_cache);
        //  spt就是页框,用mmu_memory_cache_alloc(&vcpu->arch.mmu_page_cache)分配
        sp->spt = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_cache);
        // gfn_t *gfns 用于影子页表的case
        if (!direct)
                sp->gfns = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_cache);
        // SPT/EPT页的struct page结构中page->private域会反向指向该struct kvm_mmu_page
        set_page_private(virt_to_page(sp->spt), (unsigned long)sp);

        /*
         * active_mmu_pages must be a FIFO list, as kvm_zap_obsolete_pages()
         * depends on valid pages being added to the head of the list.  See
         * comments in kvm_zap_obsolete_pages().
         */
        // 初始化有效版本号, 初始化等于vcpu->kvm->arch.mmu_valid_gen
        sp->mmu_valid_gen = vcpu->kvm->arch.mmu_valid_gen;
        // 加入链表, 以便后面快速的释放内存
        list_add(&sp->link, &vcpu->kvm->arch.active_mmu_pages);
        kvm_mod_used_mmu_pages(vcpu->kvm, +1);
        return sp;
}

这样看每次缺页都会分配新的mmu page,虚拟机每次启动是根据guest不停的进行EXIT_REASON_EPT_VIOLATION,整个页表就建立起来了。

9.1.3.3.2. link_shadow_page(): 将下级影子页表页与页表项关联起来

将新分配出来的下一级影子页表页的地址填写该entry对应的SPTE(it.sptep)中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
static void link_shadow_page(struct kvm_vcpu *vcpu, u64 *sptep,
                             struct kvm_mmu_page *sp)
{
        u64 spte;

        BUILD_BUG_ON(VMX_EPT_WRITABLE_MASK != PT_WRITABLE_MASK);

        spte = __pa(sp->spt) | shadow_present_mask | PT_WRITABLE_MASK |
               shadow_user_mask | shadow_x_mask | shadow_me_mask;

        if (sp_ad_disabled(sp))
                spte |= SPTE_AD_DISABLED_MASK;
        else
                spte |= shadow_accessed_mask;
        // 设置页表项(sptep)指向下一级页表页(spte)
        mmu_spte_set(sptep, spte);
        // 将上级页表项的parent_spte添加到当前页表页(sp)的patent_ptes链表中
        mmu_page_add_parent_pte(vcpu, sp, sptep);

        if (sp->unsync_children || sp->unsync)
                mark_unsync(sptep);
}

static void mmu_page_add_parent_pte(struct kvm_vcpu *vcpu,
                                    struct kvm_mmu_page *sp, u64 *parent_pte)
{
        // 当parent_pte不存在时,直接返回
        if (!parent_pte)
                return;

        pte_list_add(vcpu, parent_pte, &sp->parent_ptes);
}

/*
 * About rmap_head encoding:
 *
 * If the bit zero of rmap_head->val is clear, then it points to the only spte
 * in this rmap chain. Otherwise, (rmap_head->val & ~1) points to a struct
 * pte_list_desc containing more mappings.
 */

/*
 * Returns the number of pointers in the rmap chain, not counting the new one.
 */
static int pte_list_add(struct kvm_vcpu *vcpu, u64 *spte,
                        struct kvm_rmap_head *rmap_head)
{
        struct pte_list_desc *desc;
        int i, count = 0;
        //sp首次初始化时sp->parent_ptes = parent_pte
        if (!rmap_head->val) {
                rmap_printk("pte_list_add: %p %llx 0->1\n", spte, *spte);
                rmap_head->val = (unsigned long)spte;
        } else if (!(rmap_head->val & 1)) { //第二次时
                rmap_printk("pte_list_add: %p %llx 1->many\n", spte, *spte);
                desc = mmu_alloc_pte_list_desc(vcpu);
                desc->sptes[0] = (u64 *)rmap_head->val;
                desc->sptes[1] = spte;
                rmap_head->val = (unsigned long)desc | 1;
                ++count;
        } else {
                // 以后在增加时由于desc已存在不需再alloc
                rmap_printk("pte_list_add: %p %llx many->many\n", spte, *spte);
                desc = (struct pte_list_desc *)(rmap_head->val & ~1ul);
                while (desc->sptes[PTE_LIST_EXT-1] && desc->more) {
                        desc = desc->more;
                        count += PTE_LIST_EXT;
                }
                // 当sptes数量太多时用more链表来管理
                if (desc->sptes[PTE_LIST_EXT-1]) {
                        desc->more = mmu_alloc_pte_list_desc(vcpu);
                        desc = desc->more;
                }
                for (i = 0; desc->sptes[i]; ++i)
                        ++count;
                desc->sptes[i] = spte;
        }
        return count;
}

由于spte的地址只可能是8的倍数,所以其第一位肯定是0,那么我们就利用这个特点:

  • 我们用一个 unsignedlong * 来表示 pte_list ;

  • 如果这个 pte_list 为空,则表示这个之前没有创建过,那么将其赋值,即上文中 0->1 的情况;

  • 如果这个 pte_list 不为空,但是其第一位是,则表示这个rmap之前已经被设置了一个值,那么需要将这个 pte_list 的值改为某个 struct pte_list_desc 的地址,然后将第一位设成 ,来表示该地址并不是单纯的一个spte的地址,而是指向某个 structpte_list_desc ,这是上文中 1->many 的情况;

  • 如果这个 pte_list 不为空,而且其第一位是,那么通过访问由这个地址得到struct pte_list_desc ,得到更多的sptes,即上文中 many->many 的情况。

struct pte_list_desc 结构定义如下:

1
2
3
4
5
#define PTE_LIST_EXT 3
struct pte_list_desc {
        u64 *sptes[PTE_LIST_EXT];
        struct pte_list_desc *more;
};

它是一个单链表的节点每个节点都存有3个spte的地址,以及下一个节点的位置。这个反向映射用于如下case:如操作系统需要进行页面回收或换出,如果宿主机需要把某个客户机物理页换到disk,那么它就需要修改这个页的物理地址gpa对应的spte,将其设置成不存在

反向映射的遍历函数如下:

9.1.3.4. kvm_tdp_page_fault 小结

非fast page fault非异步page fault时:

2020-04-04-22-28-28.png

可以看到,首先通过try_async_pf从gfn获得pfn

__gfn_to_pfn中,首先调用gfn_to_memslot,确定该gfn在哪一个memslot中;之后调用__gfn_to_pfn_memslot,该函数首先调用__gfn_to_hva_many获得gfn到hva的映射,之后调用hva_to_pfn获得hva到pfn的映射

之后调用 __direct_map 函数进行映射。

__direct_map 中,对非叶子节点的处理调用kvm_mmu_get_page获得一页新的mmu page,之后调用link_shadow_page调用mmu_set_spte将其加入到EPT paging structure中。对于叶子节点,则直接调用mmu_set_sptemmu_set_spte调用set_spte来设置spte。set_spte是设置页表结构的最终调用函数。

kvm_mmu_get_page函数是创建mmu页结构的函数,在该函数主要流程如下:

  1. 设置 role
  2. 通过 role 和 gfn 在反向映射表中查找kvm mmu page,如果存在之前创建过的page,则返回该page
  3. 调用kvm_mmu_alloc_page创建新的struct kvm_mmu_page
  4. 调用hlist_add_head将该页加入到kvm->arch.mmu_page_hash哈希表中
  5. 调用init_shadow_page_table初始化对应的spt

10. kvm_mmu_free_page(): 回收

当页无效时, 最终会调用kvm_mmu_free_page()回收, 这里主要看下何时会回收.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void kvm_mmu_commit_zap_page(struct kvm *kvm,
                                    struct list_head *invalid_list)
{
        struct kvm_mmu_page *sp, *nsp;

        if (list_empty(invalid_list))
                return;

        /*
         * We need to make sure everyone sees our modifications to
         * the page tables and see changes to vcpu->mode here. The barrier
         * in the kvm_flush_remote_tlbs() achieves this. This pairs
         * with vcpu_enter_guest and walk_shadow_page_lockless_begin/end.
         *
         * In addition, kvm_flush_remote_tlbs waits for all vcpus to exit
         * guest mode and/or lockless shadow page table walks.
         */
        kvm_flush_remote_tlbs(kvm);
        // 从invalid_list回收页
        list_for_each_entry_safe(sp, nsp, invalid_list, link) {
                WARN_ON(!sp->role.invalid || sp->root_count);
                kvm_mmu_free_page(sp);
        }
}

prepare_zap_oldest_mmu_page将sp放入invalid list.

有三种case调用该函数:

(1) host 内存需要回收时 mmu_shrink_scan

(2) Guest OS memory region 发生变化

(3) ept 加载时mmu_alloc_direct_roots==>make_mmu_pages_available

11. EPT vm-entry处理

11.1. KVM_REQ_MMU_RELOAD

kvm_mmu_invalidate_zap_all_pages ==> kvm_reload_remote_mmus ==> make_all_cpus_request(kvm,KVM_REQ_MMU_RELOAD);

vcpu_enter_guest==>

1
2
3
if(kvm_check_request(KVM_REQ_MMU_RELOAD, vcpu))

           kvm_mmu_unload(vcpu); //call mmu_free_roots

kvm_mmu_unload会调用prepare_zap_oldest_mmu_page,将所有sp放入invalid_list

11.2. KVM_REQ_MMU_SYNC

kvm_mmu_get_page

if (sp->unsync_children) {

kvm_make_request(KVM_REQ_MMU_SYNC, vcpu);

kvm_mmu_mark_parents_unsync(sp);

}

set_spte ==》 mmu_need_write_protect ==》kvm_unsync_pages ==》__kvm_unsync_page==》kvm_mmu_mark_parents_unsync

vcpu_enter_guest==>

if (kvm_check_request(KVM_REQ_MMU_SYNC, vcpu))

kvm_mmu_sync_roots(vcpu); //call mmu_sync_roots

kvm_sync_roots==> mmu_sync_children

while (mmu_unsync_walk(parent, &pages)) { //统计有多少页为sync

bool protected = false;

for_each_sp(pages, sp, parents, i)

protected |= rmap_write_protect(vcpu->kvm, sp->gfn);

if (protected)

kvm_flush_remote_tlbs(vcpu->kvm);

for_each_sp(pages, sp, parents, i) {

kvm_sync_page(vcpu, sp, &invalid_list); //

mmu_pages_clear_parents(&parents);

}

kvm_mmu_commit_zap_page(vcpu->kvm, &invalid_list);

cond_resched_lock(&vcpu->kvm->mmu_lock);

kvm_mmu_pages_init(parent, &parents, &pages);

}

static int __kvm_sync_page(struct kvm_vcpu *vcpu, struct kvm_mmu_page *sp,

struct list_head*invalid_list, bool clear_unsync)

{

if (sp->role.cr4_pae != !!is_pae(vcpu)) {

kvm_mmu_prepare_zap_page(vcpu->kvm, sp, invalid_list);

return 1;

}

if (clear_unsync)

kvm_unlink_unsync_page(vcpu->kvm, sp); // 减少sp->unsync = 0;

//同步页, ept case sync_page = nonpaging_sync_page,该函数直接返回1

if (vcpu->arch.mmu.sync_page(vcpu, sp)) {

kvm_mmu_prepare_zap_page(vcpu->kvm, sp, invalid_list); //同步完后将页移除invalid_list

return 1;

}

kvm_mmu_flush_tlb(vcpu);

return 0;

}

11.3. KVM_REQ_TLB_FLUSH

kvm_mmu_flush_tlb ==》 kvm_make_request(KVM_REQ_TLB_FLUSH, vcpu);

vcpu_enter_guest==>

if (kvm_check_request(KVM_REQ_TLB_FLUSH, vcpu))

kvm_x86_ops->tlb_flush(vcpu);

static voidvmx_flush_tlb(struct kvm_vcpu *vcpu)

{

vpid_sync_context(to_vmx(vcpu));

if (enable_ept) {

if (!VALID_PAGE(vcpu->arch.mmu.root_hpa))

return;

ept_sync_context(construct_eptp(vcpu->arch.mmu.root_hpa));

}

}

static inline void__invvpid(int ext, u16 vpid, gva_t gva)

{

struct {

u64 vpid : 16;

u64 rsvd : 48;

u64 gva;

} operand = { vpid, 0, gva };

asm volatile (__ex(ASM_VMX_INVVPID)

/* CF==1 or ZF==1--> rc = -1 */

"; ja 1f ; ud2 ;1:"

: :"a"(&operand), "c"(ext) : "cc","memory");

}

static inline void__invept(int ext, u64 eptp, gpa_t gpa)

{

struct {

u64 eptp, gpa;

} operand = {eptp, gpa};

asm volatile (__ex(ASM_VMX_INVEPT)

/* CF==1 or ZF==1 --> rc = -1 */

"; ja 1f ; ud2 ; 1:\n"

: : "a" (&operand), "c" (ext) :"cc", "memory");

}

12. EPT VM-exit 处理

12.1. CR3寄存器

static voidvmx_set_cr3(struct kvm_vcpu *vcpu, unsigned long cr3)

{

unsigned long guest_cr3;

u64 eptp;

guest_cr3 = cr3;

if (enable_ept) {

eptp = construct_eptp(cr3);

vmcs_write64(EPT_POINTER, eptp); //设置ept地址

if (is_paging(vcpu) || is_guest_mode(vcpu)) //分页或nest kvm

guest_cr3 = kvm_read_cr3(vcpu);

else

guest_cr3 = vcpu->kvm->arch.ept_identity_map_addr;//实模式

ept_load_pdptrs(vcpu);

}

vmx_flush_tlb(vcpu);

vmcs_writel(GUEST_CR3, guest_cr3);

}

mmu_alloc_direct_roots中会分配arch.mmu.root_hpa

kvm_mmu_load==> vcpu->arch.mmu.set_cr3(vcpu,vcpu->arch.mmu.root_hpa);

12.2. handle_ept_violation

a当EXIT_QUALIFICATION

bit7=0bit8=1为非法case

bit7=1 时gpa来自对guest-linear-address的转换, bit8为0表明eptviolation发生在访问guest paging structure.

bit7 =1, bit8 = 0表明ept violation发生在gpa->hpa

bit7=0bit8=0 表明gpa不是由guest-linear-addr引起(例如mov to cr3 指令导致pdpte加载)

b. /* Itis a write fault? */

error_code= exit_qualification & (1U << 1);

/* Itis a fetch fault? */

error_code|= (exit_qualification & (1U << 2)) << 2;

/* eptpage table is present? */

error_code|= (exit_qualification >> 3) & 0x1;

vcpu->arch.exit_qualification= exit_qualification;

returnkvm_mmu_page_fault(vcpu, gpa, error_code, NULL, 0);

kvm_mmu_page_fault==》 vcpu->arch.mmu.page_fault ==》tdp_page_fault

{

。。。。。。。

if (unlikely(error_code & PFERR_RSVD_MASK)) {

r = handle_mmio_page_fault(vcpu, gpa, error_code, true);

if (likely(r != RET_MMIO_PF_INVALID))

return r;

}

r = mmu_topup_memory_caches(vcpu);

if (r)

return r;

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;

mmu_seq = vcpu->kvm->mmu_notifier_seq;

smp_rmb();

if (try_async_pf(vcpu, prefault, gfn, gpa, &pfn, write,&map_writable))

return 0;

if (handle_abnormal_pfn(vcpu, 0, gfn, pfn, ACC_ALL, &r))

return r;

spin_lock(&vcpu->kvm->mmu_lock);

if (mmu_notifier_retry(vcpu->kvm, mmu_seq))

goto out_unlock;

make_mmu_pages_available(vcpu);

if (likely(!force_pt_level))

transparent_hugepage_adjust(vcpu, &gfn, &pfn,&level);

r = __direct_map(vcpu, gpa, write, map_writable,

level, gfn, pfn,prefault);

spin_unlock(&vcpu->kvm->mmu_lock);

。。。。。。

}

mapping_level 根据gfn得到映射页表的级数

static intmapping_level(struct kvm_vcpu *vcpu, gfn_t large_gfn)

{

int host_level, level, max_level;

host_level = host_mapping_level(vcpu->kvm, large_gfn);

if (host_level == PT_PAGE_TABLE_LEVEL)

return host_level;

max_level = min(kvm_x86_ops->get_lpage_level(), host_level);

for (level = PT_DIRECTORY_LEVEL; level <= max_level; ++level)

if (has_wrprotected_page(vcpu->kvm, large_gfn, level))

break;

return level - 1;

}

try_async_pf的到gpa对应的hpa

static int __direct_map(structkvm_vcpu *vcpu, gpa_t v, int write,

int map_writable, int level, gfn_t gfn, pfn_t pfn,

bool prefault)

{

.......

//循环遍历guest os页帧号在影子页表中的页表项

for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT,iterator) {

if (iterator.level == level) { //如果等于要设置的页表级别,设置对应的页表

mmu_set_spte(vcpu, iterator.sptep, ACC_ALL,

write,&emulate, level, gfn, pfn,

prefault,map_writable);

direct_pte_prefetch(vcpu, iterator.sptep);

++vcpu->stat.pf_fixed;

break;

}

drop_large_spte(vcpu, iterator.sptep);

if (!is_shadow_present_pte(*iterator.sptep)) { //如果设置的页表之前的页表项为空,那么要建立页表

u64 base_addr = iterator.addr;

base_addr &= PT64_LVL_ADDR_MASK(iterator.level);

pseudo_gfn = base_addr >> PAGE_SHIFT;

sp = kvm_mmu_get_page(vcpu, pseudo_gfn, iterator.addr,

iterator.level - 1,

1,ACC_ALL, iterator.sptep); 分配一个新的页表

link_shadow_page(iterator.sptep, sp, true);

}

}

return emulate;

}

__direct_map 这个函数是根据传进来的gpa进行计算,从第4级(level-4)页表页开始,一级一级地填写相应页表项,这些都是在for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator) 这个宏定义里面实现的.这两种情况是这样子的:

第一种情况是指如果当前页表页的层数(iterator.level )是最后一层( level )的页表页,那么直接通过调用 mmu_set_spte (之后会细讲)设置页表项。

第二种情况是指如果当前页表页 A 不是最后一层,而是中间某一层(leve-4, level-3, level-2),而且该页表项之前并没有初始化(!is_shadow_present_pte(*iterator.sptep) ),那么需要调用kvm_mmu_get_page 得到或者新建一个页表页 B ,然后通过 link_shadow_page 将其link到页表页 A 相对应的页表项中

对于高层级的页表页,我们只需要调用link_shadow_page ,将页表项的值和相应的权限位直接设置上去就好了,但是对于最后一级的页表项,我们除了设置页表项对应的值之外,还需要做另一件事, rmap_add :arch/x86/kvm/mmu.c

static void mmu_set_spte(...)

{

...

if (set_spte(vcpu, sptep, pte_access, level,gfn, pfn, speculative,

true, host_writable)) {

...

}

...

if (is_shadow_present_pte(*sptep)) {

if (!was_rmapped) {

rmap_count = rmap_add(vcpu, sptep, gfn);

...

}

}

...

}

static int rmap_add(structkvm_vcpu *vcpu, u64 *spte, gfn_t gfn)

{

...

sp = page_header(__pa(spte));

kvm_mmu_page_set_gfn(sp, spte - sp->spt,gfn);

rmapp = gfn_to_rmap(vcpu->kvm, gfn,sp->role.level);

return pte_list_add(vcpu, spte, rmapp);

}

(3) EPT遍历操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#definefor_each_shadow_entry(_vcpu, _addr, _walker)   \

    for (shadow_walk_init(&(_walker), _vcpu, _addr); \

         shadow_walk_okay(&(_walker));        \

        shadow_walk_next(&(_walker)))

 

structkvm_shadow_walk_iterator {

    u64 addr;

    hpa_t shadow_addr;

    u64 *sptep;

    int level;

    unsigned index;

};

遍历开始时(shadow_walk_init)

iterator->addr = addr;

iterator->shadow_addr = vcpu->arch.mmu.root_hpa;

iterator->level = vcpu->arch.mmu.shadow_root_level;

取当前项的值,shadow_walk_okay

iterator->index= SHADOW_PT_INDEX(iterator->addr, iterator->level);

iterator->sptep = ((u64 *)__va(iterator->shadow_addr)) +iterator->index;

取下一项

__shadow_walk_next:

iterator->shadow_addr= spte & PT64_BASE_ADDR_MASK;

--iterator->level;

3.3.5 Sync page

(1) mmu_sync_roots

从root_hpa开始调用mmu_sync_children

mmu_unsync_walk(struct kvm_mmu_page *sp, structkvm_mmu_pages *pvec)

a) mmu_unsync_walk得到所有子sp 将这些sp都加入到pvec中

b) for_each_sp(pages,sp, parents, i) //对pvec中的页面(不包括用于page dir的page)

protected |=rmap_write_protect(vcpu->kvm, sp->gfn); // 计算该页和他的page dir是否有一个为write protected

c) if(protected)

kvm_flush_remote_tlbs(vcpu->kvm);

d) for_each_sp(pages,sp, parents, i) {

kvm_sync_page(vcpu, sp, &invalid_list);//同步page

mmu_pages_clear_parents(&parents); //遍历减少unsync_children

i. }

e) kvm_mmu_commit_zap_page(vcpu->kvm,&invalid_list); //释放invalid_list中的页

(2) kvm_sync_page==> __kvm_sync_page时 clear_unsync 设为true

static int__kvm_sync_page(struct kvm_vcpu *vcpu, struct kvm_mmu_page *sp,

struct list_head*invalid_list, bool clear_unsync)

a. if (clear_unsync)

kvm_unlink_unsync_page(vcpu->kvm, sp); //sp->unsync = 0;

b. if (vcpu->arch.mmu.sync_page(vcpu, sp)) {

kvm_mmu_prepare_zap_page(vcpu->kvm, sp, invalid_list);

(3) kvm_mmu_prepare_zap_page

a) ret =mmu_zap_unsync_children(kvm, sp, invalid_list);

通过mmu_unsync_walk; for_each_sp, 对所有sp进行如下操作

kvm_mmu_prepare_zap_pag; mmu_pages_clear_parents

b) kvm_mmu_page_unlink_children

对sp所指向的页做mmu_page_zap_pte==>drop_parent_pte

c) kvm_mmu_unlink_parents:

if (sp->unsync)

kvm_unlink_unsync_page(kvm, sp);//sp->unsync=0

d) 如果free 了root则 将页移入invalid_list

不是则移入kvm->arch.active_mmu_pages)

对所有指向sp的页框做drop_parent_pte

(4) unsync的设置时机

__direct_map ==> mmu_set_spte==>set_spte ==》 mmu_need_write_protect ==》 kvm_unsync_pages ==》 __kvm_unsync_page==》kvm_mmu_mark_parents_unsync

13. 逆向映射机制

13.1. 两种逆向映射

有两个很重要的函数:

  • mmu_page_add_parent_pte: 调用pte_list_add设置其parent_pte的reverse map
  • mmu_set_spte: 会调用rmap_add进而调用pte_list_add

其实它们都和reverse map有关

首先,对于低层级level-3 to level-1)的页表页结构 kvm_mmu_page,我们需要设置上一级的相应的页表项地址,然后通过 mmu_page_add_parent_pte 设置其parent_pte的reverse map;

页分为两类,物理页页表页,而页表页本身也被分为两类,高层级(level-4 to level-2)的页表页,和最后一级(level-1)的页表页。

对于高层级的页表页,我们只需要调用 link_shadow_page ,将页表项的值相应的权限位直接设置上去就好了,但是对于最后一级页表项,我们除了设置页表项对应的值之外,还需要做另一件事, rmap_add:

不管是 mmu_page_add_parent_pte ,还是 mmu_set_spte 调用的 rmap_add ,最后都会调用!!!pte_list_add

那么问题来了,这货是干嘛的呢?

翻译成中文的话,reverse map被称为反向映射,在上面提到的两个反向映射中,

  • 第一个叫parent_ptes,记录的是页表页指向它的页表项对应的映射,
  • 另一个是每个gfn对应的反向映射rmap,记录的是该gfn对应的spte

13.2. rmap逆向映射: gfn对应的spte

linux下根据虚拟地址经过页表转换得到物理地址。怎么根据物理地址得到对应的虚拟地址呢?这里便用到了逆向映射

逆向映射有什么用呢?最重要的,在页面换出时,由于物理内存的管理由一套相对独立的机制在负责,根据物理页面的活跃程度,对物理页面进行换出,而此时就需要更新引用了此页面的页表了,否则造成不同步而出错。如果获取对应的物理页面对应的pte的地址呢?内核的做法是先通过逆向映射得到虚拟地址,根据虚拟地址遍历页表得到pte地址。

在KVM中,逆向映射机制的作用是类似的,但是完成的却不是HPA对应的EPT页表项的定位,而是从gfn对应的影子页表项的定位。理论上讲根据gfn一步步遍历EPT也未尝不可,但是效率较低;况且在EPT所维护的页面不同于host的页表,理论上讲是虚拟机之间禁止主动的共享内存的,为了提高效率,就有了当前的逆向映射机制

mmu维护一个反向映射,映射这个页的所有页表项,可以通过gfn查找到此页的影子页表项spte。该反向映射主要用于页面回收或换出时,例如宿主机如果想把客户机的某个物理页面交换到硬盘,宿主机需要能够修改对应的SPT,将该spte设置为不存在,否则客户机访问该页面时将会访问到一个错误的页。

给定一个gfn,设定其对应的rmap呢?

  1. 首先,我们通过 gfn_to_memslot 得到这个gfn对应的memory slot
  2. 通过得到的slotgfn,算出相应的index,然后从 slot->arch.rmap 数组中取出相应的rmap
  3. 有了gfn对应的rmap之后,我们再调用 pte_list_add 将这次映射得到的spte加到这个rmap

如果通过反向映射计算spte地址呢?

  1. 页面回收时,能够知道宿主机虚拟地址HVA
  2. 通过HVA可以计算出GFN,计算公式gfn=(hva-base_hva)>>PAGE_SIZE+base_gfn
  3. 通过反向映射定位到影子页表项spte

反向映射表相关的结构存储在kvm_arch_memory_slot.rmap中,每个元素存储的是一个pte_list_desc+权限位,每个pte_list_desc是一个单链表的节点,即存储的是一个单链表结构

反向映射表相关的操作如下:

  1. 由gfn获得对应的rmap:static unsigned long *gfn_to_rmap(struct kvm *kvm, gfn_t gfn, int level)
  2. 添加gfn反向映射spte:static int rmap_add(struct kvm_vcpu *vcpu, u64 *spte, gfn_t gfn),添加的内容是struct pte_list_desc
  3. 删除反向映射:static void rmap_remove(struct kvm *kvm, u64 *spte)
  4. 获得rmap单链表中的元素:首先调用rmap_get_first()获得一个有效的rmap_iterator,其次调用static u64 *rmap_get_next(struct rmap_iterator *iter)获得链表中的下一个元素

看到这里你可能还是一头雾水,rmap到底是什么,为什么加一个rmap的项要那么复杂?

好吧,其实我的理解是这样的:

  1. 首先,rmap就是一个数组,这个数组的每个项都对应了这个gfn反向映射出的某个spte的地址
  2. 其次,由于大部分情况下一个gfn对应的spte只有一个,也就是说,大部分情况下这个数组的大小是1
  3. 但是,这个数组也可能很大,大到你也不知道应该把数组的大小设到多少合适;
  4. 所以,总结来说,rmap是一个不确定大小,但是大部分情况下大小为1的数组。

这是一个看上去很完美的设计!

由于spte的地址只可能是8的倍数(?),所以其第一位肯定是0,那么我们就利用这个特点:

  1. 我们用一个 unsigned long * 来表示一个rmap,即 rmap_head->val
  2. 如果这个 rmap_head->val 为空,则表示这个rmap之前没有创建过,那么将其赋值,即上文中 0->1 的情况;
  3. 如果这个 rmap_head->val 不为空,但是其第一位是 ,则表示这个rmap之前已经被设置了一个值,那么需要将这个 rmap_head->val 的值改为某个 struct pte_list_desc 的地址,然后将第一位设成 ,来表示该地址并不是单纯的一个spte的地址,而是指向某个 struct pte_list_desc ,这是上文中 1->many 的情况;
  4. 如果这个 pte_list 不为空,而且其第一位是 ,那么通过访问由这个地址得到的 struct pte_list_desc ,得到更多的sptes,即上文中 many->many 的情况。

struct pte_list_desc 它是一个单链表的节点每个节点都存有3个spte的地址,以及下一个节点的位置。

13.3. rmap的用处

rmap到底有什么用?

举个例子吧,假如操作系统需要进行页面回收或换出,如果宿主机需要把某个客户机物理页换到disk,那么它就需要修改这个页的物理地址gpa对应的spte,将其设置成不存在。

那么这个该怎么做呢?

当然,你可以用软件走一遍ept页表,找到其对应的spte。但是,这样太慢了!这个时候你就会想,如果有一个gfn到spte的反向映射岂不方便很多!于是,reverse map就此派上用场。

这里最后说一点,如果说有这么一个需求:宿主机想要废除当前客户机所有的MMU页结构,那么如何做最快呢?

当然,你可以从EPTP开始遍历一遍所有的页表页,处理掉所有的MMU页面和对应的映射,但是这种方法效率很低。

如果你还记得之前 kvm_mmu_page 结构里面的 mmu_valid_gen 域的话,你就可以通过将kvm->arch.mmu_valid_gen加1,那么当前所有的MMU页结构都变成了invalid,而处理掉页结构的过程可以留给后面的过程(如内存不够时)再处理,这样就可以加快这个过程。

而当mmu_valid_gen值达到最大时,可以调用kvm_mmu_invalidate_zap_all_pages手动废弃掉所有的MMU页结构。

13.4. rmap相关数据结构

虚拟机物理内存多个slot构成,每个slot都是一个kvm_memory_slot结构,表示虚拟机物理内存的一段空间.

1
2
3
4
5
6
7
8
9
struct kvm_memory_slot {
        gfn_t base_gfn;
        unsigned long npages;
        unsigned long *dirty_bitmap;
        struct kvm_arch_memory_slot arch;
        unsigned long userspace_addr;
        u32 flags;
        short id;
};

kvm_memory_slot本质是qemu进程用户空间的hva, 仅仅是qemu进程的虚拟地址空间,并没有对应物理地址,各个字段的意义不言自明了。其中有一个kvm_arch_memory_slot结构,我们重点描述。

1
2
3
4
5
6
7
8
9
10
struct kvm_rmap_head {
        unsigned long val;
};

struct kvm_arch_memory_slot {
        // 3, 每种页面大小对应一个链表
        struct kvm_rmap_head *rmap[KVM_NR_PAGE_SIZES];
        struct kvm_lpage_info *lpage_info[KVM_NR_PAGE_SIZES - 1];
        unsigned short *gfn_track[KVM_PAGE_TRACK_MAX];
};

该结构的rmap字段是指针数组,每种页面大小对应一个链表,KVM的大页面支持2M和1G的页面,普通的页面就是4KB了。

结合上面的kvm_memory_slot结构可以发现,kvm_arch_memory_slot其实是kvm_memory_slot的一个内嵌结构,所以每个slot都关联一个kvm_arch_memory_slot,也就有一个rmap数组

其实在虚拟机中qemu为虚拟机分配的页面主要是大页面,但是这里为了方面,按照4KB的普通页面做介绍。

13.5. 初始化阶段: 注册各个slot时

在qemu为虚拟机注册各个slot的时候,在KVM中会初始化逆向映射的相关内存区。

__kvm_set_memory_region --> kvm_alloc_memslot_metadata

在该函数中,用一个for循环为每种页面类型的rmap分配空间,具体分配代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int kvm_alloc_memslot_metadata(struct kvm_memory_slot *slot,
                                      unsigned long npages)
{
        int i;

        /*
         * Clear out the previous array pointers for the KVM_MR_MOVE case.  The
         * old arrays will be freed by __kvm_set_memory_region() if installing
         * the new memslot is successful.
         */
        // 重置这个kvm_memory_slot的arch
        memset(&slot->arch, 0, sizeof(slot->arch));

        for (i = 0; i < KVM_NR_PAGE_SIZES; ++i) {
                struct kvm_lpage_info *linfo;
                unsigned long ugfn;
                int lpages;
                int level = i + 1;
                // 一级页表所需的pages数目
                lpages = gfn_to_index(slot->base_g fn + npages - 1,
                                      slot->base_gfn, level) + 1;
                // rmap, 反向映射结构
                slot->arch.rmap[i] =
                        kvcalloc(lpages, sizeof(*slot->arch.rmap[i]),
                                 GFP_KERNEL_ACCOUNT);
                if (!slot->arch.rmap[i])
                        goto out_free;

gfn_to_index一个gfn转化成该gfn这个slot中的索引,而这里获取的其实就是整个slot包含的不同level页面数

然后为slot->arch.rmap[i]分配内存,每个页面对应一个unsigned Long.

13.6. 建立阶段: 填充EPT

建立阶段自然是在填充EPT的时候了,在KVM中维护EPT的核心函数是kvm_tdp_page_fault函数。在函数尾部(__direct_map尾部的mmu_set_spte中)会调用rmap_add函数建立逆向映射

1
2
3
4
5
6
7
8
9
10
11
12
13
static int rmap_add(struct kvm_vcpu *vcpu, u64 *spte, gfn_t gfn)
{
        struct kvm_mmu_page *sp;
        struct kvm_rmap_head *rmap_head;
        // 获取该页表项对应的页表页
        sp = page_header(__pa(spte));
        // 设置该页表页
        kvm_mmu_page_set_gfn(sp, spte - sp->spt, gfn);
        // gfn对应的rmap地址
        rmap_head = gfn_to_rmap(vcpu->kvm, gfn, sp);
        // 设置逆向映射
        return pte_list_add(vcpu, spte, rmap_head);
}

page_header是一个内联函数,主要目的在于获取kvm_mmu_page,一个该结构描述一个层级的页表地址保存在page结构private字段,然后调用kvm_mmu_page_set_gfn,对kvm_mmu_page进行设置。

接着就获取了gfn对应的rmap的地址,重点看下

13.6.1. gfn_to_rmap(): 获取gfn对应的rmap地址

1
2
3
4
5
6
7
8
9
10
11
12
static struct kvm_rmap_head *gfn_to_rmap(struct kvm *kvm, gfn_t gfn,
                                         struct kvm_mmu_page *sp)
{
        struct kvm_memslots *slots;
        struct kvm_memory_slot *slot;
        //
        slots = kvm_memslots_for_spte_role(kvm, sp->role);
        // 获取该gfn对应的slot
        slot = __gfn_to_memslot(slots, gfn);
       
        return __gfn_to_rmap(gfn, sp->role.level, slot);
}

首先转化成到对应的slot,然后调用了__gfn_to_rmap

1
2
3
4
5
6
7
8
9
static struct kvm_rmap_head *__gfn_to_rmap(gfn_t gfn, int level,
                                           struct kvm_memory_slot *slot)
{
        unsigned long idx;
        /*gfn在slot中的index*/
        idx = gfn_to_index(gfn, slot->base_gfn, level);
        /*rmap是一个指针数组,每个项记录对应层级的gfn对应的逆向映射,index就是下标*/
        return &slot->arch.rmap[level - PT_PAGE_TABLE_LEVEL][idx];
}

再次看到了gfn_to_index函数,这里就根据指定的gfn转化成索引,同时也是在rmap数组的下标,然后就返回对应的表项的地址,没啥好说的吧……现在地址已经获取到了,还等什么呢?设置吧,调用pte_list_add函数,该函数也值得一说

13.6.2. pte_list_add(): 设置rmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
 * Returns the number of pointers in the rmap chain, not counting the new one.
 */
static int pte_list_add(struct kvm_vcpu *vcpu, u64 *spte,
                        struct kvm_rmap_head *rmap_head)
{
        struct pte_list_desc *desc;
        int i, count = 0;
        /*如果remap_head为空,直接设置逆向映射即可 */
        if (!rmap_head->val) {
                rmap_printk("pte_list_add: %p %llx 0->1\n", spte, *spte);
                rmap_head->val = (unsigned long)spte;
        } else if (!(rmap_head->val & 1)) {
                rmap_printk("pte_list_add: %p %llx 1->many\n", spte, *spte);
                desc = mmu_alloc_pte_list_desc(vcpu);
                desc->sptes[0] = (u64 *)rmap_head->val;
                desc->sptes[1] = spte;
                rmap_head->val = (unsigned long)desc | 1;
                ++count;
        } else {
                rmap_printk("pte_list_add: %p %llx many->many\n", spte, *spte);
                desc = (struct pte_list_desc *)(rmap_head->val & ~1ul);
                while (desc->sptes[PTE_LIST_EXT-1] && desc->more) {
                        desc = desc->more;
                        count += PTE_LIST_EXT;
                }
                /*如果已经满了,就再次扩展more*/
                if (desc->sptes[PTE_LIST_EXT-1]) {
                        desc->more = mmu_alloc_pte_list_desc(vcpu);
                        desc = desc->more;
                }
                /*找到首个为空的项,进行填充*/
                for (i = 0; desc->sptes[i]; ++i)
                        ++count;
                desc->sptes[i] = spte;
        }
        return count;
}

先走下函数流程,我们已经传递进来gfn对应的rmap的地址,就是rmap_head,接下来主要分为三部分;if……else if ……else

首先,如果*rmap_head为空,则直接*rmap_head->val = (unsigned long)spte;直接把rmap地址的内容设置成表项地址. 但是这并不能解决所有问题,说到这里看下函数前面的注释

1
2
3
4
5
6
7
/*
 * About rmap_head encoding:
 *
 * If the bit zero of rmap_head->val is clear, then it points to the only spte
 * in this rmap chain. Otherwise, (rmap_head->val & ~1) points to a struct
 * pte_list_desc containing more mappings.
 */