PCI 匯流排 EEH 錯誤恢復

Linas Vepstas <linas@austin.ibm.com>

2005 年 1 月 12 日

概述:

基於 IBM POWER 的 pSeries 和 iSeries 計算機包含具有擴充套件功能的 PCI 匯流排控制器晶片,用於檢測和報告各種 PCI 匯流排錯誤條件。這些功能統稱為“EEH”,代表“增強型錯誤處理”。 EEH 硬體功能允許清除 PCI 匯流排錯誤並“重啟”PCI 卡,而無需重啟作業系統。

這與傳統的 PCI 錯誤處理形成對比,在傳統的 PCI 錯誤處理中,PCI 晶片直接連線到 CPU,並且錯誤會導致 CPU 機器檢查/檢查停止情況,從而完全停止 CPU。 另一種“傳統”技術是忽略此類錯誤,這可能導致資料損壞(使用者資料或核心資料)、介面卡掛起/無響應或系統崩潰/鎖定。 因此,EEH 背後的想法是,作業系統可以透過保護它免受 PCI 錯誤的影響,並使 OS 能夠“重啟”/恢復單個 PCI 裝置,從而變得更加可靠和健壯。

來自其他供應商的基於 PCI-E 規範的未來系統可能包含類似的功能。

EEH 錯誤的起因

EEH 最初旨在防止硬體故障,例如 PCI 卡因高溫、溼度、灰塵、振動和不良電氣連線而損壞。 在“現實生活中”看到的大多數 EEH 錯誤都是由於 PCI 卡未正確安裝,或者不幸的是,通常是由於裝置驅動程式錯誤、裝置韌體錯誤,有時是 PCI 卡硬體錯誤。

最常見的軟體錯誤是導致設備嘗試 DMA 到系統記憶體中未為該卡保留用於 DMA 訪問的位置的錯誤。 這是一個強大的功能,因為它可以防止因不良 DMA 引起的靜默記憶體損壞。 在過去幾年中,透過這種方式發現並修復了許多裝置驅動程式錯誤。 EEH 錯誤的其他可能原因包括資料或地址線奇偶校驗錯誤(例如,由於未正確安裝的卡導致的電氣連線不良)和 PCI-X 分裂完成錯誤(由於軟體、裝置韌體或裝置 PCI 硬體錯誤)。 透過物理移除並重新安裝 PCI 卡,可以治癒大多數“真正的硬體故障”。

檢測和恢復

在以下討論中,將介紹如何檢測和從 EEH 錯誤中恢復的通用概述。 接下來概述 Linux 核心中的當前實現方式。 實際實現可能會發生變化,並且一些更精細的點仍在爭論中。 如果或其他架構實現類似的功能,這些可能會反過來受到影響。

當 PCI 主橋(PHB,將 PCI 匯流排連線到系統 CPU 電子裝置的匯流排控制器)檢測到 PCI 錯誤情況時,它將“隔離”受影響的 PCI 卡。 隔離將阻止所有寫入(從系統寫入卡,或從卡寫入系統),並且它將導致所有讀取返回 all-ff (8/16/32 位讀取為 0xff、0xffff、0xffffffff)。 選擇此值是因為它與裝置從插槽中物理拔出時獲得的值相同。 這包括訪問 PCI 記憶體、I/O 空間和 PCI 配置空間。 但是,中斷將繼續傳遞。

檢測和恢復是在 ppc64 韌體的幫助下執行的。 Linux 核心中到韌體的程式設計介面被稱為 RTAS(執行時抽象服務)。 Linux 核心不(不應該)直接訪問 PCI 晶片組中的 EEH 功能,主要是因為存在許多不同的晶片組,每個晶片組都具有不同的介面和怪癖。 韌體提供了一個統一的抽象層,可與所有 pSeries 和 iSeries 硬體配合使用(並且具有前向相容性)。

