7. PCI 错误恢复¶
- 作者:
Linas Vepstas <linasvepstas@gmail.com>
Richard Lary <rlary@us.ibm.com>
Mike Mason <mmlnx@us.ibm.com>
许多 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.4. 步骤 3:链路重置¶
平台重置链路。 这是一个 PCI-Express 特定的步骤,每当检测到可以通过重置链路来“解决”的致命错误时,都会执行此步骤。
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