内核虚拟化:shadow mmu(x86)

原文档:The x86 kvm shadow mmu — The Linux Kernel documentation

在 x86 架构的虚拟化环境中,位于 arch/x86/kvm 目录下的 mmu.c 和 mmu.h 文件以及 paging_tmpl.h 文件负责向客户机提供标准的 x86 内存管理单元,并在将客户机的物理地址转换为主机物理地址时执行操作.

描述了 mmu 代码需要满足以下要求:

  • 正确性: - 客户机不应能够确定它是在模拟的 mmu 上运行,除了在时间上(我们试图遵守规范,而不是模拟特定实现的特征,如 TLB 大小)。

  • 安全性: - 客户机不得能够访问未分配给它的主机内存。

  • 性能: - 最小化 mmu 引入的性能惩罚。 

  • 可扩展性: - 需要适应大内存和大 VCPU 客户机。 

  • 硬件: - 支持完整范围的 x86 虚拟化硬件。 

  • 集成: - Linux 内存管理代码必须控制客户机内存,以便交换、页面迁移、页面合并、透明超大页和类似功能可以无需更改而正常工作。

  • 脏页跟踪: -报告对客户机内存的写操作,以启用实时迁移和基于帧缓冲的显示。

  • 占用空间: - 保持内核内存固定的数量较低(大多数内存应该是可收缩的)。

  • 可靠性: - 避免多页或 GFP_ATOMIC 分配。

首字母缩写

==== ==================================================================== pfn 主机页帧编号

hpa 主机物理地址

hva 主机虚拟地址

gfn 客户机页帧编号

gpa 客户机物理地址

gva 客户机虚拟地址

ngpa 嵌套客户机物理地址

ngva 嵌套客户机虚拟地址

pte 页表项(也用于泛指分页结构的条目)

gpte 客户机页表项(指代 gfns)

spte 影子页表项(指代 pfns)

tdp 二维分页(NPT 和 EPT 的供应商中立术语) =======================================================================

支持虚拟的实际硬件

mmu 支持第一代 mmu 硬件,允许在客户机进入期间进行当前分页模式和 cr3 的原子切换,以及二维分页(AMD 的 NPT 和 Intel 的 EPT)。它所模拟的硬件是传统的 2/3/4 级 x86 mmu,支持全局页面、pae、pse、pse36、cr0.wp 和 1GB 页面。模拟硬件还能够在支持 NPT 的主机上公开 NPT 能力。

翻译

mmu 的主要任务是编程处理器的 mmu,以翻译客户机的地址。在不同的时候需要不同的翻译:

  • 当客户机分页被禁用时,我们将客户机物理地址翻译为主机物理地址(gpa->hpa)。

  • 当客户机分页被启用时,我们将客户机虚拟地址翻译为客户机物理地址,再到主机物理地址(gva->gpa->hpa)。

  • 当客户机启动其自己的客户机时,我们将嵌套客户机虚拟地址翻译为嵌套客户机物理地址,再到客户机物理地址,最终到主机物理地址(ngva->ngpa->gpa->hpa)。

主要的挑战是在只支持 1(传统)和 2(tdp)个翻译的硬件中编码 1 到 3 个翻译。当所需的翻译数量与硬件匹配时,mmu 以直接模式运行;否则,它以影子模式运行(见下文)。

内存

客户机内存(gpa)是使用 kvm 的进程的用户地址空间的一部分。用户空间定义了客户机地址和用户地址之间的转换关系(gpa->hva);请注意,两个 gpas 可能映射到相同的 hva,但反之则不成立。

这些 hva 可以使用主机可用的任何方法支持:匿名内存、文件支持的内存和设备内存。内存可能随时由主机进行分页。

事件

mmu 受事件驱动,其中一些来自客户机,一些来自主机。

由客户机生成的事件:

  • 对控制寄存器的写入(特别是 cr3)

  • invlpg/invlpga 指令的执行

  • 对缺失或受保护的翻译的访问

由主机生成的事件:

  • gpa->hpa 转换(通过 gpa->hva 再通过 hva->hpa 的转换)

  • 内存压力(收缩器)

影子页表

