KVM MMU EPT内存管理

转载请注明:【转载自博客xelatex KVM】,并附本文链接。谢谢。
 
【注】文章中采用的版本:
Linux-3.11,https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.11.tar.gz
qemu-kvm,git clone http://git.kernel.org/pub/scm/virt/kvm/qemu-kvm.git, 
          git checkout 4d9367b76f71c6d938cf8201392abe4bfb1136cb
 
先说几个英文缩写:
  • GVA - Guest Virtual Address,虚拟机的虚拟地址
  • GPA - Guest Physical Address,虚拟机的虚拟地址
  • GFN - Guest Frame Number,虚拟机的页框号
  • HVA - Host Virtual Address,宿主机虚拟地址,也就是对应Qemu中申请的地址
  • HPA - Host Physical Address,宿主机物理地址
  • HFN - Host Frame Number,宿主机的页框号
KVM中目前一般采用硬件虚拟化来对内存的MMU进行虚拟化,以提高访存的效率。在比较老的虚拟化技术中,一般采用SPT(Shadow Page Table)来实现MMU虚拟化,所以在KVM代码中有很多地方依然采用类似的命名方式。本文主要针对Intel体系架构Intel VT-x中EPT在KVM中的应用进行说明。
 
一、内存槽(slot)的注册和管理
 
上文中我提到KVM只提供机制,不提供策略。为了实现对内存区域的管理,采用了kvm_memory_slot结构来对应Qemu中的AddressSpace。Qemu将虚拟机的线性地址(物理地址)在KVM中注册为多个内存槽,如BIOS、MMIO、GPU、RAW。

Qemu的内存模型请参考官方文档 https://github.com/qemu/qemu/blob/master/docs/memory.txt
 
KVM中memory slot的数据结构:

<include/linux/kvm_host.h>
struct kvm_memory_slot {
    gfn_t base_gfn; // 该slot对应虚拟机页框的起点
    unsigned long npages; // 该slot中有多少个页
    unsigned long *dirty_bitmap; // 脏页的bitmap
    struct kvm_arch_memory_slot arch; // 体系结构相关的结构
    unsigned long userspace_addr; // 对应HVA的地址
    u32 flags; // slot的flag
    short id; // slot识别id
};

<arch/x86/include/asm/kvm_host.h>
struct kvm_arch_memory_slot {
    unsigned long *rmap[KVM_NR_PAGE_SIZES]; // 反向映射结构(reverse map)
    struct kvm_lpage_info *lpage_info[KVM_NR_PAGE_SIZES - 1]; // Large page结构(如2MB、1GB大小页面)
}; 

KVM数据结构中与内存槽相关的结构,注意KVM对每个虚拟机都会建立和维护一个struct kvm结构。

<include/linux/kvm_host.h>
struct kvm {
	spinlock_t mmu_lock; // MMU最大的锁
	struct mutex slots_lock; // 内存槽操作锁
	struct mm_struct *mm; /* userspace tied to this vm,指向虚拟机内部的页存储结构 */
	struct kvm_memslots *memslots; // 存储该KVM所有的memslot
...
};

struct kvm_memslots {
	u64 generation;
	struct kvm_memory_slot memslots[KVM_MEM_SLOTS_NUM];
	/* The mapping table from slot id to the index in memslots[]. */
	short id_to_index[KVM_MEM_SLOTS_NUM];
};

kvm->memslots结构在创建虚拟机时被创建,代码见:

<virt/kvm/kvm_main.c>
static struct kvm *kvm_create_vm(unsigned long type)
{
...
    kvm->memslots = kzalloc(sizeof(struct kvm_memslots), GFP_KERNEL);
    if (!kvm->memslots)
        goto out_err_nosrcu;
    kvm_init_memslots_id(kvm);
...
}

内存槽的注册入口在 kvm_vm_ioctl函数中 case KVM_SET_USER_MEMORY_REGION部分,最终调用函数 __kvm_set_memory_region在KVM中建立与Qemu相对应的内存槽结构。 __kvm_set_memory_region函数主要做了如下几件事情:
  1. 数据检查
  2. 调用id_to_memslot来获得kvm->memslots中对应的memslot指针
  3. 设置memslot的base_gfn、npages等域
  4. 处理和已经存在的memslots的重叠
  5. 调用install_new_memslots装载新的memslot
