4.1. 内存虚拟化之基本原理


文章目录

  • 1. 客户机物理地址空间
  • 2. 内存虚拟化的主要功能
  • 3. 传统的地址转换
  • 4. 虚拟机的内存结构
  • 5. 四种地址以及转换关系
    • 5.1. 客户机虚拟地址到客户机物理地址: GVA -> GPA
    • 5.2. 客户机物理地址到主机虚拟地址: GPA -> HVA
    • 5.3. 主机虚拟地址到主机物理地址: HVA -> HPA
  • 6. 地址转换过程总结
    • 6.1. 两种内存虚拟化方案
  • 7. 影子页表(Shadow Page Table, SPT)
    • 7.1. 影子映射关系
    • 7.2. 影子页表的建立
    • 7.3. 影子页表的填充
    • 7.4. 影子页表的缓存
    • 7.5. 影子页表异常处理机制
    • 7.6. 影子页表方案总结
  • 8. EPT页表
    • 8.1. 地址转换流程
    • 8.2. EPT页表的建立流程

1. 客户机物理地址空间

为了实现内存虚拟化,让客户机使用一个隔离的从零开始且具有连续的内存空间,KVM 引入一层新的地址空间,即客户机物理地址空间 (Guest Physical Address, GPA),这个地址空间并不是真正的物理地址空间,它只是宿主机虚拟地址空间客户机地址空间的一个映射。

客户机来说,客户机物理地址空间都是从零开始的连续地址空间,但对于宿主机来说,客户机的物理地址空间不一定是连续的,客户机物理地址空间有可能映射在若干个不连续的宿主机地址区间,如下图 1 所示:

在这里插入图片描述

2. 内存虚拟化的主要功能

QEMU-KVM 的内存虚拟化是由 QEMU 和 KVM 二者共同实现的,其本质上是一个将 Guest 虚拟内存转换成 Host 物理内存的过程。

概括来看,主要有以下几点:

  • Guest 启动时,由 QEMU 从它的进程地址空间申请内存并分配给 Guest 使用,即内存的申请是在用户空间QEMU完成的
  • 通过 KVM 提供的 API,QEMUGuest 内存的地址信息传递并注册到 KVM 中维护,即内存的管理是由内核空间KVM 实现的
  • 整个转换过程涉及 GVA、GPA、HVA、HPA 四种地址Guest 的物理地址空间QEMU 的虚拟地址空间中分配
  • 内存虚拟化的关键在于维护 GPA 到 HVA映射关系,Guest 使用的依然是 Host 的物理内存

3. 传统的地址转换

64 位 CPU 上支持 48 位的虚拟地址寻址空间,和 52 位的物理地址寻址空间。

Linux 采用 4 级页表机制将虚拟地址(VA)转换成物理地址(PA),先从页表的基地址寄存器CR3中读取页表的起始地址,然后加上页号得到对应的页表项,从中取出页的物理地址,加上偏移量就得到 PA。

在这里插入图片描述

4. 虚拟机的内存结构

QEMU 利用mmap系统调用,在进程的虚拟地址空间中申请连续大小的空间,作为 Guest 的物理内存。

QEMU 作为 Host 上的一个进程运行,Guest 的每个 vCPU 都是 QEMU 进程的一个子线程。而 Guest 实际使用的仍是 Host 上的物理内存,因此对于 Guest 而言,在进行内存寻址时需要完成以下地址转换过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
  Guest虚拟内存地址(GVA)
          |
    Guest线性地址
          |
   Guest物理地址(GPA)
          |             Guest
   ------------------
          |             Host
    Host虚拟地址(HVA)
          |
      Host线性地址
          |
    Host物理地址(HPA)

其中,虚拟地址到线性地址的转换过程可以省略,因此 KVM 的内存寻址主要涉及以下四种地址的转换:

1
2
3
4
5
6
7
8
9
  Guest虚拟内存地址(GVA)
          |
   Guest物理地址(GPA)
          |             Guest
  ------------------
          |             Host
    Host虚拟地址(HVA)
          |
    Host物理地址(HPA)

其中,GVA->GPA的映射由 Guest OS 维护,HVA->HPA的映射由 Host OS 维护,因此需要一种机制,来维护GPA->HVA之间的映射关系。

在这里插入图片描述

常用的实现有SPT(Shadow Page Table)和EPT/NPT,前者通过软件维护影子页表,后者通过硬件特性实现二级映射。

