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