RFC-0070:支援舊版服務中斷的 PCI 通訊協定

RFC-0070:PCI 通訊協定變更,以支援舊有的中斷
狀態已接受
區域
  • 核心
說明

針對虛假的 PCI 舊版中斷事件提供緩解措施。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2020-01-17
審查日期 (年-月-日)2020-02-25

摘要

在使用者空間中,PCI 匯流排驅動程式庫需要能夠停用舊版層級觸發的中斷,直到裝置中斷服務完成為止,以免同一個 IRQ 持續喚醒匯流排驅動程式的 IRQ 執行緒。為達成這項目標,我們需要讓裝置驅動程式庫能夠通知匯流排驅動程式庫,表示它已準備好服務新的中斷,並重新啟用舊版中斷產生功能。

提振精神

大多數現代 PCI 裝置會透過 PCI 設定空間中的選用 MSI 或 MSI-X 功能控制訊息信號中斷 (MSI),這些中斷是特定裝置專屬,並由核心句柄和 MsiInterruptDispatcher 之間的直接對應關係管理。每個 MSI 都只提供給單一裝置,且從驅動程式庫角度來看,可視為標準系統中斷。

不過,PCI 舊版中斷會透過一組在所有 PCI 裝置間共用的中斷線運作,並在系統韌體表格中詳細列出,例如 PCI 韌體規格定義的 ACPI。這些中斷是根據規格以電平觸發並啟用低電平。當中斷觸發時,系統軟體負責判斷哪個裝置觸發中斷,以便服務並釋放該裝置。在 Kernel PCI 匯流排驅動程式 (kPCI) 中,這項作業會由所有舊版中斷處理,這些中斷會向核心中的所有 PciInterruptDispatcher 註冊共用中斷處理常式。這個處理常式會判斷哪部裝置產生了中斷,並傳送適當的中斷物件信號。裝置產生中斷的功能隨即停用。下次驅動程式庫等待調度器時,Unmask 鉤子會解除遮罩並重新啟用裝置的舊版中斷產生能力。

有了使用者空間 PCI 匯流排驅動程式 (uPCI),所有機制都已移至使用者空間。uPCI 本身現在會運作低負載 IRQ 工作站,以便判斷負責的裝置,並發出裝置驅動程式庫會與之互動的虛擬中斷訊號。不過,由於中斷是依層級而定,如果我們缺少裝置的驅動程式庫,或是特定驅動程式庫無法妥善處理中斷,中斷會持續觸發。不過,如果 uPCI 停用裝置的中斷,讓驅動程式庫可以處理中斷,我們就沒有現有方法可讓 PCI 裝置驅動程式庫重新啟用中斷。目前沒有任何方式可通知 uPCI 匯流排驅動程式,裝置驅動程式庫已在提供的虛擬中斷上呼叫 zx_interrupt_waitzx_port_wait,因此匯流排驅動程式不知道何時應重新啟用裝置的中斷。

設計

我們需要針對 PCI 驅動程式中的兩種中斷用法進行設計。

  1. 知道自己是 PCI 驅動程式,並直接呼叫 PciProtocol 方法的驅動程式。
  2. 以無法使用 PciProtocol 方法的方式使用中斷的驅動程式。

當使用 PCI_IRQ_MODE_LEGACY 設定的舊式中斷事件針對共用連線觸發時,Bus 驅動程式庫會負責通知正確的裝置。與 kPCI 驅動程式庫類似,當匯流排向驅動程式庫發出中斷可供服務的信號時,匯流排會停用裝置的舊版中斷,有效地遮蔽中斷。我們將新增 PCI 通訊協定方法,讓裝置驅動程式庫可要求重新啟用/解除遮罩中斷。對於可能與某些設定中使用舊版中斷的裝置互動的驅動程式,這項呼叫是必要的,但對於只使用 MSI 運作的裝置,則不需要變更。這樣就不會發生不必要的中斷,且符合上述第一種用途的需求。

這與 Linux 處理使用者空間 I/O 中斷的方式類似

針對第二種用途,我們會建立替代舊版 IRQ 模式 PCI_IRQ_MODE_LEGACY_ACKLESSConfigureIrqMode() 不會選取這個模式,必須由具備此獨特需求的驅動程式特別選取。系統會監控以這種方式設定中斷的裝置,查看每秒中斷次數是否超過所設定的數字。在這種情況下,裝置產生中斷的功能會遭到停用。這與 Linux 處理啟動中斷事件的運作方式類似。

