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() vs 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 是一个 rwlock; 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 上的 Host-writable 位和 MMU-writable 位
Host-writable 表示 gfn 在主机内核页表及其 KVM memslot 中是可写的。
MMU-writable 表示 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 在脏位图中丢失。
对于 direct sp,我们可以很容易地避免它,因为 direct sp 的 spte 固定为 gfn。 对于 indirect sp,为了简单起见,我们禁用了快速页面错误。
indirect sp 的解决方案可能是在 cmpxchg 之前固定 gfn。 固定后
我们已经持有 pfn 的引用计数;这意味着 pfn 无法释放并且无法重用于另一个 gfn。
pfn 是可写的,因此它不能被 KSM 在不同的 gfn 之间共享。
然后,我们可以确保正确设置 gfn 的脏位图。
脏位跟踪
在原始代码中,如果 spte 是只读的并且 Accessed 位已经设置,则可以快速更新(非原子地)spte,因为 Accessed 位和 Dirty 位不会丢失。
但在快速页面错误之后,这不再是真的,因为 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!!!
|
在这种情况下,Dirty 位丢失。
为了避免此类问题,如果 spte 可以在 mmu-lock 之外更新,我们总是将 spte 视为“volatile” [请参阅 spte_needs_atomic_update()];这意味着 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 之外更新则是 “volatile”,我们总是以原子方式更新 spte,并且可以避免由快速页面错误引起的竞争。 请参阅 spte_needs_atomic_update() 和 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 位将保持清除状态,直到发生写入访问,届时将使用上述脏跟踪机制对其进行设置。
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 偏移量
- 注释:
“raw”是因为更新 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
读取端临界区中必须修改的 memslots 的任何特定于架构的字段。- 注释:
必须在读取指向当前 memslots 的指针之前持有,直到完成对 memslots 的所有更改之后。
wakeup_vcpus_on_cpu_lock
¶
- 类型:
spinlock_t
- 架构:
x86
- 保护:
wakeup_vcpus_on_cpu
- 注释:
这是一个每个 CPU 的锁,它用于 VT-d 发布中断。 当支持 VT-d 发布中断并且 VM 具有分配的设备时,我们将阻塞的 vCPU 放在 blocked_vcpu_on_cpu 列表中,该列表受 blocked_vcpu_on_cpu_lock 保护。 当 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,例如更新静态调用。