2015年11月18日星期三

KVM MMU

The functions of MMU Virtualization
  •  Translate guest physical address to the specified host physical address
  •  Control the memory access permission
           – R/W, NX, U/S
  •  Track Accessed/Dirty bits of guest page table


 KVM采用了kvm_memory_slot结构来对应Qemu中的AddressSpace。Qemu将虚拟机的物理地址在KVM中注册为多个内存槽,如BIOS、MMIO、GPU、RAW。相关概念:

  • 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,宿主机的页框号 
  • PFN - host page frame number, 宿主页框号
  • PTE - page table entry,通用来指代页结构项
  • tdp - two dimentional paging(供应商中立术语,可以是NPT和EPT)
 
 
 

 1  内存槽(slot)的注册和管理


 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; //标志位
    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 Hugepage definitions for x86 */
#define KVM_NR_PAGE_SIZES   3



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



而结构体kvm_memslots包含struct kvm_memory_slot的数组:
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)
{
   ...
   struct kvm *kvm = kvm_arch_alloc_vm();
   ...
    spin_lock_init(&kvm->mmu_lock);
    kvm->mm = current->mm;
    atomic_inc(&kvm->mm->mm_count);
    kvm_eventfd_init(kvm);
    mutex_init(&kvm->lock);
    mutex_init(&kvm->irq_lock);
    mutex_init(&kvm->slots_lock);
    atomic_set(&kvm->users_count, 1);
    INIT_LIST_HEAD(&kvm->devices);
   ...
}
<include/linux/kvm_host.h>
static inline struct kvm *kvm_arch_alloc_vm()
{
    return kzalloc(sizeof(struct kvm), GFP_KERNEL);
}

<include/linux/slab.h>
/**
 * kzalloc - allocate memory. The memory is set to zero.
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate (see kmalloc).
 */
static inline void *kzalloc(size_t size, gfp_t flags)                                                                                                                            
{
    return kmalloc(size, flags | __GFP_ZERO);
}

/**
 * kmalloc - allocate memory  
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate.
 *
 * kmalloc is the normal method of allocating memory
 * for objects smaller than page size in the kernel.
*/
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
}

内存槽的注册入口在kvm_vm_ioctl函数中case KVM_SET_USER_MEMORY_REGION部分,最终调用函数__kvm_set_memory_region在KVM中建立与Qemu相对应的内存槽结构。
 case KVM_SET_USER_MEMORY_REGION: {
 r = kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem);
}  
kvm_vm_ioctl_set_memory_region() =>kvm_set_memory_region(kvm, mem) =>__kvm_set_memory_region(kvm, mem)
__kvm_set_memory_region(kvm, mem)的作用是Allocate some memory and give it an address in the guest physical address space.负责分配一段内存空间并赋予其在虚拟机物理地址空间的地址。且允许使用不连续的内存空间。
__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



2   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的创建工作。
 kvm_arch_vcpu_create=>kvm_x86_ops->vcpu_create()=>vmx_create_vcpu
<arch/x86/kvm/vmx.c>
 static struct kvm_x86_ops vmx_x86_ops = { 
...
.vcpu_create = vmx_create_vcpu,
...
err = kvm_vcpu_init(&vmx->vcpu, kvm, id);
...
}


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_cachemmu_page_header_cache,也就是说如果这两个cache中缓存的object数目不够,则会从上述对应的kmem_cache中获取,对应的代码可以参考函数mmu_topup_memory_cache
 static int mmu_topup_memory_cache(struct kvm_mmu_memory_cache *cache,  struct kmem_cache *base_cache, int min)
{
    void *obj;

    if (cache->nobjs >= min)
        return 0;
    while (cache->nobjs < ARRAY_SIZE(cache->objects)) {
        obj = kmem_cache_zalloc(base_cache, GFP_KERNEL);
        if (!obj)
            return -ENOMEM;
        cache->objects[cache->nobjs++] = obj;
    }  
    return 0;
}
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。
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。 
<arch/x86/kvm/mmu.c>
 static void init_kvm_mmu(struct kvm_vcpu *vcpu)                                                                                                                                 
{
    if (mmu_is_nested(vcpu))
        init_kvm_nested_mmu(vcpu);
    else if (tdp_enabled)
        init_kvm_tdp_mmu(vcpu);
    else
        init_kvm_softmmu(vcpu);
}
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中唯一做的事情就是初始化了之前提到的arch.mmu(通过arch->walk_mmu初始化的),并且根据host的不同进行不同的初始化过程。