5. 四种地址以及转换关系

  1. GVA - Guest虚拟地址

  2. GPA - Guest物理地址

  3. HVA - Host虚拟地址

  4. HPA -Host物理地址

5.1. 客户机虚拟地址到客户机物理地址: GVA -> GPA

Guest OS维护的页表进行传统的操作, 客户机页表

5.2. 客户机物理地址到主机虚拟地址: GPA -> HVA

由于客户机物理地址不能直接用于宿主机物理MMU进行寻址,所以需要把客户机物理地址转换成宿主机虚拟地址 (Host Virtual Address, HVA).

KVM的虚拟机实际上运行在Qemu的进程上下文中。于是,虚拟机的物理内存实际上是Qemu进程的虚拟地址

Kvm要把虚拟机的物理内存分成几个slot。这是因为,对计算机系统来说,物理地址不连续的,除了bios显存要编入内存地址,设备的内存也可能映射到内存了,所以内存实际上是分为一段段的。

在这里插入图片描述

为此,KVM用一个kvm_memory_slot数据结构来记录每一个地址区间映射关系,此数据结构包含了对应此映射区间的起始客户机页帧号 (Guest Frame Number, GFN),映射的内存页数目以及起始宿主机虚拟地址

于是 KVM就可以实现对客户机物理地址宿主机虚拟地址之间的转换,也即

  • 首先根据客户机物理地址找到对应的映射区间
  • 然后根据此客户机物理地址此映射区间偏移量就可以得到其对应的宿主机虚拟地址

5.3. 主机虚拟地址到主机物理地址: HVA -> HPA

通过宿主机的页表

6. 地址转换过程总结

Guest OS所维护的页表负责传统的从guest虚拟地址GVAguest物理地址GPA的转换。如果MMU直接装载guest OS所维护的页表来进行内存访问,那么由于页表中每项所记录的都是GPAMMU无法实现地址翻译

由于宿主机MMU不能直接装载客户机的页表!!! 来进行内存访问,所以当客户机访问宿主机物理内存时,需要经过多次地址转换。即:

  • 首先根据客户机页表客户机虚拟地址转换成客户机物理地址
  • 然后再通过客户机物理地址宿主机虚拟地址之间的映射转换成宿主机虚拟地址
  • 最后再根据宿主机页表把宿主机虚拟地址转换成宿主机物理地址。

注意: 客户机页表基地址(即客户机CR3)是客户机物理地址, 当加载CR3时可以直接通过kvm_memory_slot进行转换成宿主机虚拟地址, 然后在宿主机进行页表转换, 得到客户机页表基地址的真实物理地址.

6.1. 两种内存虚拟化方案

显然通过这种映射方式,客户机每次内存访问都需要 KVM 介入!!!,并由软件进行多次地址转换,其效率是非常低的。

因此,为了提高 GVA 到 HPA 转换的效率,KVM 提供了两种实现方式来进行客户机虚拟地址到宿主机物理地址之间的直接转换。

其一是基于纯软件的实现方式,也即通过影子页表 (Shadow Page Table) 来实现客户虚拟地址宿主机物理地址之间的直接转换。

其二是基于硬件对虚拟化的支持,来实现两者之间的转换。下面就详细阐述两种方法在 KVM 上的具体实现。

7. 影子页表(Shadow Page Table, SPT)

作用:GVA直接到HPA的地址翻译, 真正被VMM载入到物理MMU中的页表影子页表

而通过影子页表,则可以实现客户机虚拟地址到宿主机物理地址的直接转换。如下图所示:

在这里插入图片描述

KVM 通过维护记录GVA->HPA的影子页表 SPT,减少了地址转换带来的开销,可以直接GVA 转换为 HPA

在软件虚拟化的内存转换中,GVA 到 GPA 的转换通过查询 CR3 寄存器来完成,CR3 中保存了 Guest 的页表基地址,然后载入 MMU 中进行地址转换。

在加入了 SPT 技术后,当 Guest 访问 CR3 时,KVM 会捕获到这个操作EXIT_REASON_CR_ACCESS,之后 KVM 会载入特殊的 CR3影子页表,欺骗 Guest 这就是真实的 CR3。之后就和传统的访问内存方式一致,当需要访问物理内存的时候,只会经过一层影子页表的转换。

在这里插入图片描述