實作

變更作業可以按順序進行,且不會有任何遷移或 CQ 問題。

  1. 修改 pci_configure_irq_mode 以新增 out 參數,為不支援中斷模式的驅動程式儲存所選的 IRQ 模式,並更新現有的呼叫端。
  2. 新增通訊協定方法 pci_legacy_interrupt_ack (或簡稱 pci_interrupt_ack),可重新啟用裝置的舊版中斷,並傳回 ZX_OK,如果裝置未設定為使用舊版中斷,則傳回 ZX_ERR_BAD_STATE
  3. 更新 pci_configure_irq_mode 的現有呼叫端和 PCI_IRQ_MODE_LEGACY 的使用者,以便在中斷處理中使用新的通訊協定方法。
  4. 更新以抽象方式處理中斷的驅動程式,並確保它們使用 PCI_IRQ_MODE_LEGACY_NOACK 而非 PCI_IRQ_MODE_LEGACY
  5. 在所有驅動程式遷移完成後,請 uPCI IRQ 工作者在傳送裝置驅動程式庫的虛擬中斷訊號時,停用裝置的舊版中斷產生機制。
  6. PciProtocol banjo 和 Fuchsia.dev 中詳細記錄 PCI 中斷的用法。

成效

遇到的大部分 PCI 裝置都會使用 MSI 運作。仍使用舊有中斷的裝置類型通常僅限於舊硬體、效能要求較低的整合式 SOC 裝置、不支援 MSI 的模擬環境,以及很少使用中斷的裝置。

如要處理舊版中斷的驅動程式,會在中斷處理例行程序中新增額外的管道寫入作業,因為這個新的 PCI 通訊協定方法需要從驅動程式庫 devhost 代理程式寫入 uPCI。您可以透過基準測試呼叫本身,或在 Zircon 匯總基準中檢查管道寫入的匯總成本,來剖析這項資料。

安全性考量

無。

隱私權注意事項

無。

測試

CQ/CI 中的現有整合和端對端測試會驗證中斷功能在變更後是否仍正常運作,而新的單元測試則會驗證對 pci_configure_irq_mode 通訊協定方法所做的變更。

說明文件

需要擴充 PCI 說明文件,以便說明中斷模式的運作原理。此外,您可能需要在 zx_interrupt_waitzx_port_wait 說明文件中註明 pci_legacy_interrupt_ack 的需求。

缺點、替代方案和未知事項

缺點

大多數的驅動程式都會偏好只使用 MSI / MSI-X 中斷模式,因此不需要擔心這個 API,因此系統中只需要要求可能會遇到使用舊版中斷的裝置的驅動程式處理這種情況,而非所有驅動程式。不過,這會導致為特定裝置設定所編寫的驅動程式庫發生問題,導致這些驅動程式只會在無法回應時收到第一次中斷。支援多種裝置的驅動程式可能會出現這個問題。

有多個後端的情況也會增加複雜度。舉例來說,我們的 xHCI 驅動程式庫有類似以下的內容。如果其 PCI 支援涉及舊版中斷,可能會如下所示:

// Initialize the proper setup and obtain an interrupt
if (pci_.is_valid()) {
  pci_init();
} else {
  mmio_init();
}

do {
  // Wait loop on the interrupt
  // Handle the interrupt

  if (mode_ == XHCI_MODE_PCI && irq_mode_ == PCI_IRQ_MODE_LEGACY)
    status = pci_.LegacyInterruptAck();
  }
}

如果省略 ACK 代碼,這個驅動程式庫就能與 MSI 搭配運作,但在舊版模式中,系統不會在第一次之後收到任何中斷。改善 PCI 驅動程式周邊的測試架構,可提供更完善的解決方案,在開發期間找出這些錯誤。

考慮的替代方案

將過多未處理的中斷標示為偽中斷,並停用中斷

