KVM VCPU 請求

概述

KVM 支援一個內部 API,該 API 允許執行緒請求 VCPU 執行緒執行某些活動。例如,一個執行緒可以透過 VCPU 請求來請求 VCPU 重新整理其 TLB。該 API 包含以下函式

/* Check if any requests are pending for VCPU @vcpu. */
bool kvm_request_pending(struct kvm_vcpu *vcpu);

/* Check if VCPU @vcpu has request @req pending. */
bool kvm_test_request(int req, struct kvm_vcpu *vcpu);

/* Clear request @req for VCPU @vcpu. */
void kvm_clear_request(int req, struct kvm_vcpu *vcpu);

/*
 * Check if VCPU @vcpu has request @req pending. When the request is
 * pending it will be cleared and a memory barrier, which pairs with
 * another in kvm_make_request(), will be issued.
 */
bool kvm_check_request(int req, struct kvm_vcpu *vcpu);

/*
 * Make request @req of VCPU @vcpu. Issues a memory barrier, which pairs
 * with another in kvm_check_request(), prior to setting the request.
 */
void kvm_make_request(int req, struct kvm_vcpu *vcpu);

/* Make request @req of all VCPUs of the VM with struct kvm @kvm. */
bool kvm_make_all_cpus_request(struct kvm *kvm, unsigned int req);

通常,請求者希望 VCPU 在發出請求後儘快執行活動。這意味著大多數請求(kvm_make_request() 呼叫)之後會緊跟著呼叫 kvm_vcpu_kick(),而 kvm_make_all_cpus_request() 則內建了對所有 VCPU 的踢出操作。

VCPU 踢出

VCPU 踢出的目的是將 VCPU 執行緒從客戶機模式中喚出,以便執行 KVM 維護操作。為此,會發送一個 IPI,強制進行客戶機模式退出。然而,在踢出時 VCPU 執行緒可能不在客戶機模式中。因此,根據 VCPU 執行緒的模式和狀態,踢出可能會採取另外兩種行動。所有三種行動如下所示

  1. 傳送一個 IPI。這會強制進行客戶機模式退出。

  2. 喚醒休眠的 VCPU。休眠的 VCPU 是處於客戶機模式之外、並在等待佇列上等待的 VCPU 執行緒。喚醒它們會將執行緒從等待佇列中移除,允許執行緒再次執行。此行為可能會被抑制,請參見下方的 KVM_REQUEST_NO_WAKEUP。

  3. 什麼也不做。當 VCPU 不在客戶機模式且 VCPU 執行緒未休眠時,則無需執行任何操作。

VCPU 模式

VCPU 具有一個模式狀態 vcpu->mode,用於跟蹤客戶機是否在客戶機模式下執行,以及一些特定的客戶機模式外狀態。架構可以使用 vcpu->mode 來確保 VCPU 請求被 VCPU 看到(參見“確保請求被看到”),以及避免傳送不必要的 IPI(參見“IPI 減少”),甚至確保等待 IPI 確認(參見“等待確認”)。定義了以下模式

OUTSIDE_GUEST_MODE

VCPU 執行緒處於客戶機模式之外。

IN_GUEST_MODE

VCPU 執行緒處於客戶機模式。

EXITING_GUEST_MODE

VCPU 執行緒正在從 IN_GUEST_MODE 轉換到 OUTSIDE_GUEST_MODE。

READING_SHADOW_PAGE_TABLES

VCPU 執行緒處於客戶機模式之外,但它希望某些 VCPU 請求(即 KVM_REQ_TLB_FLUSH)的傳送者等待 VCPU 執行緒完成頁表讀取。

VCPU 請求內部實現

VCPU 請求僅僅是 vcpu->requests 點陣圖的位索引。這意味著也可以使用通用位操作,例如 [atomic-ops] 中文件化的那些操作,例如。

clear_bit(KVM_REQ_UNBLOCK & KVM_REQUEST_MASK, &vcpu->requests);

然而,VCPU 請求使用者應避免這樣做,因為它會破壞抽象。前 8 位保留用於架構無關請求;所有額外的位可用於架構相關請求。

架構無關請求

KVM_REQ_TLB_FLUSH