虚拟机线性地址被分成若干个memslot,每个memslot是不能重叠的,也就是说每一段内存区间都必须有独立的作用。一般来说Qemu会对RAM、IO memory、High memory等分别注册若干个memslot


二、KVM MMU创建和初始化流程

2.1 KVM MMU的创建

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

<virt/kvm/kvm_main.c>
static int kvm_vm_ioctl_create_vcpu(struct kvm *kvm, u32 id)
{
...
    vcpu = kvm_arch_vcpu_create(kvm, id);
...
    r = kvm_arch_vcpu_setup(vcpu);
...
}

该函数中首先调用 kvm_arch_vcpu_create创建vcpu,然后调用 kvm_arch_vcpu_setup初始化vcpu。在x86架构中,kvm_arch_vcpu_create最终调用vmx_create_vcpu函数进行VCPU的创建工作。MMU的创建在 vmx_create_vcpu => kvm_vcpu_init => kvm_arch_vcpu_init => kvm_mmu_create 中,如下:

<arch/x86/kvm/mmu.c>
int kvm_mmu_create(struct kvm_vcpu *vcpu)
{
	ASSERT(vcpu);

	vcpu->arch.walk_mmu = &vcpu->arch.mmu;
	vcpu->arch.mmu.root_hpa = INVALID_PAGE;
	vcpu->arch.mmu.translate_gpa = translate_gpa;
	vcpu->arch.nested_mmu.translate_gpa = translate_nested_gpa;

	return alloc_mmu_pages(vcpu);
}

该函数指定了 arch.walk_mmu就是 arch.mmu的地址,在KVM MMU相关的代码中经常会把arch.walk_mmu和arch.mmu混用,在这里指定了他们其实是一回事。我们来看在vcpu->arch中与MMU相关的结构:

<arch/x86/include/asm/kvm_host.h>
struct kvm_vcpu_arch {
...
    /*
     * Paging state of the vcpu
     *
     * If the vcpu runs in guest mode with two level paging this still saves
     * the paging mode of the l1 guest. This context is always used to
     * handle faults.
     */
    struct kvm_mmu mmu;

    /*
     * Paging state of an L2 guest (used for nested npt)
     *
     * This context will save all necessary information to walk page tables
     * of the an L2 guest. This context is only initialized for page table
     * walking and not for faulting since we never handle l2 page faults on
     * the host.
      */
    struct kvm_mmu nested_mmu;

    /*
     * Pointer to the mmu context currently used for
     * gva_to_gpa translations.
     */
    struct kvm_mmu *walk_mmu;

    struct kvm_mmu_memory_cache mmu_pte_list_desc_cache;
    struct kvm_mmu_memory_cache mmu_page_cache;
    struct kvm_mmu_memory_cache mmu_page_header_cache;
...
};

注释已经很清楚了,我就不做过多的解释了,说一下三个cache:
  • mmu_pte_list_desc_cache:用来分配struct pte_list_desc结构,该结构主要用于反向映射,参考rmap_add函数,每个rmapp指向的就是一个pte_list。后面介绍反向映射的时候会详细介绍。
  • mmu_page_cache:用来分配spt页结构,spt页结构是存储spt paging structure的页,对应kvm_mmu_page.spt
  • mmu_page_header_cache:用来分配struct kvm_mmu_page结构,从该cache分配的页面可能会调用kmem_cache机制来分配
大家能够注意到,这三个cache使用的是 kvm_mmu_memory_cache结构,该结构是KVM定义的cache结构,进一步优化了MMU分配的效率。有两个对应的kmem_cache结构:

<arch/x86/kvm/mmu.c>
static struct kmem_cache *pte_list_desc_cache;
static struct kmem_cache *mmu_page_header_cache;