主要数据结构是影子页,即 struct kvm_mmu_page。 影子页包含 512 个 spte,可以是叶子 spte 或非叶子 spte。一个影子页可能包含叶子 spte 和非叶子 spte 的混合。

非叶子 spte 允许硬件 mmu 访问叶子页,并且与直接的翻译无关。 它指向其他影子页。

叶子 spte 对应于编码到一个分页结构条目中的一个或两个翻译。 这些总是翻译堆栈的最低级,可选的更高级别翻译留给 NPT/EPT。叶子 pte 指向客户机页。

以下表格显示由叶子页表项编码的翻译,其中括号中是更高级别的翻译:

非嵌套客户机:

+-------------+---------------------+
| Translation |      Expression     |
+-------------+---------------------+
| nonpaging   |       gpa->hpa      |
| paging      |   gva->gpa->hpa     |
| paging, tdp  |   (gva->)gpa->hpa  |
+-------------+---------------------+

嵌套客户机:

+-----------+------------------------+
| Translation  |      Expression      |
+-----------+------------------------+
| non-tdp   |    ngva->gpa->hpa (*) |
| tdp       |   (ngva->)ngpa->gpa->hpa |
+-----------+------------------------+
​
(*) 如果不存在 NPT(Nested Page Tables),客户机的虚拟机监控程序将会将 ngva(嵌套客户机虚拟地址)到 gpa(客户机物理地址)的翻译编码进其页表中。