KVM 的通用 MMU 通知器可能需要重新整理客戶機的所有 TLB 條目,透過呼叫 kvm_flush_remote_tlbs() 來實現。選擇使用通用 kvm_flush_remote_tlbs() 實現的架構將需要處理此 VCPU 請求。

KVM_REQ_VM_DEAD

此請求通知所有 VCPU,虛擬機器已死且不可用,例如由於致命錯誤或虛擬機器的狀態已被有意銷燬。

KVM_REQ_UNBLOCK

此請求通知 vCPU 退出 kvm_vcpu_block。例如,它用於代表 vCPU 在主機上執行的計時器處理程式,或用於更新中斷路由並確保分配的裝置會喚醒 vCPU。

KVM_REQ_OUTSIDE_GUEST_MODE

此“請求”確保目標 vCPU 在請求傳送者繼續執行之前已退出客戶機模式。目標無需採取任何操作,因此實際上沒有為目標記錄任何請求。此請求類似於“踢出”,但與踢出不同的是,它保證 vCPU 確實已退出客戶機模式。踢出僅保證 vCPU 將在未來的某個時間點退出,例如,之前的踢出可能已經啟動了該過程,但不能保證將被踢出的 vCPU 已完全退出客戶機模式。

KVM_REQUEST_MASK

VCPU 請求在使用位操作之前應該透過 KVM_REQUEST_MASK 進行掩碼操作。這是因為只有低 8 位用於表示請求編號。高位用作標誌。目前只定義了兩個標誌。

VCPU 請求標誌

KVM_REQUEST_NO_WAKEUP

此標誌適用於僅需在客戶機模式下執行的 VCPU 立即關注的請求。也就是說,休眠的 VCPU 無需為此類請求而被喚醒。休眠的 VCPU 會在稍後因其他原因被喚醒時處理這些請求。

KVM_REQUEST_WAIT

當帶有此標誌的請求透過 kvm_make_all_cpus_request() 發出時,呼叫者將等待每個 VCPU 確認其 IPI 後再繼續。此標誌僅適用於將接收 IPI 的 VCPU。例如,如果 VCPU 正在休眠,因此不需要 IPI,則請求執行緒不會等待。這意味著此標誌可以安全地與 KVM_REQUEST_NO_WAKEUP 結合使用。有關帶有 KVM_REQUEST_WAIT 的請求的更多資訊,請參閱“等待確認”。

帶有相關狀態的 VCPU 請求

希望接收 VCPU 處理新狀態的請求者需要確保在接收 VCPU 執行緒的 CPU 觀察到請求時,新寫入的狀態對它可見。這意味著必須在新狀態寫入之後和設定 VCPU 請求位之前插入寫記憶體屏障。此外,在接收 VCPU 執行緒一側,必須在讀取請求位之後和繼續讀取與之相關的新狀態之前插入相應的讀屏障。參見 [lwn-mb] 的場景 3,訊息和標誌,以及核心文件 [memory-barriers]

函式對 kvm_check_request() 和 kvm_make_request() 提供了記憶體屏障,允許 API 在內部處理此要求。

確保請求被看到

向 VCPU 發出請求時,我們希望避免接收 VCPU 在客戶機模式下執行任意長時間而不處理請求。只要我們確保 VCPU 執行緒在進入客戶機模式之前檢查 kvm_request_pending(),並且在必要時踢出操作會發送 IPI 以強制退出客戶機模式,我們就可以確保這種情況不會發生。必須特別注意覆蓋 VCPU 執行緒最後一次 kvm_request_pending() 檢查之後到進入客戶機模式之前的這段時間,因為踢出 IPI 只會觸發處於客戶機模式或至少已停用中斷以準備進入客戶機模式的 VCPU 執行緒退出客戶機模式。這意味著最佳化的實現(參見“IPI 減少”)必須確定何時可以安全地不傳送 IPI。除了 s390 之外的所有架構都採用的一個解決方案是

  • 在停用中斷和最後一次 kvm_request_pending() 檢查之間將 vcpu->mode 設定為 IN_GUEST_MODE;

  • 在進入客戶機時原子地啟用中斷。

