PCI 总线 EEH 错误恢复

Linas Vepstas <linas@austin.ibm.com>

2005年1月12日

概述:

基于 IBM POWER 的 pSeries 和 iSeries 计算机包含 PCI 总线控制器芯片,这些芯片具有扩展功能,用于检测和报告各种 PCI 总线错误状况。这些功能统称为“EEH”,即“增强型错误处理”(Enhanced Error Handling)。EEH 硬件特性允许清除 PCI 总线错误并“重启”PCI 卡,而无需同时重启操作系统。

这与传统的 PCI 错误处理形成鲜明对比。在传统处理中,PCI 芯片直接硬连线到 CPU,错误会导致 CPU 机器检查/检查停止(machine-check/check-stop)状况,从而完全使 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(对于 8/16/32 位读取,分别为 0xff、0xffff、0xffffffff)。之所以选择这个值,是因为如果你从插槽中物理拔下设备,也会得到相同的值。这包括对 PCI 内存、I/O 空间和 PCI 配置空间的访问。但是,中断将继续传递。

检测和恢复是在 ppc64 固件的协助下进行的。Linux 内核中用于访问固件的编程接口被称为 RTAS(运行时抽象服务,Run-Time Abstraction Services)。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 基础架构将事件向上渗透。以下是实现这一目标的详细说明。

EEH 必须在引导过程的早期在 PHB 中启用,如果 PCI 插槽被热插拔,也必须启用。前者由 arch/powerpc/platforms/pseries/eeh.c 中的 eeh_init() 执行,后者由 drivers/pci/hotplug/pSeries_pci.c 调用 eeh.c 代码来完成。在对设备进行 PCI 扫描之前,必须启用 EEH。当前的 Power5 硬件如果不启用 EEH 将无法工作,尽管较旧的 Power4 在禁用 EEH 的情况下也能运行。实际上,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()。最后这个调用会导致该卡的设备驱动程序停止,从而使 uevents 发送到用户空间。这会触发用户空间脚本,这些脚本可能会针对以太网卡等发出诸如“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 已经停止。因此,理想情况下,重置应该在块层发生或在其下方发生,这样文件系统就不会受到干扰。

    Ext3fs 似乎具有容忍性,会重试读/写操作直到成功。两者在此场景下都只经过了轻量测试。

    SCSI 通用子系统已经内置了用于执行 SCSI 设备重置、SCSI 总线重置和 SCSI 主机总线适配器(HBA)重置的代码。如果 SCSI 命令失败,这些重置会被级联成一连串的重试操作。这些操作对块层完全是隐藏的。将 EEH 重置添加到这一事件链中将是非常自然的事情。

  • 如果根设备发生 SCSI 错误,除非系统管理员有先见之明地将 /bin、/sbin、/etc、/var 等运行在 ramdisk/tmpfs 中,否则一切都将丢失。

结论

正在取得进展……