PCI 直通裝置¶
在 Hyper-V 客戶虛擬機器中,PCI 直通裝置(也稱為虛擬 PCI 裝置,或 vPCI 裝置)是直接對映到虛擬機器物理地址空間的物理 PCI 裝置。客戶裝置驅動程式可以直接與硬體互動,無需主機管理程式的中介。與由管理程式虛擬化的裝置相比,這種方法為裝置提供了更高的頻寬訪問和更低的延遲。該裝置應像在裸機上執行時一樣出現在客戶機中,因此無需更改該裝置的 Linux 裝置驅動程式。
Hyper-V 中 vPCI 裝置的術語是“離散裝置分配”(DDA)。Hyper-V DDA 的公開文件可在此處獲取:DDA
DDA 通常用於儲存控制器(例如 NVMe)和 GPU。對於 NIC,有一個類似的機制稱為 SR-IOV,它透過允許客戶機裝置驅動程式直接與硬體互動來產生相同的優勢。請參閱此處的 Hyper-V 公開文件:SR-IOV
本節對 vPCI 裝置的討論包括 DDA 和 SR-IOV 裝置。
裝置呈現¶
Hyper-V 在 vPCI 裝置執行時為其提供完整的 PCI 功能,因此該裝置的 Linux 裝置驅動程式可以無需更改地使用,前提是它使用正確的 Linux 核心 API 來訪問 PCI 配置空間並與其他 Linux 功能整合。但 PCI 裝置的初始檢測及其與 Linux PCI 子系統的整合必須使用 Hyper-V 特定的機制。因此,Hyper-V 上的 vPCI 裝置具有雙重身份。它們最初透過標準 VMBus“offer”機制作為 VMBus 裝置呈現給 Linux 客戶機,因此它們具有 VMBus 身份並出現在 /sys/bus/vmbus/devices 下。Linux 中位於 drivers/pci/controller/pci-hyperv.c 的 VMBus vPCI 驅動程式透過構建 PCI 匯流排拓撲並在 Linux 中建立所有正常的 PCI 裝置資料結構來處理新引入的 vPCI 裝置,這些資料結構類似於 PCI 裝置透過裸機系統上的 ACPI 發現時會存在的資料結構。一旦這些資料結構設定完畢,該裝置在 Linux 中也具有正常的 PCI 身份,並且 vPCI 裝置的正常 Linux 裝置驅動程式可以像在裸機上執行 Linux 一樣工作。由於 vPCI 裝置透過 VMBus offer 機制動態呈現,因此它們不會出現在 Linux 客戶機的 ACPI 表中。vPCI 裝置可以在虛擬機器的整個生命週期中的任何時間新增到虛擬機器或從虛擬機器中移除,而不僅僅是在初始啟動期間。
透過這種方法,vPCI 裝置同時是 VMBus 裝置和 PCI 裝置。響應 VMBus offer 訊息,hv_pci_probe() 函式執行並建立與 Hyper-V 主機上 vPCI VSP 的 VMBus 連線。該連線具有單個 VMBus 通道。該通道用於與 vPCI VSP 交換訊息,以便在 Linux 中設定和配置 vPCI 裝置。一旦裝置在 Linux 中作為 PCI 裝置完全配置,VMBus 通道僅在 Linux 更改客戶機中要中斷的 vCPU 時,或在虛擬機器執行時 vPCI 裝置從虛擬機器中移除時使用。裝置的持續操作直接在裝置的 Linux 裝置驅動程式和硬體之間進行,VMBus 和 VMBus 通道不發揮任何作用。
PCI 裝置設定¶
PCI 裝置設定遵循 Hyper-V 最初為 Windows 客戶機建立的序列,由於 Linux PCI 子系統的整體結構與 Windows 不同,這可能不太適合 Linux 客戶機。儘管如此,透過對 Linux 的 Hyper-V 虛擬 PCI 驅動程式進行一些“黑科技”改造,虛擬 PCI 裝置在 Linux 中得以設定,從而使通用的 Linux PCI 子系統程式碼和該裝置的 Linux 驅動程式能夠“正常工作”。
每個 vPCI 裝置都在 Linux 中設定為其自己的 PCI 域,帶有一個主機橋接。PCI domainID 派生自分配給 VMBus vPCI 裝置的例項 GUID 的第 4 和第 5 位元組。Hyper-V 主機不保證這些位元組是唯一的,因此 hv_pci_probe() 有一個演算法來解決衝突。衝突解決旨在在同一虛擬機器的重啟之間保持穩定,以便 PCI domainID 不會改變,因為 domainID 出現在某些裝置的使用者空間配置中。
hv_pci_probe() 分配一個客戶機 MMIO 範圍,用作裝置的 PCI 配置空間。此 MMIO 範圍透過 VMBus 通道與 Hyper-V 主機通訊,作為告知主機裝置已準備好進入 d0 的一部分。請參見 hv_pci_enter_d0()。當客戶機隨後訪問此 MMIO 範圍時,Hyper-V 主機將攔截這些訪問並將其對映到物理裝置 PCI 配置空間。
hv_pci_probe() 還從 Hyper-V 主機獲取裝置的 BAR 資訊,並使用此資訊為 BAR 分配 MMIO 空間。然後將該 MMIO 空間設定為與主機橋接關聯,以便在 Linux 中的通用 PCI 子系統程式碼處理 BAR 時其能正常工作。
最後,hv_pci_probe() 建立根 PCI 匯流排。至此,Hyper-V 虛擬 PCI 驅動程式的“黑科技”工作就完成了,正常的 Linux PCI 機制會掃描根匯流排以檢測裝置、執行驅動程式匹配並初始化驅動程式和裝置。
PCI 裝置移除¶
Hyper-V 主機可以在虛擬機器的整個生命週期中的任何時間發起從客戶虛擬機器中移除 vPCI 裝置。移除是由 Hyper-V 主機上的管理員操作引起的,不受客戶機作業系統的控制。
透過與 vPCI 裝置關聯的 VMBus 通道,主機向客戶機發送未經請求的“彈出 (Eject)”訊息,以通知客戶虛擬機器裝置已被移除。收到此類訊息後,Linux 中的 Hyper-V 虛擬 PCI 驅動程式會非同步呼叫 Linux 核心 PCI 子系統函式來關閉和移除裝置。當這些呼叫完成後,一個“彈出完成 (Ejection Complete)”訊息透過 VMBus 通道傳送回 Hyper-V,表明裝置已被移除。此時,Hyper-V 向 Linux 客戶機發送一個 VMBus rescind 訊息,Linux 中的 VMBus 驅動程式透過移除裝置的 VMBus 身份來處理此訊息。一旦該處理完成,裝置存在的所有痕跡都將從 Linux 核心中消失。rescind 訊息還向客戶機表明 Hyper-V 已停止在客戶機中為 vPCI 裝置提供支援。如果客戶機嘗試訪問該裝置的 MMIO 空間,這將是無效引用。影響裝置的 Hypercall 將返回錯誤,並且 VMBus 通道中傳送的任何進一步訊息都將被忽略。
傳送 Eject 訊息後,Hyper-V 允許客戶虛擬機器 60 秒的時間來乾淨地關閉裝置並響應 Ejection Complete,然後才傳送 VMBus rescind 訊息。如果由於任何原因,Eject 步驟未在允許的 60 秒內完成,Hyper-V 主機將強制執行 rescind 步驟,這可能會導致客戶機中出現級聯錯誤,因為從客戶機的角度來看,裝置現在已不存在,並且訪問裝置 MMIO 空間將失敗。
由於彈出是非同步的,並且可以在客戶虛擬機器生命週期中的任何時候發生,因此 Hyper-V 虛擬 PCI 驅動程式中的正確同步非常棘手。甚至在剛提供的 vPCI 裝置完全設定之前,就已經觀察到彈出。多年來,Hyper-V 虛擬 PCI 驅動程式已多次更新,以修復在不合時宜的時間發生彈出時的競態條件。修改此程式碼時必須小心,以防止重新引入此類問題。請參閱程式碼中的註釋。
中斷分配¶
Hyper-V 虛擬 PCI 驅動程式支援使用 MSI、multi-MSI 或 MSI-X 的 vPCI 裝置。為特定 MSI 或 MSI-X 訊息分配將接收中斷的客戶機 vCPU 是複雜的,因為 Linux 中 IRQ 的設定方式對映到 Hyper-V 介面。對於單 MSI 和 MSI-X 情況,Linux 會兩次呼叫 hv_compose_msi_msg(),第一次呼叫包含一個虛擬 vCPU,第二次呼叫包含實際 vCPU。此外,hv_irq_unmask() 最終會被呼叫(在 x86 上)或 GICD 暫存器會被設定(在 arm64 上)以再次指定實際 vCPU。這三個呼叫中的每一個都與 Hyper-V 互動,Hyper-V 必須在將中斷轉發到客戶虛擬機器之前決定哪個物理 CPU 應該接收中斷。不幸的是,Hyper-V 的決策過程有點受限,可能會導致物理中斷集中在一個 CPU 上,從而造成效能瓶頸。有關此問題如何解決的詳細資訊,請參閱函式 hv_compose_msi_req_get_cpu() 上方的詳細註釋。
Hyper-V 虛擬 PCI 驅動程式將 irq_chip.irq_compose_msi_msg 函式實現為 hv_compose_msi_msg()。不幸的是,在 Hyper-V 上,該實現需要向 Hyper-V 主機發送 VMBus 訊息,並等待一箇中斷指示收到回覆訊息。由於 irq_chip.irq_compose_msi_msg 可以在持有 IRQ 鎖的情況下被呼叫,因此無法像通常那樣透過休眠直到被中斷喚醒的方式工作。相反,hv_compose_msi_msg() 必須傳送 VMBus 訊息,然後輪詢完成訊息。更復雜的是,在輪詢進行期間,vPCI 裝置可能會被彈出/撤銷,因此也必須檢測到這種情況。請參閱程式碼中關於此非常棘手區域的註釋。
Hyper-V 虛擬 PCI 驅動程式 (pci-hyperv.c) 中的大部分程式碼適用於在 x86 和 arm64 架構上執行的 Hyper-V 和 Linux 客戶機。但在中斷分配的管理方式上存在差異。在 x86 上,客戶機中的 Hyper-V 虛擬 PCI 驅動程式必須進行 hypercall,以告知 Hyper-V 哪個客戶機 vCPU 應該被每個 MSI/MSI-X 中斷中斷,以及 x86_vector IRQ 域為該中斷選擇的 x86 中斷向量號。此 hypercall 由 hv_arch_irq_unmask() 進行。在 arm64 上,Hyper-V 虛擬 PCI 驅動程式管理每個 MSI/MSI-X 中斷的 SPI 分配。Hyper-V 虛擬 PCI 驅動程式將分配的 SPI 儲存在架構 GICD 暫存器中,Hyper-V 會模擬這些暫存器,因此不需要像 x86 那樣進行 hypercall。Hyper-V 不支援在 arm64 客戶虛擬機器中為 vPCI 裝置使用 LPI,因為它不模擬 GICv3 ITS。
Linux 中的 Hyper-V 虛擬 PCI 驅動程式支援其驅動程式建立託管或非託管 Linux IRQ 的 vPCI 裝置。如果透過 /proc/irq 介面更新非託管 IRQ 的 smp_affinity,Hyper-V 虛擬 PCI 驅動程式會被呼叫以告知 Hyper-V 主機更改中斷目標,並且一切正常工作。但是,在 x86 上,如果 x86_vector IRQ 域由於 CPU 上向量耗盡而需要重新分配中斷向量,則沒有路徑通知 Hyper-V 主機此更改,從而導致問題。幸運的是,客戶虛擬機器在受限的裝置環境中執行,不會發生耗盡 CPU 上所有向量的情況。由於此類問題只是理論上的擔憂而非實際擔憂,因此尚未得到解決。
DMA¶
預設情況下,Hyper-V 在建立虛擬機器時將所有客戶虛擬機器記憶體固定在主機中,並程式設計物理 IOMMU 以允許虛擬機器對其所有記憶體進行 DMA 訪問。因此,將 PCI 裝置分配給虛擬機器是安全的,並允許客戶作業系統程式設計 DMA 傳輸。物理 IOMMU 可防止惡意客戶機向屬於主機或主機上其他虛擬機器的記憶體發起 DMA。從 Linux 客戶機的角度來看,此類 DMA 傳輸處於“直接”模式,因為 Hyper-V 不在客戶機中提供虛擬 IOMMU。
Hyper-V 假定物理 PCI 裝置始終執行快取一致性 DMA。在 x86 上執行時,這是架構所要求的行為。在 arm64 上執行時,架構允許快取一致性和非快取一致性裝置,每個裝置的行為在 ACPI DSDT 中指定。但是,當 PCI 裝置分配給客戶虛擬機器時,該裝置不會出現在 DSDT 中,因此 Hyper-V VMBus 驅動程式將 ACPI DSDT 中 VMBus 節點的快取一致性資訊傳播到所有 VMBus 裝置,包括 vPCI 裝置(因為它們具有 VMBus 裝置和 PCI 裝置的雙重身份)。請參見 vmbus_dma_configure()。當前的 Hyper-V 版本始終指示 VMBus 是快取一致的,因此 arm64 上的 vPCI 裝置總是被標記為快取一致的,並且 CPU 不會在 dma_map/unmap_*() 呼叫中執行任何同步操作。
vPCI 協議版本¶
如前所述,在 vPCI 裝置設定和拆除期間,訊息透過 Hyper-V 主機和 Linux 客戶機中的 Hyper-V vPCI 驅動程式之間的 VMBus 通道傳遞。一些訊息在 Hyper-V 的新版本中進行了修訂,因此客戶機和主機必須就使用的 vPCI 協議版本達成一致。該版本在 VMBus 通道首次建立通訊時協商。請參見 hv_pci_protocol_negotiation()。較新版本的協議擴充套件了對具有超過 64 個 vCPU 的虛擬機器的支援,並提供了有關 vPCI 裝置的額外資訊,例如它在底層硬體中最緊密關聯的客戶機虛擬 NUMA 節點。
客戶機 NUMA 節點親和性¶
當 vPCI 協議版本提供時,vPCI 裝置的客戶機 NUMA 節點親和性會作為 Linux 裝置資訊的一部分儲存,以供 Linux 驅動程式後續使用。請參見 hv_pci_assign_numa_node()。如果協商的協議版本不支援主機提供 NUMA 親和性資訊,Linux 客戶機將裝置 NUMA 節點預設為 0。但即使協商的協議版本包含 NUMA 親和性資訊,主機提供此類資訊的能力也取決於某些主機配置選項。如果客戶機收到 NUMA 節點值“0”,它可能表示 NUMA 節點 0,也可能表示“沒有可用資訊”。不幸的是,從客戶機端無法區分這兩種情況。
CoCo 虛擬機器中的 PCI 配置空間訪問¶
Linux PCI 裝置驅動程式使用 Linux PCI 子系統提供的一組標準函式訪問 PCI 配置空間。在 Hyper-V 客戶機中,這些標準函式對映到 Hyper-V 虛擬 PCI 驅動程式中的 hv_pcifront_read_config() 和 hv_pcifront_write_config() 函式。在普通虛擬機器中,這些 hv_pcifront_*() 函式直接訪問 PCI 配置空間,並且訪問會陷入 Hyper-V 進行處理。但在 CoCo 虛擬機器中,記憶體加密會阻止 Hyper-V 讀取客戶機指令流以模擬訪問,因此 hv_pcifront_*() 函式必須透過顯式引數描述要進行的訪問來呼叫 hypercalls。
配置塊後向通道¶
Hyper-V 主機和 Linux 中的 Hyper-V 虛擬 PCI 驅動程式共同實現了一個主機和客戶機之間非標準的後向通訊路徑。該後向通道路徑使用透過與 vPCI 裝置關聯的 VMBus 通道傳送的訊息。函式 hyperv_read_cfg_blk() 和 hyperv_write_cfg_blk() 是提供給 Linux 核心其他部分的主要介面。截至本文撰寫之時,這些介面僅由 Mellanox mlx5 驅動程式用於將診斷資料傳遞給在 Azure 公有云中執行的 Hyper-V 主機。函式 hyperv_read_cfg_blk() 和 hyperv_write_cfg_blk() 在一個單獨的模組中實現(pci-hyperv-intf.c,在 CONFIG_PCI_HYPERV_INTERFACE 下),該模組在非 Hyper-V 環境中執行時有效地將它們stub out。