影子页表包含一下信息:

  • role.level:

    • 影子页在影子分页层次结构中的级别。1=4k sptes,2=2M sptes,3=1G sptes,等等。

  • role.direct:

    • 如果设置,从此页可达的叶子 sptes 是线性范围的。

    • 例如,实模式翻译、由小主机页支持的大客户机页以及在 NPT 或 EPT 激活时的 gpa->hpa 翻译。

    • 线性范围从 (gfn << PAGE_SHIFT) 开始,其大小由 role.level 决定(对于第一级,为2MB;对于第二级,为1GB;对于第三级,为0.5TB;对于第四级,为256TB)。

    • 如果未设置,此页对应于由 gfn 字段表示的客户机页表。

  • role.quadrant:

    • 当 role.has_4_byte_gpte=1 时,客户机使用32位 gpte,而主机使用64位 spte。

    • 对于第一级影子页,role.quadrant 可以是 0 或 1,表示客户机页表中的第一个或第二个 512-gpte 块。

    • 对于第二级页表,每个32位 gpte 被转换为两个64位 spte(因为每个第一级客户机页由两个第一级影子页阴影),所以 role.quadrant 取值范围为0到3。

    • 每个象限映射1GB虚拟地址空间。

  • role.access:

    • 从父 ptes 继承的客户机访问权限,以 uwx 形式表示。注意执行权限是积极的,而不是否定的。

  • role.invalid:

    • 该页无效,不应使用。它是当前被固定(由指向它的 CPU 硬件寄存器指向)的根页;一旦取消固定,它将被销毁。

  • role.has_4_byte_gpte:

    • 反映了页面有效的客户机 PTE 的大小,即在使用直接映射或 64 位 gpte 时为 '0',在使用 32 位 gpte 时为 '1'。

  • role.efer_nx:

    • 包含页面有效的 efer.nx 的值。

  • role.cr0_wp:

    • 包含页面有效的 cr0.wp 的值。

  • role.smep_andnot_wp:

    • 包含页面有效的 cr4.smep && !cr0.wp 的值。

    • 具有这个条件的页面与其他页面不同;请参阅下面对 cr0.wp=0 处理的说明。

  • role.smap_andnot_wp:

    • 包含页面有效的 cr4.smap && !cr0.wp 的值。

    • 具有这个条件的页面与其他页面不同;请参阅下面对 cr0.wp=0 处理的说明。

  • role.smm:

    • 如果页面在系统管理模式中有效,则为1。

    • 此字段确定使用哪个 kvm_memslots 数组构建此影子页;它还用于通过 kvm_memslots_for_spte_role 宏和 gfn_to_memslot 从 struct kvm_mmu_page 返回到 memslot。

  • role.ad_disabled:

    • 如果 MMU 实例无法使用 A/D 位,则为1。

    • 在 Haswell 之前,EPT 没有 A/D 位;如果 L1 hypervisor 不启用 A/D 位,shadow EPT 页表也无法使用 A/D 位。

  • role.passthrough:

    • 该页面没有由客户机页表支持,但其第一个条目指向一个。

    • 如果 NPT 使用 5 级页表(host CR4.LA57=1)并阴影 L1 的 4 级 NPT(L1 CR4.LA57=1),则设置此项。

  • gfn:

    • 表示由此页阴影的翻译所在的客户机页表,或用于线性翻译的基础页帧。参见 role.direct。

  • spt:

    • 一个包含此页翻译的 64 位 spte 的页。

    • 由 kvm 和硬件访问。

    • spt 指向的页将其 page->private 指向影子页结构。

    • spt 中的 spte 可以指向客户机页,也可以指向较低级别的影子页。

    • 具体来说,如果 sp1 和 sp2 是影子页,则 sp1->spt[n] 可能指向 __pa(sp2->spt)。sp2 将通过 parent_pte 回指 sp1。

    • spt 数组形成一个有向无环图结构,其中影子页作为一个节点,客户机页作为叶子。

  • gfns:

    • 一个包含 512 个客户机帧号的数组,每个帧号对应一个存在的 pte。

    • 用于从 pte 到 gfn 的反向映射。当 role.direct 设置时,可以在使用时通过 gfn 字段计算这个数组,此时不需要分配 gfns 数组。参见 role.direct 和 gfn。

  • root_count:

    • 计数器,用于跟踪有多少硬件寄存器(guest cr3 或 pdptrs)当前指向该页。

    • 当此计数器非零时,该页不能被销毁。参见 role.invalid。

  • parent_ptes:

    • pte/ptes 指向此页的 spt 的逆映射。

    • 如果 parent_ptes 的最低位为零,表示只有一个 spte 指向此页,parent_ptes 指向这个单一的 spte;否则,存在多个 spte 指向此页,parent_ptes & ~0x1 指向一个数据结构,其中包含父 spte 的列表。

  • unsync:

    • 如果为 true,则此页中的翻译可能与客户机的翻译不匹配。

    • 这等效于当 pte 更改但在刷新 tlb 条目之前的 tlb 状态。因此,unsync ptes 在客户机执行 invlpg 或通过其他方式刷新其 tlb 时同步。

    • 仅对叶子页有效。

  • unsync_children:

    • 该页中有多少个 spte 指向不同步的页面(或具有不同步的子页面)。

  • unsync_child_bitmap:

    • 一个位图,指示该页的 spte 是否(直接或间接)指向可能不同步的页面。用于快速定位从给定页面可达的所有不同步页面。

  • clear_spte_count:

    • 仅在32位主机上存在,其中64位 spte 无法原子写入。

    • 读者在 MMU 锁不足的情况下使用此计数器来检测正在进行的更新并重试,直到写者完成写入。

  • write_flooding_count:

    • 客户机可能多次写入一个页表,如果需要将该页写保护,可能导致大量模拟(参见下面的 "同步和不同步的页面")。叶子页可以不同步,以避免触发频繁的模拟,但对于非叶子页则不可能。

    • 此字段计算自上次实际使用页表以来的模拟次数;如果在该页面上模拟太频繁,KVM 将取消映射该页面以避免将来的模拟。

反向映射

MMU 维护了一个反向映射,通过该映射可以根据其 gfn(guest frame number,客户机帧号)找到映射某个页面的所有页表项(pte)。例如,在交换(swap)页面时使用这个反向映射。

同步和未同步页面

客户机使用两个事件来同步其TLB(Translation Lookaside Buffer)和页表:TLB 刷新和页面失效(invlpg)。

TLB 刷新意味着我们需要同步从客户机的 cr3(控制寄存器 3)可达的所有 spte(页表项)。这是一项昂贵的操作,因此我们保持所有客户机页表的写保护,并在写入 gpte(全局页表项)时同步 spte 到 gpte。