影子页表由 KVM 维护,实际上就是一个 Guest 页表到 Host 页表的映射。KVM 会将 Guest 的页表设置为只读,当 Guest OS 对页表进行修改时就会触发 Page Fault,VM-EXIT 到 KVM,之后 KVM 会对 GVA 对应的页表项进行访问权限检查,结合错误码进行判断:

  • 如果是 Guest OS 引起的,则将该异常注入回去,Guest OS 将调用自己的缺页处理函数,申请一个 Page,并将 Page 的 GPA 填充到上级页表项中
  • 如果是 Guest OS 的页表和 SPT 不一致引起的,则同步 SPT,根据 Guest 页表和 mmap 映射找到 GPA 到 HVA 的映射关系,然后在 SPT 中增加/更新GVA-HPA表项

当 Guest 切换进程时,会把带切换进程的页表基址载入到 Guest 的 CR3 中,导致 VM-EXIT 到 KVM 中。KVM 再通过哈希表找到对应的 SPT,然后加载到机器的 CR3 中。

影子页表的引入,减少了GVA->HPA的转换开销,但是缺点在于需要为 Guest 的每个进程都维护一个影子页表,这将带来很大的内存开销。同时影子页表的建立是很耗时的,如果 Guest 的进程过多,将导致影子页表频繁切换。因此 Intel 和 AMD 在此基础上提供了基于硬件的虚拟化技术。

影子页表简化了地址转换过程,实现了客户机虚拟地址空间宿主机物理地址空间直接映射。但是由于客户机每个进程都有自己的虚拟地址空间,所以KVM需要为客户机中的每个进程页表都要维护一套相应的影子页表

客户机访问内存时,真正被装入宿主机MMU的是客户机当前页表所对应的影子页表,从而实现了从客户机虚拟地址到宿主机物理地址的直接转换。而且,在 TLB 和 CPU 缓存上缓存的是来自影子页表客户机虚拟地址宿主机物理地址之间的映射,也因此提高了缓存的效率。

影子页表中,每个页表项指向的都是宿主机的物理地址。这些表项是随着客户机操作系统客户机页表修改相应地建立的。客户机中的每一个页表项都有一个影子页表项与之相对应。如下图 3 所示:

在这里插入图片描述

为了快速检索客户机页表所对应的的影子页表,KVM 为每个客户机都维护了一个哈希表影子页表客户机页表通过此哈希表进行映射

对于每一个客户机来说,客户机的页目录页表都有唯一客户机物理地址,通过页目录 / 页表的客户机物理地址就可以在哈希链表中快速地找到对应的影子页目录 / 页表

检索哈希表时,KVM客户机页目录 / 页表客户机物理地址低 10 位作为键值进行索引,根据其键值定位到对应的链表,然后遍历此链表找到对应的影子页目录/页表。当然,如果不能发现对应的影子页目录 / 页表,说明 KVM没有为其建立,于是 KVM 就为其分配新的物理页并加入此链表,从而建立起客户机页目录 / 页表和对应的影子页目录 / 页表之间的映射。

客户机切换进程时,客户机操作系统会把待切换进程的页表基址载入 CR3,而 KVM 将会截获这一特权指令,进行新的处理,也即在哈希表中找到与此页表基址对应的影子页表基址,载入客户机 CR3,使客户机在恢复运行时 CR3 实际指向的是新切换进程对应的影子页表

7.1. 影子映射关系

SPD是PD的影子页表,SPT1/SPT2是PT1/PT2的影子页表。由于客户PDE和PTE给出的页表基址和页基址并不是真正的物理地址,所以我们采用虚线表示PDE到GUEST页表以及PTE到普通GUEST页的映射关系。

在这里插入图片描述

7.2. 影子页表的建立

  • 开始时,VMM中的与guest OS所拥有的页表相对应的影子页表是空的;
  • 而影子页表又是载入到CR3中真正为物理MMU所利用进行寻址的页表,因此开始时任何的内存访问操作都会引起缺页异常;导致vm发生VM Exit;进入 handle_exception();
1
2
3
4
5
6
7
8
9
10
if (is_page_fault(intr_info)) {
        /* EPT won't cause page fault directly */
        BUG_ON(enable_ept);
        cr2 = vmcs_readl(EXIT_QUALIFICATION);
        trace_kvm_page_fault(cr2, error_code);

        if (kvm_event_needs_reinjection(vcpu))
            kvm_mmu_unprotect_page_virt(vcpu, cr2);
        return kvm_mmu_page_fault(vcpu, cr2, error_code, NULL, 0);
    }

