7. PCI 错误恢复

作者:

许多 PCI 总线控制器能够检测总线上各种硬件 PCI 错误,例如数据和地址总线上的奇偶校验错误,以及 SERR 和 PERR 错误。一些更先进的芯片组能够处理这些错误;其中包括 PCI-E 芯片组,以及 IBM Power4、Power5 和 Power6 系列 pSeries 机器上的 PCI 主桥。通常采取的措施是断开受影响的设备,停止所有对其的 I/O 操作。断开连接的目的是为了避免系统损坏;例如,停止由于 DMA 传输到“无效”地址而导致的系统内存损坏。通常,还会提供重新连接机制,以便重置受影响的 PCI 设备并使其恢复工作状态。重置阶段需要在受影响的设备驱动程序和 PCI 控制器芯片之间进行协调。本文档描述了一个通用 API,用于通知设备驱动程序总线断开连接,然后执行错误恢复。此 API 目前在 2.6.16 及更高版本的内核中实现。

报告和恢复分几个步骤执行。首先,当 PCI 硬件错误导致总线断开连接时,该事件会尽快报告给所有受影响的设备驱动程序,包括多功能卡上的设备驱动程序的多个实例。这允许设备驱动程序避免在自旋循环中死锁,等待某些 I/O 空间寄存器发生变化,而实际上永远不会发生变化。它还使驱动程序有机会根据需要推迟传入的 I/O。

接下来,恢复分几个阶段进行。大部分复杂性是由于需要处理多功能设备,即与它们关联的多个设备驱动程序。在第一阶段,允许每个驱动程序指示其所需的重置类型,选择是简单地重新启用 I/O 或请求插槽重置。

如果任何驱动程序请求插槽重置,则将执行此操作。

在重置和/或重新启用 I/O 之后,会再次通知所有驱动程序,以便它们可以执行可能需要的任何设备设置/配置。在这些都完成后,会发出最终的“恢复正常操作”事件。

选择基于内核的实现而不是用户空间实现的最大原因是需要处理连接到存储介质的 PCI 设备的总线断开,特别是断开与保存根文件系统的设备的连接。如果根文件系统断开连接,用户空间机制将不得不经过大量的曲折才能完成恢复。几乎所有当前的 Linux 文件系统都不能容忍与其底层块设备的断开连接/重新连接。相比之下,总线错误在设备驱动程序中很容易管理。实际上,大多数设备驱动程序已经处理了非常相似的恢复过程;例如,SCSI 通用层已经提供了处理 SCSI 总线错误和 SCSI 总线重置的重要机制。

7.1. 详细设计

以下设计和实现细节基于 2005 年 4 月 5 日左右与 Ben Herrenschmidt 进行的一系列公开电子邮件讨论。

错误恢复 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 来恢复硬件,或者如果它希望有机会提取一些诊断信息(请参阅下面的 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() 回调。

这是“早期恢复”调用。允许再次进行 I/O,但不允许 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 错误事件是由软件错误引起的,例如,由于编程错误而导致 DMA 到通配地址或虚假的分割事务。有关软件错误原因的实际经验的更多详细信息,请参阅 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. 结束