7. PCI 錯誤恢復

作者:

許多 PCI 匯流排控制器能夠檢測總線上各種硬體 PCI 錯誤,例如資料和地址總線上的奇偶校驗錯誤,以及 SERR 和 PERR 錯誤。一些更高階的晶片組能夠處理這些錯誤;這些晶片組包括 PCI-E 晶片組,以及在基於 IBM Power4、Power5 和 Power6 的 pSeries 盒子上找到的 PCI 主橋。採取的典型操作是斷開受影響的裝置,停止所有到它的 I/O。斷開連線的目的是避免系統損壞;例如,停止由於 DMAs 定址到“錯誤”地址而導致的系統記憶體損壞。通常,還會提供一種重新連線機制,以便重置受影響的 PCI 裝置並使其恢復工作狀態。本文件描述了一個通用 API,用於通知裝置驅動程式匯流排斷開連線,然後執行錯誤恢復。此 API 目前在 2.6.16 及更高版本的核心中實現。

報告和恢復分幾個步驟執行。首先,當 PCI 硬體錯誤導致匯流排斷開連線時,該事件會盡快報告給所有受影響的裝置驅動程式,包括多功能卡上的裝置驅動程式的多個例項。這允許裝置驅動程式避免在自旋鎖中死鎖,等待某些 i/o 空間暫存器改變,而它永遠不會改變。它還使驅動程式有機會根據需要延遲傳入的 I/O。

接下來,恢復分幾個階段執行。大部分複雜性是由於需要處理多功能裝置而導致的,即與多個裝置驅動程式關聯的裝置。在第一階段,允許每個驅動程式指示它希望的重置型別,選擇是簡單地重新啟用 I/O 或請求插槽重置。

如果任何驅動程式請求插槽重置,那麼將執行該操作。

在重置和/或重新啟用 I/O 之後,會再次通知所有驅動程式,以便它們可以執行任何可能需要的裝置設定/配置。在所有這些都完成後,會發出一個最終的“恢復正常操作”事件。

選擇基於核心的實現而不是使用者空間實現的最大原因是需要處理連線到儲存介質的 PCI 裝置的匯流排斷開連線,特別是與儲存根檔案系統的裝置的斷開連線。如果根檔案系統斷開連線,使用者空間機制將不得不經歷大量扭曲才能完成恢復。幾乎所有當前的 Linux 檔案系統都不能容忍與其底層塊裝置的斷開連線/重新連線。相比之下,匯流排錯誤很容易在裝置驅動程式中管理。實際上,大多數裝置驅動程式已經處理非常相似的恢復過程;例如,SCSI 通用層已經提供了用於處理 SCSI 匯流排錯誤和 SCSI 匯流排重置的重要機制。

7.1. 詳細設計

以下是基於與 Ben Herrenschmidt 大約在 2005 年 4 月 5 日進行的公共電子郵件討論鏈的設計和實現細節。

錯誤恢復 API 支援以函式指標結構的形式暴露給驅動程式,該結構由 struct pci_driver 中的新欄位指向。 未能提供結構的驅動程式是“非感知的”,並且採取的實際恢復步驟取決於平臺。 arch/powerpc 實現將模擬 PCI 熱插拔移除/新增。

此結構的形式如下

struct pci_error_handlers
{
        int (*error_detected)(struct pci_dev *dev, pci_channel_state_t);
        int (*mmio_enabled)(struct pci_dev *dev);
        int (*slot_reset)(struct pci_dev *dev);
        void (*resume)(struct pci_dev *dev);
        void (*cor_error_detected)(struct pci_dev *dev);
};

可能的通道狀態是

typedef enum {
        pci_channel_io_normal,  /* I/O channel is in normal state */
        pci_channel_io_frozen,  /* I/O to channel is blocked */
        pci_channel_io_perm_failure, /* PCI card is dead */
} pci_channel_state_t;

可能的返回值是

enum pci_ers_result {
        PCI_ERS_RESULT_NONE,        /* no result/none/not supported in device driver */
        PCI_ERS_RESULT_CAN_RECOVER, /* Device driver can recover without slot reset */
        PCI_ERS_RESULT_NEED_RESET,  /* Device driver wants slot to be reset. */
        PCI_ERS_RESULT_DISCONNECT,  /* Device has completely failed, is unrecoverable */
        PCI_ERS_RESULT_RECOVERED,   /* Device driver is fully recovered and operational */
};