下面我们来看struct kvm_mmu结构,init_kvm_tdp_mmu中几乎初始化了kvm_mmu中所有的域(此处可以参考官方文档: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 (*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];
    u64 bad_mt_xwr;
    /*
     * 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;
        unsigned smap_andnot_wp:1;
        unsigned :8;
         unsigned smm:8;
    };
};
对该结构体中各个域进行说明:
  • set_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地址翻译流程
      为了实现内存虚拟化,让客户机使用一个隔离的、从零开始且具有连续的内存空间,KVM 引入一层新的地址空间,即客户机物理地址空间 (Guest Physical Address, GPA),这个地址空间并不是真正的物理地址空间,它只是宿主机虚拟地址空间在客户机地址空间的一个映射。对客户机来说,客户机物理地址空间都是从零开始 的连续地址空间,但对于宿主机来说,客户机的物理地址空间并不一定是连续的,客户机物理地址空间有可能映射在若干个不连续的宿主机地址区间。
       由于客户机物理地址不能直接用于宿主机物理 MMU 进行寻址,所以需要把客户机物理地址转换成宿主机虚拟地址 (Host Virtual Address, HVA),为此,KVM 用一个 kvm_memory_slot 数据结构来记录每一个地址区间的映射关系,此数据结构包含了对应此映射区间的起始客户机页帧号 (Guest Frame Number, GFN),映射的内存页数目以及起始宿主机虚拟地址。于是 KVM 就可以实现对客户机物理地址到宿主机虚拟地址之间的转换,也即首先根据客户机物理地址找到对应的映射区间,然后根据此客户机物理地址在此映射区间的偏移量 就可以得到其对应的宿主机虚拟地址。进而再通过宿主机的页表也可实现客户机物理地址到宿主机物理地址之间的转换,也即 GPA 到 HPA 的转换。
实 现内存虚拟化,最主要的是实现客户机虚拟地址 (Guest Virtual Address, GVA) 到宿主机物理地址之间的转换。根据上述客户机物理地址到宿主机物理地址之间的转换以及客户机页表,即可实现客户机虚拟地址空间到客户机物理地址空间之间的 映射,也即 GVA 到 HPA 的转换。显然通过这种映射方式,客户机的每次内存访问都需要 KVM 介入,并由软件进行多次地址转换,其效率是非常低的。因此,为了提高 GVA 到 HPA 转换的效率,KVM 提供了两种实现方式来进行客户机虚拟地址到宿主机物理地址之间的直接转换。其一是基于纯软件的实现方式,也即通过影子页表 (Shadow Page Table) 来实现客户虚拟地址到宿主机物理地址之间的直接转换。其二是基于硬件对虚拟化的支持,来实现两者之间的转换。
       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,页表只有“一级”。参考如下图片:
 

       影子页表简化了地址转换过程,实现了客户机虚拟地址空间到宿主机物理地址空间的直接映射。但是由于客户机中每个进程都有自己的虚拟地址空间,所以 KVM 需要为客户机中的每个进程页表都要维护一套相应的影子页表。在客户机访问内存时,真正被装入宿主机 MMU 的是客户机当前页表所对应的影子页表,从而实现了从客户机虚拟地址到宿主机物理地址的直接转换。而且,在 TLB 和 CPU 缓存上缓存的是来自影子页表中客户机虚拟地址和宿主机物理地址之间的映射,也因此提高了缓存的效率。
在影子页表中,每个页表项指向的都是宿主机的物理地址。这些表项是随着客户机操作系统对客户机页表的修改而相应地建立的。客户机中的每一个页表项都有一个影子页表项与之相对应。
       为了快速检索客户机页表所对应的的影子页表,KVM 为每个客户机都维护了一个哈希表,影子页表和客户机页表通过此哈希表进行映射。对于每一个客户机来说,客户机的页目录和页表都有唯一的客户机物理地址,通 过页目录 / 页表的客户机物理地址就可以在哈希链表中快速地找到对应的影子页目录 / 页表。在检索哈希表时,KVM 把客户机页目录 / 页表的客户机物理地址低 10 位作为键值进行索引,根据其键值定位到对应的链表,然后遍历此链表找到对应的影子页目录 / 页表。当然,如果不能发现对应的影子页目录 / 页表,说明 KVM 还没有为其建立,于是 KVM 就为其分配新的物理页并加入此链表,从而建立起客户机页目录 / 页表和对应的影子页目录 / 页表之间的映射。当客户机切换进程时,客户机操作系统会把待切换进程的页表基址载入 CR3,而 KVM 将会截获这一特权指令,进行新的处理,也即在哈希表中找到与此页表基址对应的影子页表基址,载入客户机 CR3,使客户机在恢复运行时 CR3 实际指向的是新切换进程对应的影子页表。
 影子页表异常处理机制
在通过影子页表进行寻址的过程中,有两种原因会引起影子页表的缺页异常,一种是由客户机本身所引起的缺页异常, 具体来说就是客户机所访问的客户机页表项存在位 (Present Bit) 为 0,或者写一个只读的客户机物理页,再者所访问的客户机虚拟地址无效等。另一种异常是由客户机页表和影子页表不一致引起的异常。
当缺页异常 发生时,KVM 首先截获该异常,然后对发生异常的客户机虚拟地址在客户机页表中所对应页表项的访问权限进行检查,并根据引起异常的错误码,确定出此异常的原因,进行相应 的处理。如果该异常是由客户机本身引起的,KVM 则直接把该异常交由客户机的缺页异常处理机制来进行处理。如果该异常是由客户机页表和影子页表不一致引起的,KVM 则根据客户机页表同步影子页表。为此,KVM 要建立起相应的影子页表数据结构,填充宿主机物理地址到影子页表的页表项,还要根据客户机页表项的访问权限修改影子页表对应页表项的访问权限。
由 于影子页表可被载入物理 MMU 为客户机直接寻址使用, 所以客户机的大多数内存访问都可以在没有 KVM 介入的情况下正常执行,没有额外的地址转换开销,也就大大提高了客户机运行的效率。但是影子页表的引入也意味着 KVM 需要为每个客户机的每个进程的页表都要维护一套相应的影子页表,这会带来较大内存上的额外开销,此外,客户机页表和和影子页表的同步也比较复杂。因 此,Intel 的 EPT(Extent Page Table) 技术和 AMD 的 NPT(Nest Page Table) 技术都对内存虚拟化提供了硬件支持。这两种技术原理类似,都是在硬件层面上实现客户机虚拟地址到宿主机物理地址之间的转换。下面就以 EPT 为例分析一下 KVM 基于硬件辅助的内存虚拟化实现。

EPT 页表

  When EPT is in use, the addresses in the paging-structures are not used as physical addresses to access memory and memory-mapped I/O. Instead, they are treated as guest-physical addresses and are translated through a set of EPT paging structures to produce physical addresses.  
 EPT 技术在原有客户机页表对客户机虚拟地址到客户机物理地址映射的基础上,又引入了 EPT 页表来实现客户机物理地址到宿主机物理地址的另一次映射,这两次地址映射都是由硬件自动完成。客户机运行时,客户机页表被载入 CR3,而 EPT 页表被载入专门的 EPT 页表指针寄存器 EPTP。EPT 页表对地址的映射机理与客户机页表对地址的映射机理相同,
 

     
 EPT_2

在客户机物理地址到宿主机物理地址转换的过程中,由于缺页、写权限不足等原因也会导致客户机退出,产生 EPT 异常。对于 EPT 缺页异常,KVM 首先根据引起异常的客户机物理地址,映射到对应的宿主机虚拟地址,然后为此虚拟地址分配新的物理页,最后 KVM 再更新 EPT 页表,建立起引起异常的客户机物理地址到宿主机物理地址之间的映射。对 EPT 写权限引起的异常,KVM 则通过更新相应的 EPT 页表来解决。
       由此可以看出,EPT 页表相对于前述的影子页表,其实现方式大大简化。而且,由于客户机内部的缺页异常也不会致使客户机退出,因此提高了客户机运行的性能。此外,KVM 只需为每个客户机维护一套 EPT 页表,也大大减少了内存的额外开销。
      Intel EPT采用与之对应的两级页表结构,即虚拟机内部维护自己的GVA->GPA页表结构(如果虚拟机打开了paging的支持),由硬件辅助维护 EPT/NPT paging structure来进行GPA->HPA的映射。这里所谓的“两级”即虚拟机系统维护自己的页表,透明的进行地址翻译;VMM负责将虚拟机请求的 GPA映射到宿主机的物理地址。
 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);
...
}

kvm_mmu_reload(vcpu) => 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寄存器。


【转载自博客xelatex KVM】http://blog.csdn.net/xelatex_kvm/article/details/17685123

没有评论:

发表评论