他们分别对应 mmu _pte_list_desc_cache mmu_page_header_cache,也就是说如果这两个cache中缓存的object数目不够,则会从上述对应的kmem_cache中获取,对应的代码可以参考函数 mmu_topup_memory_cache;而 mmu_page_cache中的object数目不够时,则调用mmu_topup_memory_cache_page函数,其中直接调用了 __get_free_page函数来获得页面。在一些初始化函数中,需要初始化这些cache以便加速运行时的分配,初始化函数为 mmu_topup_memory_caches,该初始化过程在mmu page fault处理函数(如tdp_page_fault)、MMU初始化函数(kvm_mmu_load)和写SPT的pte函数(kvm_mmu_pte_write)中被调用。

如果不关注效率的话可以忽略上述cache。


2.2 KVM MMU的初始化

KVM MMU的初始化过程在 kvm_vm_ioctl_create_vcpu => kvm_arch_vcpu_setup => kvm_mmu_setup => init_kvm_mmu 调用链中。init_kvm_mmu函数根据创建MMU的类型分别有三个调用路径 init_kvm_nested_mmuinit_kvm_tdp_mmuinit_kvm_softmmu。init_kvm_nested_mmu是nested virtualization中调用的,init_kvm_tdp_mmu是支持EPT的虚拟化调用的(tdp的含义是Two-dimentional Paging,也就是EPT),init_kvm_soft_mmu是软件SPT(Shadow Page Table)调用的。我们这里只关注 init_kvm_tdp_mmu

init_kvm_tdp_mmu中唯一做的事情就是初始化了2.1中提到的arch.mmu(通过arch->walk_mmu初始化的),并且根据host的不同进行不同的初始化过程。

