x86 kvm 影子 MMU¶
mmu(在 arch/x86/kvm 中,文件 mmu.[ch] 和 paging_tmpl.h)负责向客户机呈现标准的 x86 mmu,同时将客户机物理地址转换为主机物理地址。
mmu 代码尝试满足以下要求
- 正确性
客户机不应该能够确定它运行在模拟的 mmu 上,除了时间(我们尝试遵守规范,而不是模拟特定实现的特征,例如 tlb 大小)
- 安全性
客户机不能触及未分配给它的主机内存
- 性能
最小化 mmu 带来的性能损失
- 缩放
需要缩放到大内存和大 vcpu 客户机
- 硬件
支持全系列的 x86 虚拟化硬件
- 集成
Linux 内存管理代码必须控制客户机内存,以便交换、页面迁移、页面合并、透明巨页和类似功能可以正常工作而无需更改
- 脏跟踪
报告对客户机内存的写入,以实现实时迁移和基于帧缓冲区的显示
- 占用空间
保持锁定的内核内存量较低(大多数内存应该是可收缩的)
- 可靠性
避免多页或 GFP_ATOMIC 分配
缩写¶
pfn |
主机页帧号 |
hpa |
主机物理地址 |
hva |
主机虚拟地址 |
gfn |
客户机帧号 |
gpa |
客户机物理地址 |
gva |
客户机虚拟地址 |
ngpa |
嵌套客户机物理地址 |
ngva |
嵌套客户机虚拟地址 |
pte |
页表条目(也用于泛指分页结构条目) |
gpte |
客户机 pte(指 gfn) |
spte |
影子 pte(指 pfn) |
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 到 3 个翻译编码到仅支持 1 个(传统)和 2 个 (tdp) 翻译的硬件中。 当所需翻译的数量与硬件匹配时,mmu 以直接模式运行;否则,它以影子模式运行(参见下文)。
内存¶
客户机内存 (gpa) 是使用 kvm 的进程的用户地址空间的一部分。 用户空间定义了客户机地址和用户地址之间的翻译 (gpa->hva); 请注意,两个 gpa 可能别名到同一个 hva,但反之则不然。
这些 hva 可以使用主机可用的任何方法进行支持:匿名内存、文件支持内存和设备内存。 内存可能随时被主机分页。
事件¶
mmu 由事件驱动,一些来自客户机,一些来自主机。
客户机生成的事件
写入控制寄存器(尤其是 cr3)
invlpg/invlpga 指令执行
访问丢失或受保护的翻译
主机生成的事件
gpa->hpa 翻译的更改(通过 gpa->hva 更改或 hva->hpa 更改)
内存压力(收缩器)
影子页面¶
主要数据结构是影子页面,“struct kvm_mmu_page”。 一个影子页面包含 512 个 sptes,可以是叶子 spte,也可以是非叶子 spte。 一个影子页面可能包含叶子 spte 和非叶子 spte 的混合。
非叶子 spte 允许硬件 mmu 到达叶子页面,并且不直接与翻译相关。 它指向其他影子页面。
叶子 spte 对应于编码到一个分页结构条目中的一个或两个翻译。 这些始终是翻译堆栈的最低级别,可选的更高级别翻译留给 NPT/EPT。 叶子 pte 指向客户机页面。
下表显示了叶子 pte 编码的翻译,更高级别的翻译在括号中
非嵌套客户机
nonpaging: gpa->hpa paging: gva->gpa->hpa paging, tdp: (gva->)gpa->hpa嵌套客户机
non-tdp: ngva->gpa->hpa (*) tdp: (ngva->)ngpa->gpa->hpa (*) the guest hypervisor will encode the ngva->gpa translation into its page tables if npt is not present
- 影子页面包含以下信息
- 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 位 gptes,而主机使用 64 位 sptes。 这意味着客户机页表包含比主机更多的 ptes,因此需要多个影子页面来影子一个客户机页面。 对于第一级影子页面,role.quadrant 可以是 0 或 1,表示客户机页表中的第一个或第二个 512-gpte 块。 对于第二级页表,每个 32 位 gpte 都转换为两个 64 位 sptes(因为每个第一级客户机页面都由两个第一级影子页面影子),因此 role.quadrant 取值范围为 0..3。 每个象限映射 1GB 虚拟地址空间。
- role.access
从父 ptes 继承的客户机访问权限,格式为 uwx。 注意,执行权限是正的,而不是负的。
- role.invalid
页面无效,不应使用。 它是一个当前被钉住的根页面(由指向它的 cpu 硬件寄存器); 一旦它被取消钉住,它将被销毁。
- role.has_4_byte_gpte
反映了页面有效的客户机 PTE 的大小,即,如果使用直接映射或 64 位 gptes,则为“0”; 如果使用 32 位 gptes,则为“1”。
- role.efer_nx
包含页面有效的 efer.nx 的值。
- role.cr0_wp
包含页面有效的 cr0.wp 的值。
- role.smep_andnot_wp
包含 cr4.smep && !cr0.wp 的值,页面有效(此值为 true 的页面与其他页面不同;请参见下面 cr0.wp=0 的处理)。
- role.smap_andnot_wp
包含 cr4.smap && !cr0.wp 的值,页面有效(此值为 true 的页面与其他页面不同;请参见下面 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。 EPT 在 Haswell 之前没有 A/D 位; 如果 L1 虚拟机监控程序不启用它们,影子 EPT 页表也不能使用 A/D 位。
- role.guest_mode
指示影子页面是为嵌套客户机创建的。
- role.passthrough
页面不受客户机页表的支持,但其第一个条目指向一个。 如果 NPT 使用 5 级页表(主机 CR4.LA57=1)并且影子 L1 的 4 级 NPT (L1 CR4.LA57=0),则设置此标志。
- mmu_valid_gen
此页面的 MMU 生成,用于在 VM 中快速清除所有 MMU 页面,而不会阻塞 vCPU 太长时间。 具体来说,KVM 更新每个 VM 的有效 MMU 生成,导致每个 mmu 页面的 mmu_valid_gen 不匹配。 这使得所有现有的 MMU 页面都过时。 过时的页面不能使用。 因此,vCPU 必须在重新进入客户机之前加载一个新的有效根。 MMU 生成始终为“0”或“1”。 注意,TDP MMU 不使用此字段,因为非根 TDP MMU 页面只能从它们自己的根访问。 因此,TDP MMU 只需在根页面中使用 role.invalid 即可使所有 MMU 页面无效。
- gfn
要么是包含由此页面影子的翻译的客户机页表,要么是线性翻译的基本页帧。 请参见 role.direct。
- spt
一个充满 64 位 sptes 的页面,包含此页面的翻译。 可由 kvm 和硬件访问。 spt 指向的页面的 page->private 将指回影子页面结构。 spt 中的 sptes 要么指向客户机页面,要么指向较低级别的影子页面。 具体来说,如果 sp1 和 sp2 是影子页面,则 sp1->spt[n] 可以指向 __pa(sp2->spt)。 sp2 将通过 parent_pte 指回 sp1。 spt 数组形成一个 DAG 结构,其中影子页面作为节点,客户机页面作为叶子。
- shadowed_translation
一个包含 512 个影子翻译条目的数组,每个存在的 pte 一个。 用于从 pte 反向映射到 gfn 以及其访问权限。 当设置 role.direct 时,shadow_translation 数组不会分配。 这是因为当使用时,此数组的任何元素中包含的 gfn 都可以从 gfn 字段计算出来。 此外,当设置 role.direct 时,KVM 不跟踪每个 gfn 的访问权限。 请参见 role.direct 和 gfn。
- root_count / tdp_mmu_root_count
root_count 是 Shadow MMU 中根影子页面的引用计数器。 vCPU 在获取将用作根页面的影子页面时会提高引用计数,即,将直接加载到硬件中的页面(CR3、PDPTR、nCR3 EPTP)。 当引用计数非零时,根页面不能被销毁。 请参见 role.invalid。 tdp_mmu_root_count 类似,但专门在 TDP MMU 中用作原子引用计数。
- parent_ptes
指向此页面的 spt 的 pte/ptes 的反向映射。 如果 parent_ptes 位 0 为零,则只有一个 spte 指向此页面,并且 parent_ptes 指向这个单个 spte,否则,存在多个 sptes 指向此页面,并且 (parent_ptes & ~0x1) 指向具有父 sptes 列表的数据结构。
- ptep
指向此影子页面的 SPTE 的内核虚拟地址。 此字段专门由 TDP MMU 使用,它是与 parent_ptes 的联合。
- unsync
如果为 true,则此页面中的翻译可能与客户机的翻译不匹配。 这相当于 tlb 的状态,当 pte 被更改但在 tlb 条目被刷新之前。 因此,当客户机执行 invlpg 或通过其他方式刷新其 tlb 时,unsync ptes 会同步。 对叶子页面有效。
- unsync_children
页面中有多少个 sptes 指向 unsync 的页面(或具有未同步的子页面)。
- unsync_child_bitmap
一个位图,指示 spt 中哪些 sptes(直接或间接地)指向可能未同步的页面。 用于快速定位从给定页面可访问的所有未同步页面。
- clear_spte_count
仅在 32 位主机上存在,其中 64 位 spte 不能以原子方式写入。 读取器在运行 MMU 锁定时使用此值来检测正在进行的更新并重试它们,直到写入器完成写入。
- write_flooding_count
客户机可能会多次写入页表,如果页面需要写保护,则会导致大量模拟(参见下面的“同步和未同步页面”)。 叶子页面可以不同步,这样它们就不会触发频繁的模拟,但对于非叶子页面来说这是不可能的。 此字段计算自上次实际使用页表以来的模拟次数; 如果在此页面上过于频繁地触发模拟,KVM 将取消映射该页面以避免将来进行模拟。
- tdp_mmu_page
如果影子页面是 TDP MMU 页面,则为 1。 当 KVM 遍历任何可能包含来自 TDP MMU 和影子 MMU 的页面的数据结构时,此变量用于分叉控制流。
反向映射¶
mmu 维护一个反向映射,由此给定页面的所有 ptes 都可以通过其 gfn 访问。 例如,这在交换页面时使用。
同步和未同步页面¶
客户机使用两个事件来同步其 tlb 和页表:tlb 刷新和页面失效 (invlpg)。
tlb 刷新意味着我们需要同步从客户机的 cr3 可访问的所有 sptes。 这很昂贵,因此我们保持所有客户机页表的写保护,并在写入 gpte 时将 sptes 同步到 gptes。
一个特殊情况是客户机页表可以从当前客户机 cr3 访问。 在这种情况下,客户机有义务在使用翻译之前发出 invlpg 指令。 我们通过删除客户机页面的写保护并允许客户机自由修改它来利用这一点。 当客户机调用 invlpg 时,我们会同步修改后的 gptes。 这减少了当客户机修改多个 gptes 时,或者当客户机页面不再用作页表而用于随机客户机数据时,我们必须执行的模拟量。
作为副作用,我们必须在 tlb 刷新时重新同步所有可访问的未同步影子页面。
对事件的反应¶
客户机页面错误(或 npt 页面错误,或 ept 违规)
这是最复杂的事件。 页面错误的根本原因可能是
真正的客户机错误(客户机翻译不允许访问)(*)
访问丢失的翻译
访问受保护的翻译 - 当记录脏页时,内存受到写保护 - 同步的影子页面受到写保护 (*)
访问不可翻译的内存 (mmio)
(*) 不适用于直接模式
页面错误的处理方式如下
如果错误代码的 RSV 位被设置,则页面错误是由客户机访问 MMIO 引起的,并且缓存的 MMIO 信息可用。
遍历影子页表
检查 spte 中有效的生成号(参见下面的“快速失效 MMIO sptes”)
将信息缓存到 vcpu->arch.mmio_gva、vcpu->arch.mmio_access 和 vcpu->arch.mmio_gfn,并调用模拟器
如果错误代码的 P 位和 R/W 位都被设置,则这可能被处理为“快速页面错误”(无需获取 MMU 锁即可修复)。 请参见 KVM 锁概述 中的描述。
如果需要,遍历客户机页表以确定客户机翻译 (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 处理
遍历影子页面层次结构并删除受影响的翻译
尝试重新实例化指示的翻译,希望客户机在不久的将来使用它
客户机控制寄存器更新
mov to cr3
查找新的影子根
同步新可访问的影子页面
mov to cr0/cr4/efer
为新的分页模式设置 mmu 上下文
查找新的影子根
同步新可访问的影子页面
主机翻译更新
mmu 通知器被调用并更新了 hva
通过反向映射查找受影响的 sptes
删除(或更新)翻译
模拟 cr0.wp¶
如果未启用 tdp,则主机必须保持 cr0.wp=1,以便页面写保护适用于客户机内核,而不是客户机用户空间。 当客户机 cr0.wp=1 时,这不会出现问题。 但是当客户机 cr0.wp=0 时,我们无法将 gpte.u=1, gpte.w=0 的权限映射到任何 spte(语义要求允许任何客户机内核访问加上用户读取访问)。
我们通过将权限映射到两个可能的 sptes 来处理此问题,具体取决于错误类型
内核写入错误:spte.u=0, spte.w=1(允许完全内核访问,禁止用户访问)
读取错误:spte.u=1, spte.w=0(允许完全读取访问,禁止内核写入访问)
(用户写入错误会生成 #PF)
在第一种情况下,还有两个额外的复杂性
如果 CR4.SMEP 被启用:由于我们将页面变成了内核页面,因此内核现在可以执行它。 我们通过也设置 spte.nx 来处理此问题。 如果我们收到用户提取或读取错误,我们将更改 spte.u=1 并且 spte.nx=gpte.nx 返回。 为了使这起作用,当使用影子分页时,KVM 强制 EFER.NX 为 1。
如果 CR4.SMAP 被禁用:由于页面已更改为内核页面,因此当 CR4.SMAP 被启用时无法重复使用它。 我们将 CR4.SMAP && !CR0.WP 设置到影子页面的 role 中以避免这种情况。 注意,这里我们不关心 CR4.SMAP 被启用的情况,因为 KVM 将由于权限检查失败而直接将 #PF 注入到客户机。
为了防止在 cr0.wp 更改为 1 后,一个被转换为具有 cr0.wp=0 的内核页面的 spte 被内核写入,我们将 cr0.wp 的值作为页面 role 的一部分。 这意味着使用 cr0.wp 的一个值创建的 spte 不能在 cr0.wp 具有不同值时使用 - 它将简单地被影子页面查找代码错过。 当在使用 cr0.wp=0 和 cr4.smep=0 创建的 spte 在将 cr4.smep 更改为 1 之后使用时,存在类似的问题。 为了避免这种情况,!cr0.wp && cr4.smep 的值也成为页面 role 的一部分。
大页面¶
mmu 支持大客户机页面和小客户机页面的所有组合。 支持的页面大小包括 4k、2M、4M 和 1G。 4M 页面被视为两个单独的 2M 页面,无论是在客户机上还是在主机上,因为 mmu 始终使用 PAE 分页。
要实例化一个大的 spte,必须满足四个约束
spte 必须指向一个大的主机页面
客户机 pte 必须是至少等效大小的大 pte(如果启用了 tdp,则没有客户机 pte,并且满足此条件)
如果 spte 将是可写的,则大页面帧可能不会与任何写保护页面重叠
客户机页面必须完全包含在单个内存插槽中
为了检查最后两个条件,mmu 为每个内存插槽和大页面大小维护一组 ->disallow_lpage 数组。 每个写保护页面都会导致其 disallow_lpage 递增,从而阻止实例化大的 spte。 未对齐的内存插槽末尾的帧具有人为膨胀的 ->disallow_lpages,因此它们永远无法被实例化。
快速失效 MMIO sptes¶
如上面的“对事件的反应”中所述,kvm 会将 MMIO 信息缓存在叶子 sptes 中。 当添加一个新的 memslot 或更改现有的 memslot 时,此信息可能会过时并且需要失效。 这还需要在遍历所有影子页面时保持 MMU 锁定,并且使用类似的技术使其更具可伸缩性。
MMIO sptes 有一些备用位,这些位用于存储生成号。 全局生成号存储在 kvm_memslots(kvm)->generation 中,并且每当客户机内存信息更改时都会增加。
当 KVM 找到 MMIO spte 时,它会检查 spte 的生成号。 如果 spte 的生成号不等于全局生成号,它将忽略缓存的 MMIO 信息并通过慢速路径处理页面错误。
由于只有 18 位用于存储 mmio spte 上的生成号,因此当发生溢出时,所有页面都会被清除。
不幸的是,单个内存访问可能会多次访问 kvm_memslots(kvm),最后一次访问发生在检索生成号并将其存储到 MMIO spte 中时。 因此,MMIO spte 可能是基于过时的信息创建的,但具有最新的生成号。
为了避免这种情况,在 synchronize_srcu 返回后,生成号会再次递增; 因此,kvm_memslots(kvm)->generation 的位 63 仅在 memslot 更新期间设置为 1,而某些 SRCU 读取器可能正在使用旧副本。 我们不希望使用使用奇数生成号创建的 MMIO sptes,并且我们可以在不丢失 MMIO spte 中的位的情况下做到这一点。“正在进行的更新”位生成号不存储在 MMIO spte 中,因此当从 spte 中提取生成时,它隐式为零。 如果 KVM 不走运并在正在进行的更新期间创建 MMIO spte,则对 spte 的下一次访问将始终是缓存未命中。 例如,在更新窗口期间的后续访问将由于正在进行的标志差异而未命中,而在更新窗口关闭后的访问将具有更高的生成号(与 spte 相比)。
进一步阅读¶
来自 KVM 论坛 2008 的 NPT 演示文稿 https://www.linux-kvm.org/images/c/c8/KvmForum2008%24kdf2008_21.pdf