获得缺页异常发生时的CR2,及当时访问的虚拟地址;
进入kvm_mmu_page_fault()(vmx.c)->
r = vcpu->arch.mmu.page_fault(vcpu, cr2, error_code);(mmu.c)->
FNAME(page_fault)(struct kvm_vcpu *vcpu, gva_t addr, u32 error_code)(paging_tmpl.h)->
FNAME(walk_addr)() 查guest页表,物理地址是否存在, 这时肯定是不存在的
The page is not mapped by the guest. Let the guest handle it.
inject_page_fault()->kvm_inject_page_fault() 异常注入流程;

Guest OS修改从GVA->GPA的映射关系填入页表;
继续访问,由于影子页表仍是空,再次发生缺页异常;
FNAME(page_fault)->
FNAME(walk_addr)() 查guest页表,物理地址映射均是存在->
FNAME(fetch):
遍历影子页表,完成创建影子页表(填充影子页表);
在填充过程中,将客户机页目录结构页对应影子页表页表项标记为写保护,目的截获对于页目录的修改(页目录也是内存页的一部分,在页表中也是有映射的,guest对页目录有写权限,那么在影子页表的页目录也是可写的,这样对页目录的修改导致VMM失去截获的机会)

7.3. 影子页表的填充

1
2
3
4
5
6
7
shadow_page = kvm_mmu_get_page(vcpu, table_gfn, addr, level-1, direct, access, sptep);
index = kvm_page_table_hashfn(gfn);
hlist_for_each_entry_safe
if (sp->gfn == gfn)
{……}
else
{sp = kvm_mmu_alloc_page(vcpu, parent_pte);}

为了快速检索GUEST页表所对应的的影子页表,KVM 为每个GUEST都维护了一个哈希
表,影子页表和GUEST页表通过此哈希表进行映射。对于每一个GUEST来说,GUEST
的页目录和页表都有唯一的GUEST物理地址,通过页目录/页表的客户机物理地址就
可以在哈希链表中快速地找到对应的影子页目录/页表。

7.4. 影子页表的缓存

  • Guest OS修改从GVA->GPA的映射关系,为保证一致性,VMM必须对影子页表也做相应的维护,这样,VMM必须截获这样的内存访问操作;
  • 导致VM Exit的机会
    • INVLPG
    • MOV TO CR3
    • TASK SWITCH(发生MOV TO CR3 )
  • 以INVLPG触发VM Exit为例:
  • static void FNAME(invlpg)(struct kvm_vcpu *vcpu, gva_t gva)
    • Paging_tmpl.h
    • 影子页表项的内容无效
  • GUEST在切换CR3时,VMM需要清空整个TLB,使所有影子页表的内容无效。在多进程GUEST操作系统中,CR3将被频繁地切换,某些影子页表的内容可能很快就会被再次用到,而重建影子页表是一项十分耗时的工作,这里需要缓存影子页表,即GUEST切换CR3时不清空影子页表。

7.5. 影子页表异常处理机制

在通过影子页表进行寻址的过程中,有两种原因会引起影子页表的缺页异常,一种是由客户机本身所引起的缺页异常,具体来说就是客户机所访问的客户机页表项存在位 (Present Bit) 为 0,或者写一个只读的客户机物理页,再者所访问的客户机虚拟地址无效等。另一种异常是由客户机页表影子页表不一致引起的异常。

缺页异常发生时,KVM 首先截获该异常,然后对发生异常的客户机虚拟地址客户机页表中所对应页表项的访问权限进行检查,并根据引起异常的错误码,确定出此异常的原因,进行相应的处理。

如果该异常是由客户机本身引起的,KVM 则直接把该异常交由客户机的缺页异常处理机制来进行处理。

如果该异常是由客户机页表影子页表不一致引起的,KVM 则根据客户机页表同步影子页表。为此,KVM 要建立起相应的影子页表数据结构,填充宿主机物理地址影子页表的页表项,还要根据客户机页表项的访问权限修改影子页表对应页表项的访问权限。

由于影子页表可被载入物理 MMU客户机直接寻址使用, 所以客户机的大多数内存访问都可以在没有 KVM 介入的情况下正常执行,没有额外的地址转换开销,也就大大提高了客户机运行的效率。但是影子页表的引入也意味着 KVM 需要为每个客户机每个进程的页表都要维护一套相应的影子页表,这会带来较大内存上的额外开销,此外,客户机页表和和影子页表的同步也比较复杂。

