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 个 spte,可以是叶 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 spte,2=2M spte,3=1G spte 等。
- role.direct
如果设置,从此页可到达的叶 spte 用于线性范围。示例包括实模式转换、小宿主机页支持的大型客户机页,以及当 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。这意味着一个客户机页表包含比宿主机更多的 pte,因此需要多个影子页来影子化一个客户机页。对于第一级影子页,role.quadrant 可以为 0 或 1,表示客户机页表中的第一个或第二个 512-gpte 块。对于第二级页表,每个 32 位 gpte 都转换为两个 64 位 spte(因为每个第一级客户机页都由两个第一级影子页影子化),因此 role.quadrant 的取值范围为 0..3。每个象限映射 1GB 的虚拟地址空间。
- role.access
以 uwx 形式从父 pte 继承的客户机访问权限。请注意,执行权限是正的,而不是负的。
- 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。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 生成用于快速清除虚拟机内的所有 MMU 页面,而不会长时间阻塞 vCPU。具体而言,KVM 更新每个虚拟机的有效 MMU 生成,这会导致每个 MMU 页面的 mmu_valid_gen 不匹配。这使得所有现有的 MMU 页面都过时。过时的页面无法使用。因此,vCPU 必须在重新进入客户机之前加载新的、有效的根。MMU 生成始终为“0”或“1”。请注意,TDP MMU 不使用此字段,因为非根 TDP MMU 页面只能从其拥有的根访问。因此,对于 TDP MMU 来说,在根页面中使用 role.invalid 来使所有 MMU 页面失效就足够了。
- 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 数组形成一个 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(根计数 / tdp_mmu_根计数)
root_count 是阴影 MMU 中根阴影页面的引用计数器。当 vCPU 获取将用作根页面的阴影页面时,即直接加载到硬件中的页面(CR3、PDPTR、nCR3 EPTP),vCPU 会增加 refcount。当其 refcount 为非零时,无法销毁根页面。请参阅 role.invalid。tdp_mmu_root_count 类似,但仅在 TDP MMU 中用作原子 refcount。
- parent_ptes(父 pte)
指向此页面 spt 的 pte/ptes 的反向映射。如果 parent_ptes 位 0 为零,则只有一个 spte 指向此页面,并且 parent_ptes 指向此单个 spte,否则,存在多个 spte 指向此页面,并且 (parent_ptes & ~0x1) 指向一个包含父 spte 列表的数据结构。
- ptep
指向此阴影页面的 SPTE 的内核虚拟地址。此字段专门供 TDP MMU 使用,是与 parent_ptes 的联合。
- unsync(未同步)
如果为 true,则此页面中的转换可能与客户机的转换不匹配。这等效于更改 pte 但在刷新 tlb 条目之前 tlb 的状态。因此,当客户机执行 invlpg 或通过其他方式刷新其 tlb 时,将同步 unsync pte。对叶子页面有效。
- unsync_children(未同步子项)
页面中有多少 spte 指向未同步(或具有未同步子项)的页面。
- unsync_child_bitmap(未同步子项位图)
一个位图,指示 spt 中哪些 spte(直接或间接)指向可能未同步的页面。用于快速定位从给定页面可访问的所有未同步页面。
- clear_spte_count(清除 spte 计数)
仅在 32 位主机上存在,其中无法原子写入 64 位 spte。读取器在 MMU 锁外运行时使用此选项来检测正在进行的更新并重试它们,直到写入器完成写入。
- write_flooding_count(写入泛滥计数)
客户机可能会多次写入页表,如果页面需要写保护,则会导致大量模拟(请参阅下面的“同步和未同步页面”)。叶子页面可以未同步,这样它们就不会触发频繁的模拟,但这对于非叶子页面来说是不可能的。此字段计算自上次实际使用页表以来的模拟次数;如果在此页面上过于频繁地触发模拟,KVM 将取消映射页面以避免将来进行模拟。
- tdp_mmu_page(tdp_mmu_页面)
如果阴影页面是 TDP MMU 页面,则为 1。当遍历可能包含来自 TDP MMU 和阴影 MMU 的页面的任何数据结构时,此变量用于分叉 KVM 的控制流。
反向映射¶
mmu 维护一个反向映射,通过该映射,给定页面的 gfn 可以访问映射该页面的所有 pte。例如,在换出页面时会使用此映射。
同步和未同步页面¶
客户机使用两个事件来同步其 tlb 和页表:tlb 刷新和页面失效 (invlpg)。
tlb 刷新意味着我们需要同步从客户机的 cr3 可访问的所有 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 锁即可修复)。请参阅 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 到 cr3
查找新的阴影根
同步新访问的阴影页面
mov 到 cr0/cr4/efer
为新的分页模式设置 mmu 上下文
查找新的阴影根
同步新访问的阴影页面
主机转换更新
使用更新后的 hva 调用 mmu 通知程序
通过反向映射查找受影响的 spte
删除(或更新)转换
模拟 cr0.wp¶
如果未启用 tdp,则主机必须保持 cr0.wp=1,以便页面写保护对客户机内核有效,而不是客户机用户空间有效。当客户机 cr0.wp=1 时,这不会有问题。但是,当客户机 cr0.wp=0 时,我们无法将 gpte.u=1、gpte.w=0 的权限映射到任何 spte(语义要求允许任何客户机内核访问和用户读取访问)。
我们通过根据错误类型将权限映射到两个可能的 spte 来处理这种情况
内核写入错误: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=0 转换为内核页面的 spte 在 cr0.wp 更改为 1 后被内核写入,我们将 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。由于 mmu 始终使用 PAE 分页,因此 4M 页面在客户机和主机上都被视为两个单独的 2M 页面。
要实例化大型 spte,必须满足四个约束
spte 必须指向大型主机页面
客户机 pte 必须是至少具有同等大小的大型 pte(如果启用了 tdp,则没有客户机 pte,并且满足此条件)
如果 spte 是可写入的,则大型页面帧可能不会与任何写保护页面重叠
客户机页面必须完全包含在单个内存槽中
为了检查最后两个条件,mmu 为每个内存槽和大型页面大小维护一组 ->disallow_lpage 数组。每个写保护页面都会导致其 disallow_lpage 递增,从而阻止实例化大型 spte。未对齐的内存槽末尾的帧具有人为膨胀的 ->disallow_lpage,因此永远无法实例化它们。
快速失效 MMIO spte¶
如上文“事件响应”中所述,kvm 将在叶子 sptes 中缓存 MMIO 信息。当添加新的内存槽或更改现有内存槽时,此信息可能会过时,需要使其失效。在遍历所有影子页面时,还需要持有 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 位仅在内存槽更新期间设置为 1,而某些 SRCU 读取器可能正在使用旧副本。我们不希望使用以奇数代数创建的 MMIO sptes,并且我们可以在不丢失 MMIO spte 中的一位的情况下做到这一点。代数的“正在进行更新”位不存储在 MMIO spte 中,因此当从 spte 中提取代数时,它隐式为零。如果 KVM 不幸在更新正在进行时创建了 MMIO spte,则下次访问该 spte 时始终会发生缓存未命中。例如,在更新窗口期间的后续访问会由于正在进行的标志而导致不一致而错过,而更新窗口关闭后的访问将具有更高的代数(与 spte 相比)。
进一步阅读¶
来自 2008 年 KVM 论坛的 NPT 演示文稿 https://www.linux-kvm.org/images/c/c8/KvmForum2008%24kdf2008_21.pdf