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 等。

结论

有进步......