驅動程式不必實現所有這些回撥;但是,如果它實現任何一個,它必須實現 error_detected()。 如果未實現回撥,則認為不支援相應的功能。 例如,如果 mmio_enabled() 和 resume() 不存在,則假定驅動程式沒有進行任何直接恢復,並且需要插槽重置。 通常,驅動程式會想要知道 slot_reset()。

平臺為從 PCI 錯誤事件中恢復而採取的實際步驟將取決於平臺,但將遵循下面描述的一般順序。

7.1.1. 步驟 0:錯誤事件

PCI 匯流排錯誤由 PCI 硬體檢測到。 在 powerpc 上,該插槽被隔離,因為所有 I/O 都被阻止:所有讀取都返回 0xffffffff,所有寫入都被忽略。

7.1.2. 步驟 1:通知

平臺在受錯誤影響的每個驅動程式的每個例項上呼叫 error_detected() 回撥。

此時,根據平臺的不同,該裝置可能不再可訪問(該插槽將在 powerpc 上隔離)。 驅動程式可能已經由於 I/O 失敗而“注意到”該錯誤,但這是正確的“同步點”,也就是說,它使驅動程式有機會清理,等待掛起的東西(定時器,等等……)完成; 它可以獲取訊號量,排程等……除了觸控裝置之外的所有內容。 在此函式中以及在其返回之後,驅動程式不應進行任何新的 IO。 在任務上下文中呼叫。 這有點像“靜默”點。 請參閱本文件末尾有關中斷的註釋。

參與此係統的所有驅動程式都必須實現此呼叫。 驅動程式必須返回以下結果程式碼之一

  • PCI_ERS_RESULT_CAN_RECOVER

    如果驅動程式認為它可能僅透過敲打 IO 來恢復 HW,或者如果它希望有機會提取一些診斷資訊(請參閱下面的 mmio_enable),則驅動程式返回此值。

  • PCI_ERS_RESULT_NEED_RESET

    如果驅動程式無法在沒有插槽重置的情況下恢復,則驅動程式返回此值。

  • PCI_ERS_RESULT_DISCONNECT

    如果驅動程式根本不想恢復,則驅動程式返回此值。

採取的下一步將取決於驅動程式返回的結果程式碼。

如果段/插槽上的所有驅動程式都返回 PCI_ERS_RESULT_CAN_RECOVER,那麼平臺應重新啟用插槽上的 IO(或者如果不隔離插槽,則不執行任何特殊操作),並且恢復進入步驟 2(MMIO 啟用)。

如果任何驅動程式請求插槽重置(透過返回 PCI_ERS_RESULT_NEED_RESET),那麼恢復進入步驟 4(插槽重置)。

如果平臺無法恢復插槽,則下一步是步驟 6(永久故障)。

注意

當前的 powerpc 實現假定裝置驅動程式_不會_在此例程中排程或使用訊號量;當前的 powerpc 實現使用一個核心執行緒來通知所有裝置;因此,如果一個裝置睡眠/排程,則所有裝置都會受到影響。 更好地執行需要在錯誤恢復實現中進行復雜的多執行緒邏輯(例如,等待所有通知執行緒在繼續恢復之前“加入”)。 這似乎過於複雜,不值得實現。

當前的 powerpc 實現不太關心裝置此時是否嘗試 I/O。 I/O 將失敗,在讀取時返回 0xff,並且寫入將被刪除。 如果嘗試了超過 EEH_MAX_FAILS 次 I/O 到凍結的介面卡,則 EEH 假定裝置驅動程式已進入無限迴圈,並在 syslog 中列印錯誤。 然後需要重新啟動才能使裝置再次工作。

7.1.3. 步驟 2:MMIO 已啟用

平臺重新啟用到該裝置的 MMIO(但通常不啟用 DMA),然後在所有受影響的裝置驅動程式上呼叫 mmio_enabled() 回撥。

這是“早期恢復”呼叫。 再次允許 IO,但 DMA 不允許,但有一些限制。 這不是驅動程式再次開始操作的回撥,僅用於檢視/探測裝置,提取診斷資訊(如果有),並最終執行諸如觸發裝置本地重置之類的操作,但不重新啟動操作。 如果某個段上的所有驅動程式都同意它們可以嘗試恢復,並且 HW 沒有執行自動鏈路重置,則進行此回撥。 如果平臺無法僅在沒有插槽重置或鏈路重置的情況下重新啟用 IO,則它將不會呼叫此回撥,而是直接進入步驟 3(鏈路重置)或步驟 4(插槽重置)

注意