下面我们来看 struct kvm_mmu结构,init_kvm_tdp_mmu中几乎初始化了kvm_mmu中所有的域(此处可以参考官方文档[2][3]: https://www.kernel.org/doc/Documentation/virtual/kvm/mmu.txt,或者kernel目录中Documentation/virtual/kvm/mmu.txt)

<arch/x86/include/asm/kvm_host.h>
/*
 * x86 supports 3 paging modes (4-level 64-bit, 3-level 64-bit, and 2-level
 * 32-bit).  The kvm_mmu structure abstracts the details of the current mmu
 * mode.
 */
struct kvm_mmu {
	void (*new_cr3)(struct kvm_vcpu *vcpu);
	void (*set_cr3)(struct kvm_vcpu *vcpu, unsigned long root);
	unsigned long (*get_cr3)(struct kvm_vcpu *vcpu);
	u64 (*get_pdptr)(struct kvm_vcpu *vcpu, int index);
	int (*page_fault)(struct kvm_vcpu *vcpu, gva_t gva, u32 err,
			  bool prefault);
	void (*inject_page_fault)(struct kvm_vcpu *vcpu,
				  struct x86_exception *fault);
	void (*free)(struct kvm_vcpu *vcpu);
	gpa_t (*gva_to_gpa)(struct kvm_vcpu *vcpu, gva_t gva, u32 access,
			    struct x86_exception *exception);
	gpa_t (*translate_gpa)(struct kvm_vcpu *vcpu, gpa_t gpa, u32 access);
	int (*sync_page)(struct kvm_vcpu *vcpu,
			 struct kvm_mmu_page *sp);
	void (*invlpg)(struct kvm_vcpu *vcpu, gva_t gva);
	void (*update_pte)(struct kvm_vcpu *vcpu, struct kvm_mmu_page *sp,
			   u64 *spte, const void *pte);
	hpa_t root_hpa;
	int root_level;
	int shadow_root_level;
	union kvm_mmu_page_role base_role;
	bool direct_map;

	/*
	 * Bitmap; bit set = permission fault
	 * Byte index: page fault error code [4:1]
	 * Bit index: pte permissions in ACC_* format
	 */
	u8 permissions[16];

	u64 *pae_root;
	u64 *lm_root;
	u64 rsvd_bits_mask[2][4];

	/*
	 * Bitmap: bit set = last pte in walk
	 * index[0:1]: level (zero-based)
	 * index[2]: pte.ps
	 */
	u8 last_pte_bitmap;

	bool nx;

	u64 pdptrs[4]; /* pae */
};

/*
 * kvm_mmu_page_role, below, is defined as:
 *
 *   bits 0:3 - total guest paging levels (2-4, or zero for real mode)
 *   bits 4:7 - page table level for this shadow (1-4)
 *   bits 8:9 - page table quadrant for 2-level guests
 *   bit   16 - direct mapping of virtual to physical mapping at gfn
 *              used for real mode and two-dimensional paging
 *   bits 17:19 - common access permissions for all ptes in this shadow page
 */
union kvm_mmu_page_role {
	unsigned word;
	struct {
		unsigned level:4;
		unsigned cr4_pae:1;
		unsigned quadrant:2;
		unsigned pad_for_nice_hex_output:6;
		unsigned direct:1;
		unsigned access:3;
		unsigned invalid:1;
		unsigned nxe:1;
		unsigned cr0_wp:1;
		unsigned smep_andnot_wp:1;
	};
};

对该结构体中各个域进行说明:
  • new_cr3update_pte为函数指针,分别对应着MMU的操作,初始化过程会对这些指针进行初始化,其功能在其命名中即可体现,这里就不详细介绍了
  • root_hpa:存储Paging Structure中根目录的结构,如EPT中的eptp
  • root_level:Host Paging Structure中根目录的级别(如64位支持paging的系统可以支持level=4的页结构)
  • shadow_root_level:SPT Paging Structure中根目录的级别(如64位支持paging的系统可以支持level=4的EPT页结构)
  • base_role:创建MMU页面时采用的基本的page role(下面描述摘自[3],就不翻译了)
    • role.level: 
      The level in the shadow paging hierarchy that this shadow page belongs to. 1=4k sptes, 2=2M sptes, 3=1G sptes, etc.
    • role.direct: 
      If set, leaf sptes reachable from this page are for a linear range. Examples include real mode translation, large guest pages backed by small host pages, and gpa->hpa translations when NPT or EPT is active. The linear range starts at (gfn << PAGE_SHIFT) and its size is determined by role.level (2MB for first level, 1GB for second level, 0.5TB for third level, 256TB for fourth level) If clear, this page corresponds to a guest page table denoted by the gfn field.
    • role.quadrant: 
      When role.cr4_pae=0, the guest uses 32-bit gptes while the host uses 64-bit sptes. That means a guest page table contains more ptes than the host, so multiple shadow pages are needed to shadow one guest page. For first-level shadow pages, role.quadrant can be 0 or 1 and denotes the first or second 512-gpte block in the guest page table.  For second-level page tables, each 32-bit gpte is converted to two 64-bit sptes (since each first-level guest page is shadowed by two first-level shadow pages) so role.quadrant takes values in the range 0..3.  Each quadrant maps 1GB virtual address space.
    • role.access:
      Inherited guest access permissions in the form uwx.  Note execute  permission is positive, not negative.
    • role.invalid:
      The page is invalid and should not be used.  It is a root page that is  currently pinned (by a cpu hardware register pointing to it); once it is  unpinned it will be destroyed.
    • role.cr4_pae:
      Contains the value of cr4.pae for which the page is valid (e.g. whether  32-bit or 64-bit gptes are in use).
    • role.nxe:
      Contains the value of efer.nxe for which the page is valid.
    • role.cr0_wp:
      Contains the value of cr0.wp for which the page is valid.
    • role.smep_andnot_wp:
      Contains the value of cr4.smep && !cr0.wp for which the page is valid  (pages for which this is true are different from other pages; see the  treatment of cr0.wp=0 below).
  • direct_map:该MMU是否保证存储的页结构和VCPU使用的页结构的一致性。如果为true则每次MMU内容时都会刷新VCPU的TLB,否则需要手动同步。
  • permissions:在page fault处理时不同page fault error code对应的权限,权限由 ACC_* 系列宏指定
  • last_pte_bitmap:上一次访问的pte
  • nx:对应CPU efer.nx,详见Intel手册
  • pdptrs:Guest的页表结构,对应VMCS中GUEST_PDPTR0、GUEST_PDPTR1、GUEST_PDPTR2和GUEST_PDPTR3,参考ept_save_pdptrsept_load_pdptrs函数。

三、struct kvm_mmu_page的结构

kvm_mmu_page结构是KVM MMU中最重要的结构之一,其存储了KVM MMU页表的结构。如下:

<arch/x86/include/asm/kvm_host.h>
struct kvm_mmu_page {
	struct list_head link;
	struct hlist_node hash_link;

	/*
	 * The following two entries are used to key the shadow page in the
	 * hash table.
	 */
	gfn_t gfn;
	union kvm_mmu_page_role role;

	u64 *spt;
	/* hold the gfn of each spte inside spt */
	gfn_t *gfns;
	bool unsync;
	int root_count;          /* Currently serving as active root */
	unsigned int unsync_children;
	unsigned long parent_ptes;	/* Reverse mapping for parent_pte */

	/* The page is obsolete if mmu_valid_gen != kvm->arch.mmu_valid_gen.  */
	unsigned long mmu_valid_gen;

	DECLARE_BITMAP(unsync_child_bitmap, 512);

#ifdef CONFIG_X86_32
	/*
	 * Used out of the mmu-lock to avoid reading spte values while an
	 * update is in progress; see the comments in __get_spte_lockless().
	 */
	int clear_spte_count;
#endif

	/* Number of writes since the last time traversal visited this page.  */
	int write_flooding_count;
};

各个域解释如下:
  • link:link将该结构链接到kvm->arch.active_mmu_pages和invalid_list上,标注该页结构不同的状态
  • hash_link:hash_link将该结构链接到kvm->arch.mmu_page_hash哈希表上,以便进行快速查找,hash key由接下来的gfn和role决定
  • gfn:在直接映射中存储线性地址的基地址;在非直接映射中存储guest page table,该PT包含了由该页映射的translation。非直接映射不常见
  • role:该页的“角色”,详细参见上文对union kvm_mmu_page_role的说明
  • spt:对应的SPT/EPT页地址,SPT/EPT页的struct page结构中page->private域会反向指向该struct kvm_mmu_page。该域可以指向一个lower-level shadow pages,也可以指向真正的数据page。
  • parent_ptes:指向上一级spt
  • unsync:该域只对页结构的叶子节点有效,可以执行该页的翻译是否与guest的翻译一致。如果为false,则可能出现修改了该页中的pte但没有更新tlb,而guest读取了tlb中的数据,导致了不一致。
  • root_count:该页被多少个vcpu作为根页结构
  • unsync_children:记录该页结构下面有多少个子节点是unsync状态的
  • mmu_valid_gen:该页的generation number。KVM维护了一个全局的的gen number(kvm->arch.mmu_valid_gen),如果该域与全局的gen number不相等,则将该页标记为invalid page。该结构用来快速的碾压掉KVM的MMU paging structure。例如,如果想废弃掉当前所有的MMU页结构,需要处理掉所有的MMU页面和对应的映射;但是通过该结构,可以直接将kvm->arch.mmu_valid_gen加1,那么当前所有的MMU页结构都变成了invalid,而处理掉页结构的过程可以留给后面的过程(如内存不够时)再处理,可以加快废弃所有MMU页结构的速度。当mmu_valid_gen值达到最大时,可以调用kvm_mmu_invalidate_zap_all_pages手动废弃掉所有的MMU页结构。
  • unsync_child_bitmap:记录了unsync的子结构的位图
  • clear_spte_count:仅针对32位host有效,具体作用参考函数__get_spte_lockless的注释
  • write_flooding_count:在写保护模式下,对于任何一个页的写都会导致KVM进行一次emulation。对于叶子节点(真正指向数据页的节点),可以使用unsync状态来保护频繁的写操作不会导致大量的emulation,但是对于非叶子节点(paging structure节点)则不行。对于非叶子节点的写emulation会修改该域,如果写emulation非常频繁,KVM会unmap该页以避免过多的写emulation。


四、KVM地址翻译流程

【声明】该节部分内容参考博文 http://blog.csdn.net/lux_veritas/article/details/9284635,感谢原作者 Lux_Veritas。
【声明】该节图片转自 http://blog.csdn.net/wdwbw/article/details/9250049,感谢原作者 wdwbw

Intel EPT(和AMD NPT)使用了Two-dimentional Paging的结构来实现地址翻译,我习惯翻译成“ 两级页表结构”(有些人翻译成“二维地址结构”或“二维页表结构”)。与传统的SPT(Shadow Page Table)不同,传统MMU虚拟化没有硬件支持,GVA到HPA的翻译需要经过GVA->GPA->HVA->HPA的翻译过程,带来很大的代价;Xen的半虚拟化(Para-virtualization)机制从而提供了SPT的支持,即SPT直接映射了GVA->HPA,省略了中间的映射过程。SPT在最初建立时是个空的页结构,Guest VM对某个页面的第一次访问走完整的地址翻译流程(即 GVA->GPA->HVA->HPA),并且建立SPT GVA->HPA的映射。该结构中GVA直接映射到HPA,页表只有“一级”。参考如下图片:

但是该结构的缺陷在于不能够提供透明的虚拟机访存机制。Intel EPT采用与之对应的两级页表结构,即虚拟机内部维护自己的GVA->GPA页表结构(如果虚拟机打开了paging的支持),由硬件辅助维护EPT/NPT paging structure来进行GPA->HPA的映射。这里所谓的“两级”即虚拟机系统维护自己的页表,透明的进行地址翻译;VMM负责将虚拟机请求的GPA映射到宿主机的物理地址。

下图表示了由Guest OS得到其GPA之后,如何找到其对应的HPA的示意图:



五、EPT页表建立流程

在初始化时,会将 arch.mmu. root_hpa置成INVALID_PAGE,而在虚拟机的入口函数 vcpu_enter_guest中调用 kvm_mmu_reload函数来完成EPT根页表的初始化。

<arch/x86/kvm/x86.c>
static int vcpu_enter_guest(struct kvm_vcpu *vcpu)
{
...
    r = kvm_mmu_reload(vcpu);
...
}

<arch/x86/kvm/mmu.h>
static inline int kvm_mmu_reload(struct kvm_vcpu *vcpu)
{
	if (likely(vcpu->arch.mmu.root_hpa != INVALID_PAGE))
		return 0;

	return kvm_mmu_load(vcpu);
}

<arch/x86/kvm/mmu.c>
int kvm_mmu_load(struct kvm_vcpu *vcpu)
{
	int r;

	r = mmu_topup_memory_caches(vcpu);
	if (r)
		goto out;
	r = mmu_alloc_roots(vcpu);
	kvm_mmu_sync_roots(vcpu);
	if (r)
		goto out;
	/* set_cr3() should ensure TLB has been flushed */
	vcpu->arch.mmu.set_cr3(vcpu, vcpu->arch.mmu.root_hpa);
out:
	return r;
}

可以看到在 kvm_mmu_load函数中调用了 mmu_alloc_roots函数来初始化根目录的页面,并调用 arch.mmu.set_cr3(实际为 vmx_set_cr3)来设置Guest的CR3寄存器。


五、EPT页表缺页处理流程

Intel EPT相关的VMEXIT有两个:
  • EPT Misconfiguration:EPT 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
当Guest第一次访问某页面时,首先触发的是Guest OS的page fault,Guest OS会修复好自己mmu的页结构,并且访问对应的GPA,此时由于对应的EPT结构还没有建立,会触发EPT Violation。对于Intel EPT,EPT缺页的处理在函数 tdp_page_fault中。

tdp_page_fault 调用路径如下图(非fast page fault,非异步page fault):


可以看到,首先通过try_async_pf从gfn获得pfn,之后调用__direct_map函数进行映射。

__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中,对非叶子节点的处理调用 kvm_mmu_get_page获得一页新的mmu page,之后调用 link_shadow_page调用 mmu_set_spte将其加入到EPT paging structure中。对于叶子节点,则直接调用 mmu_set_sptemmu_set_spte调用 set_spte来设置spte。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


六、反向映射(Reverse Map)

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

如果通过反向映射计算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)获得链表中的下一个元素
struct pte_list_desc结构如下:

<arch/x86/kvm/mmu.c>
struct pte_list_desc {
	u64 *sptes[PTE_LIST_EXT];
	struct pte_list_desc *more;
};

PTE_LIST_EXT定义的数组是用来对齐一个cache line,在pte_list_add中,desc->sptes[0] = (u64 *)*pte_list,desc->sptes[1] = spte。

反向映射在 mmu_set_spte中被添加。





引用:
[3] kernel目录中Documentation/virtual/kvm/mmu.txt