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,例如更新靜態呼叫。