提出以下建議;尚無平臺實現此建議:建議:所有 I/O 都應在此回撥中_同步_完成,它們觸發的錯誤將透過正常的 pci_check_whatever() API 返回,由於此處發生的錯誤,將不會發出新的 error_detected() 回撥。 但是,此類錯誤可能會導致為整個段重新阻止 IO,從而使同一段上的其他裝置可能已經完成的恢復無效,從而迫使整個段進入下一個狀態之一,即鏈路重置或插槽重置。

驅動程式應返回以下結果程式碼之一
  • PCI_ERS_RESULT_RECOVERED

    如果驅動程式認為該裝置功能齊全,並且認為可以再次開始正常驅動程式操作,則驅動程式返回此值。 不能保證驅動程式實際上將被允許繼續進行,因為同一段上的另一個驅動程式可能已失敗,從而在支援它的平臺上觸發了插槽重置。

  • PCI_ERS_RESULT_NEED_RESET

    如果驅動程式認為該裝置在其當前狀態下無法恢復,並且需要插槽重置才能繼續,則驅動程式返回此值。

  • PCI_ERS_RESULT_DISCONNECT

    與上面相同。 完全失敗,即使在重置驅動程式死後也無法恢復。 (有待更精確的定義)

下一步取決於驅動程式返回的結果。 如果所有驅動程式都返回 PCI_ERS_RESULT_RECOVERED,則平臺將進入步驟 3(鏈路重置)或步驟 5(恢復操作)。

如果任何驅動程式返回 PCI_ERS_RESULT_NEED_RESET,則平臺將進入步驟 4(插槽重置)

7.1.5. 步驟 4:插槽重置

為了響應 PCI_ERS_RESULT_NEED_RESET 的返回值,平臺將對請求的 PCI 裝置執行插槽重置。 平臺為執行插槽重置而採取的實際步驟將取決於平臺。 完成插槽重置後,平臺將呼叫裝置 slot_reset() 回撥。

Powerpc 平臺實現兩個級別的插槽重置:軟重置(預設)和基本重置(可選)。

Powerpc 軟重置包括斷言介面卡 #RST 行,然後將 PCI BAR 和 PCI 配置標頭恢復到與新鮮系統電源開啟,然後是電源開啟 BIOS/系統韌體初始化後的狀態等效的狀態。 軟重置也稱為熱重置。

Powerpc 基本重置僅由 PCI Express 卡支援,並導致裝置的有限狀態機、硬體邏輯、埠狀態和配置暫存器初始化為其預設條件。

對於大多數 PCI 裝置,軟重置足以進行恢復。 提供了可選的基本重置,以支援少量 PCI Express 裝置,對於這些裝置,軟重置不足以進行恢復。

如果平臺支援 PCI 熱插拔,那麼可以透過切換插槽電源來執行重置。

平臺將 PCI 配置空間恢復到“新鮮電源開啟”狀態,而不是“最後狀態”,這一點很重要。 在插槽重置後,裝置驅動程式幾乎總是使用其標準裝置初始化例程,並且異常的配置空間設定可能會導致裝置掛起、核心崩潰或靜默資料損壞。

此呼叫使驅動程式有機會重新初始化硬體(重新下載韌體等)。 此時,驅動程式可以假定該卡處於新鮮狀態並且功能齊全。 該插槽已解除凍結,並且驅動程式可以完全訪問 PCI 配置空間、記憶體對映 I/O 空間和 DMA。 中斷(傳統、MSI 或 MSI-X)也將可用。

驅動程式此時不應重新啟動正常的 I/O 處理操作。 如果所有裝置驅動程式在此回撥上報告成功,則平臺將呼叫 resume() 以完成該序列,並讓驅動程式重新啟動正常的 I/O 處理。

如果驅動程式在重置後仍無法使裝置執行,則該驅動程式仍然可以為此函式返回嚴重故障。 如果平臺之前嘗試了軟重置,則它現在可能會嘗試硬重置(電源迴圈),然後再次呼叫 slot_reset()。 如果裝置仍然無法恢復,則無法再做任何事情;在這種情況下,平臺通常會報告“永久故障”。 在這種情況下,該裝置將被視為“已死”。

多功能卡的驅動程式將需要相互協調,以確定哪個驅動程式例項將執行任何“一次性”或全域性裝置初始化。 例如,Symbios sym53cxx2 驅動程式僅從 PCI 功能 0 執行裝置初始化

+       if (PCI_FUNC(pdev->devfn) == 0)
+               sym_reset_scsi_bus(np, 0);
結果程式碼
  • PCI_ERS_RESULT_DISCONNECT 與上面相同。

需要基本重置的 PCI Express 卡的驅動程式必須在其探測函式中設定 pci_dev 結構中的 needs_freset 位。 例如,QLogic qla2xxx 驅動程式為某些 PCI 卡型別設定 needs_freset 位