此解決方案還需要在請求執行緒和接收 VCPU 中小心放置記憶體屏障。有了記憶體屏障,我們就可以排除 VCPU 執行緒在最後一次檢查時觀察到 !kvm_request_pending(),然後在為其發出的下一個請求(即使該請求是在檢查後立即發出的)未收到 IPI 的可能性。這是透過 Dekker 記憶體屏障模式([lwn-mb] 的場景 10)完成的。由於 Dekker 模式需要兩個變數,此解決方案將 vcpu->modevcpu->requests 配對。將它們代入模式得到

CPU1                                    CPU2
=================                       =================
local_irq_disable();
WRITE_ONCE(vcpu->mode, IN_GUEST_MODE);  kvm_make_request(REQ, vcpu);
smp_mb();                               smp_mb();
if (kvm_request_pending(vcpu)) {        if (READ_ONCE(vcpu->mode) ==
                                            IN_GUEST_MODE) {
    ...abort guest entry...                 ...send IPI...
}                                       }

如上所述,IPI 僅對處於客戶機模式或已停用中斷的 VCPU 執行緒有用。這就是為什麼 Dekker 模式的這種特定情況已擴充套件到在將 vcpu->mode 設定為 IN_GUEST_MODE 之前停用中斷。WRITE_ONCE() 和 READ_ONCE() 用於嚴格實現記憶體屏障模式,確保編譯器不會干擾 vcpu->mode 經過精心規劃的訪問。

IPI 減少

由於只需一個 IPI 即可使 VCPU 檢查任何/所有請求,因此它們可以合併。這很容易透過讓第一個傳送 IPI 的踢出操作也把 VCPU 模式更改為 !IN_GUEST_MODE 來完成。過渡狀態 EXITING_GUEST_MODE 正是為此目的而使用。

等待確認

某些請求(設定了 KVM_REQUEST_WAIT 標誌的請求)需要傳送 IPI,並且需要等待確認,即使目標 VCPU 執行緒處於 IN_GUEST_MODE 之外的模式。例如,一種情況是當目標 VCPU 執行緒處於 READING_SHADOW_PAGE_TABLES 模式時,該模式在停用中斷後設定。為了支援這些情況,KVM_REQUEST_WAIT 標誌將傳送 IPI 的條件從檢查 VCPU 是否處於 IN_GUEST_MODE 更改為檢查它是否不處於 OUTSIDE_GUEST_MODE。

無請求的 VCPU 踢出

由於是否傳送 IPI 的決定取決於兩變數的 Dekker 記憶體屏障模式,因此很明顯,無請求的 VCPU 踢出幾乎從不正確。如果沒有保證非 IPI 生成的踢出操作仍然會導致接收 VCPU 執行某個動作(就像最終的 kvm_request_pending() 檢查對伴隨請求的踢出操作所做的那樣),那麼該踢出可能根本沒有任何作用。例如,如果對一個即將將其模式設定為 IN_GUEST_MODE 的 VCPU 發出了一個無請求的踢出操作(這意味著沒有傳送 IPI),那麼 VCPU 執行緒可能會繼續其進入過程,而實際上並未執行該踢出操作旨在啟動的任何任務。

一個例外是 x86 的已釋出中斷機制。然而在這種情況下,即使是無請求的 VCPU 踢出也與上述相同的 local_irq_disable() + smp_mb() 模式相結合;已釋出中斷描述符中的 ON 位(Outstanding Notification,未決通知)扮演了 vcpu->requests 的角色。傳送已釋出中斷時,在讀取 vcpu->mode 之前設定 PIR.ON;同樣,在 VCPU 執行緒中,vmx_sync_pir_to_irr() 在將 vcpu->mode 設定為 IN_GUEST_MODE 後讀取 PIR。

其他注意事項

休眠的 VCPU

VCPU 執行緒可能需要在呼叫可能使其進入休眠狀態的函式(例如 kvm_vcpu_block())之前和/或之後考慮請求。它們是否考慮以及如果考慮則需要考慮哪些請求,取決於架構。kvm_vcpu_block() 呼叫 kvm_arch_vcpu_runnable() 來檢查它是否應該喚醒。這樣做的原因之一是為架構提供一個在必要時可以檢查請求的函式。

參考資料

[atomic-ops]

Documentation/atomic_bitops.txt 和 Documentation/atomic_t.txt

[memory-barriers]

Documentation/memory-barriers.txt