因此,Intel 的 EPT(Extent Page Table) 技术和 AMD 的 NPT(Nest Page Table) 技术都对内存虚拟化提供了硬件支持。这两种技术原理类似,都是在硬件层面上实现客户机虚拟地址到宿主机物理地址之间的转换。下面就以 EPT 为例分析一下 KVM 基于硬件辅助的内存虚拟化实现。

7.6. 影子页表方案总结

内存虚拟化的两次转换:

  • GVA->GPA (GUEST的页表实现)
  • GPA->HPA (VMM进行转换)

影子页表将两次转换合一:

根据GVA->GPA->HPA计算出GVA->HPA,填入影子页表

优点:

由于影子页表可被载入物理 MMU 为客户机直接寻址使用,所以客户机的大多数内存访问都可以在没有 KVM 介入的情况下正常执行,没有额外的地址转换开销,也就大大提高了客户机运行的效率。

缺点:

1、KVM 需要为每个客户机的每个进程的页表都要维护一套相应的影子页表,这会带来较大内存上的额外开销;

2、客户在读写CR3、执行INVLPG指令或客户页表不完整等情况下均会导致VM exit,这导致了内存虚拟化效率很低

3、客户机页表和和影子页表的同步也比较复杂。

因此,Intel 的 EPT(Extent Page Table) 技术和 AMD 的 NPT(Nest Page Table) 技术都对内存虚拟化提供了硬件支持。这两种技术原理类似,都是在硬件层面上实现客户机虚拟地址到宿主机物理地址之间的转换。

8. EPT页表

在这里插入图片描述

Intel EPT 技术引入了 EPT(Extended Page Table)和 EPTP(EPT base pointer)的概念。EPT 中维护着 GPA 到 HPA 的映射,而 EPTP 负责指向 EPT。

EPT 技术在原有客户机页表客户机虚拟地址客户机物理地址映射的基础上,又引入了 EPT 页表来实现客户机物理地址宿主机物理地址的另一次映射.

两次地址映射都是由硬件自动完成, 二维地址翻译结构:

  • Guest维护自身的客户页表: GVA->GPA
  • EPT维护 GPA->HPA 的映射

客户机运行时,客户机页表被载入 物理CR3,而 EPT 页表被载入专门的 EPT 页表指针寄存器 EPTP。于是在进行地址转换时,首先通过 CR3 指向的页表实现 GVA 到 GPA 的转换,再通过 EPTP 指向的 EPT 完成 GPA 到 HPA 的转换。当发生 EPT Page Fault 时,需要 VM-EXIT 到 KVM,更新 EPT。

  • 优点:Guest 的缺页在 Guest OS 内部处理,不会 VM-EXIT 到 KVM 中。地址转化基本由硬件(MMU)查页表来完成,大大提升了效率,且只需为 Guest 维护一份 EPT 页表,减少内存的开销
  • 缺点:两级页表查询,只能寄望于 TLB 命中

EPT 页表对地址的映射机理客户机页表对地址的映射机理相同,下图 4 出示了一个页面大小为 4K 的映射过程:

在这里插入图片描述

8.1. 地址转换流程

  1. 处于non-root模式CPU加载guest进程的gCR3;
  2. gCR3是GPA,cpu需要通过查询EPT页表来实现GPA->HPA
  3. 如果没有,CPU触发EPT Violation, 由VMM截获处理
  4. 假设客户机m级页表宿主机EPTn级,在TLB均miss的最坏情况下,会产生m*n次内存访问,完成一次客户机的地址翻译

在这里插入图片描述

8.2. EPT页表的建立流程

  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对应在真实物理内存单元中的内容,便可通过这一套二维页表结构获得。

客户机物理地址宿主机物理地址转换的过程中,由于缺页、写权限不足等原因也会导致客户机退出,产生 EPT 异常

对于 EPT 缺页异常,KVM 首先根据引起异常的客户机物理地址,映射到对应的宿主机虚拟地址!!!,然后为此虚拟地址分配新的物理页,最后 KVM 再更新 EPT 页表,建立起引起异常的客户机物理地址宿主机物理地址之间的映射。对 EPT 写权限引起的异常,KVM 则通过更新相应的 EPT 页表来解决。

由此可以看出,EPT 页表相对于前述的影子页表,其实现方式大大简化。而且,由于客户机内部的缺页异常不会致使客户机退出,因此提高了客户机运行的性能。此外,KVM 只需为每个客户机维护一套 EPT 页表,也大大减少了内存的额外开销