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 错误的侵害,并使操作系统能够“重启”/恢复单个 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 卡。隔离将阻止所有写入(从系统写入到卡,或从卡写入到系统),并且它将导致所有读取返回全 ff (0xff, 0xffff, 0xffffffff 用于 8/16/32 位读取)。选择此值是因为它与设备从插槽中物理拔出时获得的值相同。这包括访问 PCI 内存、I/O 空间和 PCI 配置空间。但是,中断将继续传递。
检测和恢复是在 ppc64 固件的帮助下执行的。Linux 内核中与固件的编程接口称为 RTAS(运行时抽象服务)。Linux 内核不(应该不)直接访问 PCI 芯片组中的 EEH 功能,主要是因为那里有许多不同的芯片组,每个芯片组都有不同的接口和怪癖。固件提供了一个统一的抽象层,可与所有 pSeries 和 iSeries 硬件一起工作(并且向前兼容)。
如果操作系统或设备驱动程序怀疑 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 读取是否返回全 0xff。如果是,则它们调用 eeh_dn_check_failure(),后者会询问固件全 ff 值是否是真正的 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 配置空间信息。
在 drivers/pci/hotplug/pSeries_pci.c 中实现了一个针对 EEH notifier_block 事件的处理程序,名为 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 错误,除非系统管理员事先有先见之明地将 /bin、/sbin、/etc、/var 等从 ramdisk/tmpfs 中运行,否则一切都将丢失。
结论¶
正在取得进展...