KVM 锁概述¶
1. 获取顺序¶
互斥锁的获取顺序如下
cpus_read_lock() 在 kvm_lock 之外获取
kvm_usage_lock 在 cpus_read_lock() 之外获取
kvm->lock 在 vcpu->mutex 之外获取
kvm->lock 在 kvm->slots_lock 和 kvm->irq_lock 之外获取
kvm->slots_lock 在 kvm->irq_lock 之外获取,尽管它们一起获取的情况非常罕见。
kvm->mn_active_invalidate_count 确保 invalidate_range_start() 和 invalidate_range_end() 回调对使用相同的 memslots 数组。当修改 memslots 时,kvm->slots_lock 和 kvm->slots_arch_lock 在等待侧获取,因此 MMU 通知器不得获取 kvm->slots_lock 或 kvm->slots_arch_lock。
cpus_read_lock() 与 kvm_lock
在 kvm_lock 之外获取 cpus_read_lock() 是有问题的,尽管这是官方的顺序,因为很容易在持有 kvm_lock 的同时无意中触发 cpus_read_lock()。遍历 vm_list 时要小心,例如,尽可能避免复杂的操作。
对于 SRCU
synchronize_srcu(&kvm->srcu)
在 kvm->lock、vcpu->mutex 和 kvm->slots_lock 的临界区内调用。这些锁 _不能_ 在 kvm->srcu 读取侧临界区内获取;也就是说,以下是错误的srcu_read_lock(&kvm->srcu); mutex_lock(&kvm->slots_lock);
kvm->slots_arch_lock 反而是在调用
synchronize_srcu()
之前释放。因此,它 _可以_ 在 kvm->srcu 读取侧临界区内获取,例如在处理 vmexit 时。
在 x86 上
vcpu->mutex 在 kvm->arch.hyperv.hv_lock 和 kvm->arch.xen.xen_lock 之外获取
kvm->arch.mmu_lock 是一个读写锁;kvm->arch.tdp_mmu_pages_lock 和 kvm->arch.mmu_unsync_pages_lock 的临界区也必须获取 kvm->arch.mmu_lock
其他所有内容都是叶子:在临界区内不获取其他锁。
2. 异常¶
快速页错误
快速页错误是快速路径,用于修复 x86 上 mmu-lock 之外的客户机页错误。目前,在以下两种情况下,页错误可以很快得到修复
访问跟踪:SPTE 不存在,但它被标记为访问跟踪。这意味着我们需要恢复保存的 R/X 位。这将在稍后详细描述。
写保护:SPTE 存在,并且错误是由写保护引起的。这意味着我们只需要更改 spte 的 W 位。
我们用来避免所有竞争的是 spte 上的主机可写位和 MMU 可写位
主机可写意味着 gfn 在主机内核页表及其 KVM memslot 中是可写的。
MMU 可写意味着 gfn 在客户机的 mmu 中是可写的,并且不受影子页写保护的保护。
在快速页错误路径上,如果 spte.HOST_WRITEABLE = 1 并且 spte.WRITE_PROTECT = 1,我们将使用 cmpxchg 原子地设置 spte 的 W 位,以恢复访问跟踪 spte 的已保存 R/X 位,或者两者都进行。这是安全的,因为每当更改这些位时都可以通过 cmpxchg 检测到。
但是我们需要仔细检查这些情况
从 gfn 到 pfn 的映射
从 gfn 到 pfn 的映射可能会发生变化,因为我们只能确保在 cmpxchg 期间 pfn 不会发生变化。这是一个 ABA 问题,例如,以下情况将会发生
一开始 gpte = gfn1
gfn1 is mapped to pfn1 on host
spte is the shadow page table entry corresponding with gpte and
spte = pfn1
|
|
在快速页错误路径上 |
|
CPU 0 |
CPU 1 |
old_spte = *spte;
|
|
pfn1 被换出 spte = 0;
pfn1 为 gfn2 重新分配。 gpte 由客户机更改为指向 gfn2 spte = pfn1;
|
|
if (cmpxchg(spte, old_spte, old_spte+W)
mark_page_dirty(vcpu->kvm, gfn1)
OOPS!!!
|
我们为 gfn1 脏日志记录,这意味着 gfn2 在脏位图中丢失。
对于直接 sp,我们可以轻松避免这种情况,因为直接 sp 的 spte 固定为 gfn。对于间接 sp,为了简单起见,我们禁用了快速页错误。
间接 sp 的一种解决方案是在 cmpxchg 之前锁定 gfn。锁定后
我们已经持有 pfn 的引用计数;这意味着 pfn 不能被释放并重新用于另一个 gfn。
pfn 是可写的,因此它不能通过 KSM 在不同的 gfn 之间共享。
然后,我们可以确保正确为 gfn 设置了脏位图。
脏位跟踪
在原始代码中,如果 spte 是只读的,并且已设置了访问位,则可以快速更新(非原子)spte,因为访问位和脏位不会丢失。
但是在快速页错误之后,情况并非如此,因为 spte 可以在读取 spte 和更新 spte 之间被标记为可写。如下所示
一开始 spte.W = 0
spte.Accessed = 1
|
|
CPU 0 |
CPU 1 |
在 mmu_spte_update() 中 old_spte = *spte;
/* 'if' condition is satisfied. */
if (old_spte.Accessed == 1 &&
old_spte.W == 0)
spte = new_spte;
|
|
在快速页错误路径上 spte.W = 1
在 spte 上进行内存写入 spte.Dirty = 1
|
|
else
old_spte = xchg(spte, new_spte);
if (old_spte.Accessed &&
!new_spte.Accessed)
flush = true;
if (old_spte.Dirty &&
!new_spte.Dirty)
flush = true;
OOPS!!!
|
在这种情况下,脏位丢失。
为了避免这种问题,如果 spte 可以在 mmu-lock 之外更新,我们总是将 spte 视为“易失的”[请参阅 spte_has_volatile_bits()];这意味着在这种情况下,spte 始终是原子更新的。
由于 spte 更新导致刷新 tlb
如果 spte 从可写更新为只读,我们应该刷新所有 TLB,否则 rmap_write_protect 将找到只读 spte,即使可写的 spte 可能缓存在 CPU 的 TLB 上。
如前所述,spte 可以在快速页错误路径上的 mmu-lock 之外更新为可写。为了方便审核路径,我们查看是否在 mmu_spte_update() 中刷新 TLB 是由于此原因造成的,因为这是一个更新 spte(present -> present)的常用函数。
由于如果 spte 可以在 mmu-lock 之外更新,则它是“易失的”,我们始终原子地更新 spte,并且可以避免快速页错误引起的竞争。请参阅 spte_has_volatile_bits() 和 mmu_spte_update() 中的注释。
无锁访问跟踪
这用于使用 EPT 但不支持 EPT A/D 位的 Intel CPU。在这种情况下,PTE 被标记为 A/D 禁用(使用忽略的位),当调用 KVM MMU 通知程序来跟踪对页面的访问时(通过 kvm_mmu_notifier_clear_flush_young),它会通过清除 PTE 中的 RWX 位并将原始 R & X 位存储在更多未使用的/忽略的位中,在硬件中将 PTE 标记为不存在。当 VM 稍后尝试访问该页面时,会生成一个错误,并且使用上述快速页错误机制原子地将 PTE 恢复到 Present 状态。当 PTE 被标记为访问跟踪时,不会保存 W 位,并且在恢复到 Present 状态期间,W 位根据它是否是写入访问来设置。如果不是,则 W 位将保持清除状态,直到发生写入访问,此时将使用上述脏跟踪机制设置 W 位。
3. 参考¶
kvm_lock
¶
- 类型:
互斥锁
- 架构:
任何
- 保护:
vm_list
kvm_usage_lock
¶
- 类型:
互斥锁
- 架构:
任何
- 保护:
kvm_usage_count
硬件虚拟化启用/禁用
- 注释:
之所以存在,是为了允许在 kvm_usage_count 受到保护的情况下获取 cpus_read_lock(),这简化了虚拟化启用逻辑。
kvm->mn_invalidate_lock
¶
- 类型:
spinlock_t
- 架构:
任何
- 保护:
mn_active_invalidate_count,mn_memslots_update_rcuwait
kvm_arch::tsc_write_lock
¶
- 类型:
raw_spinlock_t
- 架构:
x86
- 保护:
kvm_arch::{last_tsc_write,last_tsc_nsec,last_tsc_offset}
vmcb 中的 tsc 偏移量
- 注释:
“原始”,因为更新 tsc 偏移量不得被抢占。
kvm->mmu_lock
¶
- 类型:
spinlock_t 或 rwlock_t
- 架构:
任何
- 保护:
-影子页/影子 tlb 条目
- 注释:
它是一个自旋锁,因为它在 mmu 通知器中使用。
kvm->srcu
¶
- 类型:
srcu 锁
- 架构:
任何
- 保护:
kvm->memslots
kvm->buses
- 注释:
在访问 memslots(例如,使用 gfn_to_* 函数时)以及访问内核中 MMIO/PIO 地址->设备结构映射 (kvm->buses) 时,必须持有 srcu 读锁。如果多个函数需要,可以将 srcu 索引存储在每个 vcpu 的 kvm_vcpu->srcu_idx 中。
kvm->slots_arch_lock
¶
- 类型:
互斥锁
- 架构:
任何 (但只在 x86 上需要)
- 保护:
在
kvm->srcu
读取端临界区中必须修改的任何特定于架构的内存槽字段。- 注释:
必须在读取当前内存槽的指针之前持有此锁,直到对内存槽的所有更改完成之后。
wakeup_vcpus_on_cpu_lock
¶
- 类型:
spinlock_t
- 架构:
x86
- 保护:
wakeup_vcpus_on_cpu
- 注释:
这是一个 per-CPU 锁,用于 VT-d 发布中断。当支持 VT-d 发布中断且虚拟机分配了设备时,我们将阻塞的 vCPU 放在由 blocked_vcpu_on_cpu_lock 保护的 blocked_vcpu_on_cpu 列表中。当 VT-d 硬件由于来自分配设备的外部中断而发出唤醒通知事件时,我们将在列表中找到要唤醒的 vCPU。
vendor_module_lock
¶
- 类型:
互斥锁
- 架构:
x86
- 保护:
加载供应商模块(kvm_amd 或 kvm_intel)。
- 注释:
之所以存在,是因为使用 kvm_lock 会导致死锁。 kvm_lock 在通知器中获取,例如 __kvmclock_cpufreq_notifier(),该通知器可能在持有 cpu_hotplug_lock 时被调用,例如从 cpufreq_boost_trigger_state() 调用,并且许多操作在加载供应商模块时需要获取 cpu_hotplug_lock,例如更新静态调用。