一个特殊情况是当一个客户机页表可以从当前客户机的 cr3 可达时。在这种情况下,客户机有责任在使用翻译之前发出 invlpg 指令。我们利用这一点通过从客户机页中去除写保护,允许客户机自由修改它。在客户机调用 invlpg 时,我们同步已修改的 gpte。这减少了在客户机修改多个 gpte 或者当一个客户机页不再用作页表而被用于随机客户机数据时,我们需要进行模拟的量。

作为副作用,我们必须在 TLB 刷新时重新同步所有可达的未同步影子页。

事件处理方式

  • 客户机页故障(或 NPT 页故障,或 EPT 违规)

这是最复杂的事件。页故障的原因可能是:

  • 真实的客户机故障(客户机地址不允许访问)(*)

  • 访问丢失的地址

  • 访问受保护的地址

    • 在记录脏页时,内存被写保护

    • 同步的影子页被写保护 (*)

  • 访问不可翻译的内存(MMIO) (*) 在直接模式下不适用

处理页故障的步骤如下:

  • 如果错误代码的 RSV 位被设置,那么页故障是由客户机访问 MMIO 引起的,同时缓存的 MMIO 信息是可用的。

    • 遍历影子页表

    • 检查 spte 中的有效生成号(参见下面的 "快速失效 MMIO spte")

    • 将信息缓存到 vcpu->arch.mmio_gva、vcpu->arch.mmio_access 和 vcpu->arch.mmio_gfn,并调用模拟器

  • 如果错误代码的 P 位和 R/W 位都设置了,这可能可以作为 "快速页故障" 处理(无需获取 MMU 锁)。详见 Documentation/virt/kvm/locking.rst 中的描述。

  • 如果需要,遍历客户机页表以确定客户机翻译(gva->gpa 或 ngpa->gpa)

    • 如果权限不足,则将故障反映回客户机

  • 确定主机页

    • 如果这是一个 MMIO 请求,则没有主机页;将信息缓存到 vcpu->arch.mmio_gva、vcpu->arch.mmio_access 和 vcpu->arch.mmio_gfn

  • 遍历影子页表以找到翻译的 spte,根据需要实例化缺失的中间页表

    • 如果这是一个 MMIO 请求,将 MMIO 信息缓存到 spte,并在 spte 上设置一些保留位(参见 kvm_mmu_set_mmio_spte_mask 的调用者)

  • 尝试取消同步页面

    • 如果成功,则可以让客户机继续并修改 gpte

  • 模拟指令

    • 如果失败,取消遮蔽页面并让客户机继续

  • 更新由指令修改的任何翻译

invlpg 处理:

  • 遍历影子页层次结构并删除受影响的翻译

  • 尝试重新实例化指示的翻译,希望客户机在不久的将来会使用它

客户机控制寄存器更新:

  • 移动到 cr3

    • 查找新的影子根

    • 同步新可达的影子页

  • 移动到 cr0/cr4/efer

    • 为新的分页模式设置 mmu 上下文

    • 查找新的影子根

    • 同步新可达的影子页

主机翻译更新:

  • mmu 通知器使用更新的 hva 调用

  • 通过反向映射查找受影响的 spte

  • 删除(或更新)翻译

模拟 cr0.wp(CR0.WP是x86架构中控制页表项写保护的位)

如果未启用 TDP(Two-Dimensional Paging),主机必须保持 CR0.WP=1,以便页写保护适用于虚拟机内核而不是虚拟机用户空间。当虚拟机的 CR0.WP=1 时,这不会产生问题。然而,当虚拟机的 CR0.WP=0 时,我们无法将 gpte.u=1gpte.w=0 的权限映射到任何 spte 上(语义要求允许任何虚拟机内核访问以及用户读取访问)。

我们通过将权限映射到两种可能的 spte 上来处理这个问题,具体取决于故障类型:

  • 内核写入故障:spte.u=0spte.w=1(允许完全内核访问,不允许用户访问)

  • 读取故障:spte.u=1spte.w=0(允许完全读取访问,不允许内核写入访问)

