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 之外的客户机页错误的快速路径。 目前,在以下两种情况下,页面错误可以是快速的

  1. 访问跟踪:SPTE 不存在,但已标记为用于访问跟踪。 这意味着我们需要恢复保存的 R/X 位。 稍后将对此进行更详细的描述。

  2. 写保护: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 检测到。

但是我们需要仔细检查这些情况

  1. 从 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 的脏位图。

  1. 脏位跟踪

在原始代码中,如果 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 在这种情况下总是以原子方式更新。

  1. 由于 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,例如更新静态调用。