與 Linux 類似,我們可以為連續的假中斷設定閾值,如果達到閾值,我們可以停用或忽略該中斷行,直到重新啟動為止。這種做法的主要問題是,當 Linux 處理共用中斷時,會透過核心中的硬 IRQ 處理常式呼叫處理常式鏈結,然後在所有處理常式完成後回應中斷。這可確保所有驅動程式庫中斷處理常式都在 ack 之前執行,因此只有在沒有處理常式正確處理中斷時,才會發生虛假中斷。在 Zircon 中,由於 uPCI 驅動程式庫和裝置驅動程式是處於離線狀態,我們可以傳送信號喚醒 irq 處理執行緒,但無法得知這些執行緒是否已執行完畢。這會導致一般情況下發生不必要的中斷,具體取決於驅動程式庫的 IRQ 執行緒安排和處理特定中斷條件的速度。不過,在一般情況下,這種做法仍會導致比 ack 提案更多的虛假中斷。

新增 PCI 通訊協定方法,以便等待中斷

考慮的選項是新增方法,以便處理等待任何類型的中斷,有效地pci_interrupt_wait避免在中斷中需要額外的條件。

不幸的是,這會導致 PCI 中斷需要以不同於其他中斷的方式處理。我認為,盡可能讓任何中斷物件與任何其他中斷物件使用相同的介面,可帶來許多好處,而且驅動程式庫作者也必須能夠繼續使用 zx_interrupt_waitzx_port_bindzx_port_waitzx_object_wait_async。系統中的大多數驅動程式都會同時處理 IRQ 連接埠和多個中斷,或多個後端 (UART、PCI、USB),因此請務必不要違反中斷物件周圍的介面。

在衍生 InterruptDispatcher 中處理這個問題

我們可以保留 kPCI 的 PciInterruptDispatcher 概念,並將這項工作委派給它,以便在 PCI 裝置驅動程式庫和核心之間維持中斷處理。不幸的是,這會導致使用者空間 PCI 驅動程式庫和 Zircon 核心之間的耦合程度大幅增加。

  1. 每個專門的 InterruptDispatcher 都必須能夠寫入 PCI 裝置的控制登錄,以便停用舊版中斷。這個方法最好使用 VMO,這與我們對 MSI 的做法類似,或者為任何建立此物件的系統呼叫提供位址。因為 Dispatcher 必須知道對應的裝置,因此無法避免這個問題。
  2. 中斷停用是控制登錄中的單一位元,在匯流排和裝置初始化期間,使用者空間匯流排驅動程式庫經常修改此位元,如果有任何待處理的中斷,就會造成嚴重的競爭條件風險。
  3. 如果我們在裝置本身使用衍生 InterruptDispatcher,仍需處理如何判斷共用行上觸發的中斷事件。由於裝置不再使用匯流排處理中斷,因此我們需要在核心中保留類似 kPCI 的 SharedIrqHandler 邏輯。
  4. 我們現在也需要再次透過核心,瞭解 ACPI / 電路板檔案中的 PCI 舊版 IRQ 路由表。這項作業現在會在使用者空間和 ACPI 中處理。

目前,我認為這個方法沒有合理的發展方向,除非我們願意承認 PCI 是需要一些自訂核心程式碼和約 2 個額外系統呼叫來執行其工作的特殊驅動程式庫。

既有技術與參考資料

在我的研究中,在使用者空間中完全處理這個問題是 Zircon 特有的情況。

  1. OSX 的 DriverKit 會使用 IOInterruptDispatchSource,與中斷設定和處理程序中的處理程序搭配運作。此外,PCIDriverKit 僅支援 MSI 和 MSI-X 中斷模式。

    PCIDriverKit > IOPCIDevice

  2. 大多數 Linux PCI 中斷都是在核心本身中處理。共用中斷會由驅動程式註冊的處理常式鏈結。當中斷觸發時,核心會依序呼叫每個處理常式,直到其中一個處理常式處理中斷為止。如果共用中斷行有足夠的假中斷,而這些假中斷未由任何句柄處理,則核心會停用中斷。

    Linux 啟動中斷

    Linux 也透過 Userspace I/O (UIO) 介面支援簡單的使用者空間驅動程式。這可讓驅動程式庫在提供的 /dev/uioX sysfs 節點上執行阻斷 read(),以便等待中斷。中斷會在觸發時停用,但驅動程式庫可以透過對 sysfs 節點發出 write() 呼叫,重新啟用中斷。

    使用者空間 I/O 操作說明

  3. 大多數 Windows PCI 驅動程式都是使用核心模式驅動程式架構 (KMDF) 建構而成。其中斷處理常式會在核心中斷調度作業的一部分中呼叫,而驅動程式會註冊在 IRQ 情境中執行的處理常式。

    中斷服務例行程序簡介