(用户写入故障会生成 #PF)

在第一种情况下,存在两个额外的复杂性:

  • 如果启用了 CR4.SMEP:由于我们已经将页面转换为内核页面,内核现在可以执行它。我们通过还设置 spte.nx 来处理这个。如果我们遇到用户取指令或读取故障,我们将更改 spte.u=1spte.nx=gpte.nx。为了使此工作,当使用影子分页时,KVM 强制将 EFER.NX 设置为 1。

  • 如果禁用了 CR4.SMAP:由于页面已更改为内核页面,在启用 CR4.SMAP 时无法重新使用它。我们将 CR4.SMAP && !CR0.WP 设置为影子页面的角色,以避免这种情况。请注意,在这里,我们不关心启用 CR4.SMAP 的情况,因为 KVM 将直接向虚拟机注入 #PF,因为权限检查失败。

为了防止具有 CR0.WP=0 的 spte 在 CR0.WP 更改为 1 后被内核写入,我们将 CR0.WP 的值作为页面角色的一部分。这意味着使用一个值为 CR0.WP 的 spte 无法在 CR0.WP 具有不同值时使用 - 它将被影子页面查找代码忽略。当使用 CR0.WP=0CR4.SMEP=0 创建的 spte 在将 CR4.SMEP 更改为 1 后使用时存在类似的问题。为了避免这种情况,!CR0.WP && CR4.SMEP 的值也作为页面角色的一部分。

大页

MMU支持大大小小的虚拟机和主机页面的所有组合。支持的页面大小包括4k、2M、4M和1G。由于MMU始终使用PAE分页,因此4M页面在虚拟机和主机上都被视为两个独立的2M页面。

要实例化一个大的SPT(页表项),必须满足四个约束条件:

  • SPT必须指向一个大的主机页面。

  • 客户PTE必须是至少相同大小的大PTE(如果启用了TDP,则没有客户PTE,此条件得到满足)。

  • 如果SPT将是可写的,则大页面帧不能与任何写保护页面重叠。

  • 客户页面必须完全包含在单个内存插槽中。

为了检查最后两个条件,MMU为每个内存插槽和大页面大小维护了一组->disallow_lpage数组。每个写保护页面都会导致其disallow_lpage递增,从而阻止实例化大的SPT。位于不对齐内存插槽末尾的帧具有人为膨胀的->disallow_lpages,因此它们永远无法实例化。

快速使无效的 MMIO SPT(页表项)

如上文中提到的"Reaction to events",KVM 将在叶子 SPT(页表项)中缓存 MMIO 信息。当添加新的内存插槽或更改现有内存插槽时,这些信息可能会变得过时,需要进行失效操作。这也需要在遍历所有影子页时保持 MMU 锁,并且使用类似的技术可以使其更具可扩展性。

MMIO SPT 具有一些备用位,用于存储生成编号。全局生成编号存储在 kvm_memslots(kvm)->generation 中,每当更改客户内存信息时都会增加。

当 KVM 发现一个 MMIO SPT 时,它会检查 SPT 的生成编号。如果 SPT 的生成编号与全局生成编号不相等,它将忽略缓存的 MMIO 信息,并通过慢路径处理页面错误。

由于仅使用 18 位来存储 MMIO SPT 上的生成编号,当发生溢出时会清除所有页面。

不幸的是,单个内存访问可能会多次访问 kvm_memslots(kvm),最后一次发生在获取生成编号并存储到 MMIO SPT 中时。因此,MMIO SPT 可能是基于过时信息创建的,但具有最新的生成编号。

为了避免这种情况,生成编号在 synchronize_srcu 返回后再次递增;因此,在进行内存插槽更新时,kvm_memslots(kvm)->generation 的第 63 位被设置为 1,而一些 SRCU 读者可能正在使用旧副本。我们不想使用基于奇数生成编号创建的 MMIO SPT,而且我们可以在不失去 MMIO SPT 中的一个位的情况下实现这一点。生成编号的 "更新正在进行中" 位未存储在 MMIO SPT 中,因此在从 SPT 中提取生成编号时,该位会被隐式设置为零。如果 KVM 不幸地在更新进行中创建了 MMIO SPT,那么对 SPT 的下一次访问将始终是缓存未命中。例如,在更新窗口期间的后续访问将由于进行中的标志发散而未命中,而在更新窗口关闭后的访问将具有较高的生成编号(与 SPT 相比)。

Further reading

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值