+       /* Set EEH reset type to fundamental if required by hba  */
+       if (IS_QLA24XX(ha) || IS_QLA25XX(ha) || IS_QLA81XX(ha))
+               pdev->needs_freset = 1;
+

平臺將進入步驟 5(恢復操作)或步驟 6(永久故障)。

注意

如果驅動程式返回 PCI_ERS_RESULT_DISCONNECT,則當前的 powerpc 實現不會嘗試電源迴圈重置。 但是,它可能應該這樣做。

7.1.6. 步驟 5:恢復操作

如果段上的所有驅動程式都從先前的 3 個回撥之一返回 PCI_ERS_RESULT_RECOVERED,則平臺將在所有受影響的裝置驅動程式上呼叫 resume() 回撥。 此回撥的目標是告訴驅動程式重新啟動活動,一切都已恢復並正在執行。 此回撥不返回結果程式碼。

此時,如果發生新錯誤,平臺將重新啟動新的錯誤恢復序列。

7.1.7. 步驟 6:永久故障

發生了“永久故障”,平臺無法恢復該裝置。 平臺將使用 pci_channel_state_t 值 pci_channel_io_perm_failure 呼叫 error_detected()。

此時,裝置驅動程式應假定最壞的情況。 它應取消所有掛起的 I/O,拒絕所有新的 I/O,並向更高的層返回 -EIO。 然後,裝置驅動程式應清除其所有記憶體並將其自身從核心操作中移除,就像在系統關閉期間一樣。

平臺通常會以某種方式通知系統操作員永久故障。 如果該裝置支援熱插拔,則操作員可能會想要移除並更換該裝置。 但是,請注意,並非所有故障都是真正“永久的”。 有些是由過熱引起的,有些是由卡座不良引起的。 許多 PCI 錯誤事件是由軟體錯誤引起的,例如,DMAs 定址到錯誤地址或由於程式設計錯誤引起的偽造拆分事務。 有關軟體錯誤原因的實際經驗的更多詳細資訊,請參見 PCI 匯流排 EEH 錯誤恢復 中的討論。

7.1.8. 結論;一般性評論

呼叫回撥的方式是平臺策略。 沒有插槽重置功能的平臺可能只想“忽略”無法恢復的驅動程式(斷開它們的連線),並嘗試讓同一段上的其他卡恢復。 但是請記住,在大多數實際情況下,每個段只有一個驅動程式。

現在,關於中斷的註釋。 如果您收到中斷並且您的裝置已死或已隔離,則存在問題 :) 當前策略是將此轉化為平臺策略。 也就是說,恢復 API 僅要求

  • 無法保證從錯誤檢測開始到呼叫 slot_reset 回撥為止,可以從該段上的任何裝置進行中斷傳遞,此時預計中斷將完全可操作。

  • 不能保證中斷傳遞已停止,也就是說,在檢測到錯誤後或在中斷處理程式中檢測到錯誤的驅動程式(以防止正確確認中斷(從而刪除源))應僅返回 IRQ_NOTHANDLED。 由平臺來處理該狀況,通常是在錯誤處理期間遮蔽 IRQ 源。 預計平臺“知道”哪些中斷已路由到能夠進行錯誤管理的插槽,並且可以在錯誤處理期間處理暫時停用該 IRQ 編號(這不是很複雜)。 這意味著共享中斷的其他裝置的一些 IRQ 延遲,但根本沒有其他方法。 高階平臺無論如何都不應該在許多裝置之間共享中斷:)

注意

有關 powerpc 平臺的實現細節,請參見檔案 PCI 匯流排 EEH 錯誤恢復

在撰寫本文時,實現錯誤恢復的補丁的裝置驅動程式列表越來越多。 並非所有這些補丁都在主線中。 這些可以用作“示例”

  • drivers/scsi/ipr

  • drivers/scsi/sym53c8xx_2

  • drivers/scsi/qla2xxx

  • drivers/scsi/lpfc

  • drivers/next/bnx2.c

  • drivers/next/e100.c

  • drivers/net/e1000

  • drivers/net/e1000e

  • drivers/net/ixgbe

  • drivers/net/cxgb3

  • drivers/net/s2io.c

當錯誤嚴重性為“可糾正”時,會在 handle_error_source() 中呼叫 cor_error_detected() 回撥。 該回調是可選的,並且如果需要,允許進行額外的日誌記錄。 請參閱示例

  • drivers/cxl/pci.c

7.1.9. 結束