如果 OS 或裝置驅動程式懷疑 PCI 插槽已被 EEH 隔離,則可以呼叫韌體來確定是否是這種情況。 如果是這樣,則裝置驅動程式應將自身置於一致的狀態(因為它無法完成任何未完成的工作)並開始恢復卡。 恢復通常包括重置 PCI 裝置(將 PCI #RST 線保持高電平兩秒鐘),然後設定裝置配置空間(基地址暫存器 (BAR)、延遲定時器、快取行大小、中斷線等)。 接下來是裝置驅動程式的重新初始化。 在最壞的情況下,可以切換卡上的電源,至少在支援熱插拔的插槽上是這樣。 原則上,遠高於裝置驅動程式的層可能不需要知道 PCI 卡已透過這種方式“重啟”; 理想情況下,在重置卡時,乙太網/磁碟/USB I/O 最多應該暫停一下。

如果卡在三次或四次重置後無法恢復,則核心/裝置驅動程式應假設最壞的情況,即卡已完全損壞,並將此錯誤報告給系統管理員。 此外,錯誤訊息透過 RTAS 報告,也透過 syslogd (/var/log/messages) 報告,以提醒系統管理員 PCI 重置。 處理失敗介面卡的正確方法是使用標準的 PCI 熱插拔工具來移除和更換損壞的卡。

當前 PPC64 Linux EEH 實現

目前,已經實現了一種通用的 EEH 恢復機制,因此無需修改單個裝置驅動程式即可支援 EEH 恢復。 這種通用機制搭在 PCI 熱插拔基礎設施上,並將事件滲透到使用者空間/udev 基礎設施中。 以下是對如何完成此操作的詳細描述。

在引導過程中,以及如果熱插拔 PCI 插槽,則必須在 PHB 中非常早地啟用 EEH。 前者由 arch/powerpc/platforms/pseries/eeh.c 中的 eeh_init() 執行,後者由 drivers/pci/hotplug/pSeries_pci.c 呼叫到 eeh.c 程式碼中執行。 必須在 PCI 裝置掃描可以繼續之前啟用 EEH。 如果未啟用 EEH,則當前的 Power5 硬體將無法工作; 雖然較舊的 Power4 可以在停用它的情況下執行。 實際上,EEH 不再可以關閉。 PCI 裝置必須向 EEH 程式碼註冊; EEH 程式碼需要了解 PCI 裝置的 I/O 地址範圍,以便檢測錯誤。 給定任意地址,例程 pci_get_device_by_addr() 將找到與該地址關聯的 pci 裝置(如果有)。

預設的 arch/powerpc/include/asm/io.h 宏 readb()、inb()、insb() 等包含一個檢查,以檢視 i/o 讀取是否返回 all-0xff。 如果是這樣,這些將呼叫 eeh_dn_check_failure(),後者會反過來詢問韌體 all-ff's 值是否是真正的 EEH 錯誤的標誌。 如果不是,則處理照常繼續。 這些誤報或“假陽性”的總數可以在 /proc/ppc64/eeh 中看到(可能會發生變化)。 通常,幾乎所有這些都發生在引導期間,掃描 PCI 匯流排時,其中大量的 0xff 讀取是匯流排掃描過程的一部分。

如果檢測到凍結的插槽,則 arch/powerpc/platforms/pseries/eeh.c 中的程式碼會將堆疊跟蹤列印到 syslog (/var/log/messages)。 事實證明,此堆疊跟蹤對於裝置驅動程式作者來說非常有用,可以找出檢測到 EEH 錯誤的點,因為錯誤本身通常會稍微提前發生。

接下來,它使用 Linux 核心通知程式鏈/工作佇列機制,以允許任何感興趣的各方找出失敗。 裝置驅動程式或核心的其他部分可以使用 eeh_register_notifier(struct notifier_block *) 來找出有關 EEH 事件的資訊。 該事件將包括指向 pci 裝置、裝置節點和一些狀態資訊的指標。 事件的接收者可以“隨心所欲”; 預設處理程式將在本節中進一步描述。

為了協助裝置的恢復,eeh.c 匯出以下函式

rtas_set_slot_reset()

斷言 PCI #RST 線 1/8 秒

rtas_configure_bridge()

請求韌體配置拓撲上位於 pci 插槽下的任何 PCI 橋。

eeh_save_bars() 和 eeh_restore_bars()

儲存和恢復裝置及其下方任何裝置的 PCI 配置空間資訊。

EEH notifier_block 事件的處理程式在 drivers/pci/hotplug/pSeries_pci.c 中實現,稱為 handle_eeh_events()。 它儲存裝置的 BAR,然後呼叫 rpaphp_unconfig_pci_adapter()。 最後一次呼叫會導致卡的裝置驅動程式停止,這會導致 uevent 轉到使用者空間。 這會觸發使用者空間指令碼,該指令碼可能會發出諸如乙太網卡的“ifdown eth0”之類的命令,依此類推。 然後,此處理程式休眠 5 秒鐘,希望能給使用者空間指令碼足夠的時間來完成。 然後,它會重置 PCI 卡,重新配置裝置 BAR 和任何下方的橋。 然後,它會呼叫 rpaphp_enable_pci_slot(),這會重新啟動裝置驅動程式並觸發更多的使用者空間事件(例如,為乙太網卡呼叫“ifup eth0”)。

裝置關閉和使用者空間事件

本節記錄了取消配置 pci 插槽時發生的情況,重點介紹了裝置驅動程式如何關閉,以及如何將事件傳遞到使用者空間指令碼。

以下是導致在 EEH 重置的第一階段呼叫裝置驅動程式關閉函式的事件的示例序列。 以下序列是 pcnet32 裝置驅動程式的示例

rpa_php_unconfig_pci_adapter (struct slot *)  // in rpaphp_pci.c
{
  calls
  pci_remove_bus_device (struct pci_dev *) // in /drivers/pci/remove.c
  {
    calls
    pci_destroy_dev (struct pci_dev *)
    {
      calls
      device_unregister (&dev->dev) // in /drivers/base/core.c
      {
        calls
        device_del (struct device *)
        {
          calls
          bus_remove_device() // in /drivers/base/bus.c
          {
            calls
            device_release_driver()
            {
              calls
              struct device_driver->remove() which is just
              pci_device_remove()  // in /drivers/pci/pci_driver.c
              {
                calls
                struct pci_driver->remove() which is just
                pcnet32_remove_one() // in /drivers/net/pcnet32.c
                {
                  calls
                  unregister_netdev() // in /net/core/dev.c
                  {
                    calls
                    dev_close()  // in /net/core/dev.c
                    {
                       calls dev->stop();
                       which is just pcnet32_close() // in pcnet32.c
                       {
                         which does what you wanted
                         to stop the device
                       }
                    }
                 }
               which
               frees pcnet32 device driver memory
            }
 }}}}}}

在 drivers/pci/pci_driver.c 中,struct device_driver->remove() 只是 pci_device_remove(),它呼叫 struct pci_driver->remove(),後者是 pcnet32_remove_one(),它呼叫 unregister_netdev()(在 net/core/dev.c 中),它呼叫 dev_close()(在 net/core/dev.c 中),它呼叫 dev->stop(),它是 pcnet32_close(),然後執行相應的關閉。

---

以下是當取消配置 pci 裝置時傳送到使用者空間的事件的類似堆疊跟蹤

rpa_php_unconfig_pci_adapter() {             // in rpaphp_pci.c
  calls
  pci_remove_bus_device (struct pci_dev *) { // in /drivers/pci/remove.c
    calls
    pci_destroy_dev (struct pci_dev *) {
      calls
      device_unregister (&dev->dev) {        // in /drivers/base/core.c
        calls
        device_del(struct device * dev) {    // in /drivers/base/core.c
          calls
          kobject_del() {                    //in /libs/kobject.c
            calls
            kobject_uevent() {               // in /libs/kobject.c
              calls
              kset_uevent() {                // in /lib/kobject.c
                calls
                kset->uevent_ops->uevent()   // which is really just
                a call to
                dev_uevent() {               // in /drivers/base/core.c
                  calls
                  dev->bus->uevent() which is really just a call to
                  pci_uevent () {            // in drivers/pci/hotplug.c
                    which prints device name, etc....
                 }
               }
               then kobject_uevent() sends a netlink uevent to userspace
               --> userspace uevent
               (during early boot, nobody listens to netlink events and
               kobject_uevent() executes uevent_helper[], which runs the
               event process /sbin/hotplug)
           }
         }
         kobject_del() then calls sysfs_remove_dir(), which would
         trigger any user-space daemon that was watching /sysfs,
         and notice the delete event.

當前設計的優缺點

當前的 EEH 軟體恢復設計存在幾個問題,這些問題可能會在未來的修訂中得到解決。 但首先,請注意當前設計的最大優點是不需要對單個裝置驅動程式進行任何更改,因此當前設計具有廣泛的覆蓋範圍。 該設計最大的缺點是它可能會擾亂不需要擾亂的網路守護程式和檔案系統。

  • 一個較小的抱怨是重置網絡卡會導致使用者空間來回的 ifdown/ifup 嗝聲,這可能會擾亂網路守護程式,它們甚至不需要知道 PCI 卡正在重啟。

  • 一個更嚴重的問題是,對於 SCSI 裝置,相同的重置會對已掛載的檔案系統造成嚴重破壞。 指令碼無法在事後解除安裝檔案系統而不重新整理掛起的緩衝區,但這是不可能的,因為 I/O 已經停止。 因此,理想情況下,重置應該發生在塊層或塊層以下,以便不會擾亂檔案系統。

    Reiserfs 不容忍從塊裝置返回的錯誤。 Ext3fs 似乎可以容忍,不斷重試讀取/寫入直到成功。 兩者僅在此場景中進行了輕微測試。

    SCSI 通用子系統已經具有用於執行 SCSI 裝置重置、SCSI 匯流排重置和 SCSI 主機匯流排介面卡 (HBA) 重置的內建程式碼。 如果 SCSI 命令失敗,這些命令會層疊到一個嘗試重置的鏈中。 將 EEH 重置新增到此事件鏈中將非常自然。

  • 如果根裝置發生 SCSI 錯誤,則一切都會丟失,除非系統管理員有先見之明,可以從 ramdisk/tmpfs 執行 /bin、/sbin、/etc、/var 等。

結論

有進步......