PowerPC Linux PCI 总线 EEH 错误恢复机制深度解析

发布时间:2026/9/13 19:03:54
PowerPC Linux PCI 总线 EEH 错误恢复机制深度解析 PowerPC Linux PCI 总线 EEH 错误恢复机制深度解析【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux导读本文基于 Linux 内核源码树中的 Documentation/arch/powerpc/eeh-pci-error-recovery.rst系统讲解 IBM POWER 体系架构pSeries/iSeries以及后续的 PowerNV中EEHEnhanced Error Handling增强错误处理的完整机制从 PCI 总线错误如何被硬件隔离到固件如何抽象屏蔽芯片差异再到内核如何通过热插拔基础设施实现只重启 PCI 卡、不重启操作系统的设备级恢复。读完本文你将掌握 EEH 错误的成因分类、检测与恢复的完整调用链、当前 Linux 实现的源码级工作流程以及该设计在文件系统与网络栈场景下的已知局限。EEH 概述让操作系统免受 PCI 错误伤害IBM 的 POWER 体系pSeries 与 iSeries计算机所搭载的 PCI 总线控制器芯片具备检测并报告大量 PCI 总线错误条件的扩展能力这些特性统称为EEHEnhanced Error Handling。EEH 硬件特性的核心价值在于PCI 总线错误可以被清除、PCI 卡可以被重启而无需同时重启操作系统。这与传统 PCI 错误处理方式形成鲜明对比传统方式一PCI 芯片直接与 CPU 相连一旦出错将触发 CPU 的 machine-check / check-stop 条件导致整个 CPU 停机传统方式二干脆忽略这类错误但这可能导致用户数据或内核数据损坏、适配器挂起无响应甚至系统崩溃或锁死。因此EEH 的设计初衷是让操作系统通过屏蔽 PCI 错误、赋予 OS单独重启/恢复单个 PCI 设备的能力从而变得更加可靠和健壮。正如文档作者IBM 的 Linas Vepstas所指出的未来其他厂商基于 PCI-E 规范的平台也可能包含类似特性——这一预言在当下 PCIe AERAdvanced Error Reporting等机制中得到了印证。EEH 错误的成因分析EEH 最初是为防范硬件故障而设计例如 PCI 卡因过热、潮湿、灰尘、振动以及不良电气连接而失效。但实际生产环境中看到的绝大多数 EEH 错误其根源是成因类别具体描述插卡未插好PCI 卡未正确就位poorly seated是现场故障的最大来源设备驱动 bug驱动代码错误导致非法 DMA 访问设备固件 bug板卡固件逻辑缺陷板卡硬件 bugPCI 卡硬件设计或实现缺陷其中最典型的软件 bug是设备试图DMA 到系统内存中未为该卡预留 DMA 的区域。EEH 对这类问题的防护非常有价值——它阻止了原本可能发生的静默内存损坏silent memory corruption。过去数年间正是借助 EEH 机制发现并修复了大量此类设备驱动 bug。此外EEH 错误的其他可能来源还包括数据线或地址线的奇偶校验错误例如插卡接触不良导致的电气连通性问题PCI-X split-completion 错误由软件、设备固件或设备 PCI 硬件 bug 引发。文档同时给出了一个实用经验绝大多数真正的硬件故障可以通过物理拔出并重新插好 PCI 卡来解决。检测与恢复从隔离到重启的工作流程错误检测全 0xff 读取与固件确认当 PCI Host BridgePHB即连接 PCI 总线与系统 CPU 电子复杂体的总线控制器检测到 PCI 错误条件时它会隔离isolate受影响的 PCI 卡。隔离会产生以下可观测效果阻塞所有写操作无论是系统写往 PCI 卡还是 PCI 卡写往系统使所有读操作返回全 18/16/32 位读分别返回0xff、0xffff、0xffffffff。选择全 1 值的原因很巧妙这与设备从插槽上被物理拔除时读到的值完全相同。该行为覆盖 PCI 内存空间、I/O 空间和 PCI 配置空间唯一例外是中断仍然会继续投递这正是后续恢复流程能够异步进行的前提。固件抽象RTAS 的介入检测与恢复借助 ppc64 固件完成。Linux 内核中与固件交互的编程接口称为RTASRun-Time Abstraction Services运行时抽象服务。内核不应该也不应该尝试直接访问 PCI 芯片组中的 EEH 功能原因在于市面上存在多种不同的芯片组各有不同的接口和怪癖quirks。固件提供了统一的抽象层可适配所有 pSeries/iSeries 硬件并具备向前兼容性。如果操作系统或设备驱动怀疑某个 PCI 插槽已被 EEH 隔离可以通过固件调用来确认。确认后设备驱动应将自己置入一致状态既然无法完成任何挂起的工作启动卡的恢复流程。典型的恢复流程包括重置 PCI 设备将 PCI #RST 线拉高约两秒重新设置设备配置空间——基地址寄存器BAR、延迟定时器latency timer、缓存行大小cache line size、中断线interrupt line等重新初始化设备驱动。在最坏情况下还可以对卡进行断电重启至少对支持热插拔的插槽可行。原则上远高于设备驱动层的软件层无需感知PCI 卡以这种方式被重启过——理想情况下在卡被重置期间Ethernet/磁盘/USB I/O 至多经历一次短暂停顿。如果卡在三到四次重置后仍无法恢复内核/设备驱动应假定最坏情况——卡已彻底死亡并向系统管理员报告该错误。错误信息会通过 RTAS 以及 syslogd/var/log/messages上报用于提醒管理员发生了 PCI 重置。处理彻底失败适配器的正确方式是使用标准的 PCI 热插拔工具移除并更换故障卡。当前 PPC64 Linux EEH 实现解析当前内核中已实现一套通用 EEH 恢复机制其最大优点是单个设备驱动无需任何修改即可获得 EEH 恢复支持。该通用机制搭便车在 PCI 热插拔基础设施之上并通过 userspace/udev 基础设施向上层渗透事件。下面结合当前仓库源码详细拆解这一机制。启动与注册EEH 必须在 PCI 扫描前启用EEH 必须在引导早期、以及 PCI 插槽被热插拔时在 PHB 上完成启用引导早期启用由eeh_init()完成文档记载位于arch/powerpc/platforms/pseries/eeh.c热插拔场景由drivers/pci/hotplug/pSeries_pci.c调用 eeh 代码完成。在当前内核源码树中核心的eeh_init()位于 arch/powerpc/kernel/eeh.c它接收一个struct eeh_ops *ops参数将平台相关的 EEH 操作集注册到通用框架中。EEH必须在 PCI 扫描设备之前启用。文档特别指出当前 Power5 硬件在未启用 EEH 时将无法工作虽然较老的 Power4 可以在禁用状态下运行——因此实际上 EEH 已经无法关闭。所有 PCI 设备必须向 EEH 代码注册EEH 代码需要知道 PCI 设备的 I/O 地址范围才能检测错误。给定任意地址例程pci_get_device_by_addr()可找出与该地址关联的 PCI 设备如果存在。检测路径读宏内嵌的全 0xff 检查默认的 arch/powerpc/include/asm/io.h 宏——readb()、inb()、insb()等——内嵌了一个检查判断 I/O 读是否返回全 0xff。若是则调用eeh_dn_check_failure()后者再向固件询问全 0xff 值是否代表真正的 EEH 错误。如果不是则处理照常继续。这些误报false positives的总数可以在/proc/ppc64/eeh中查看该接口可能随版本变化。文档指出几乎所有误报都发生在引导期间 PCI 总线扫描阶段——总线扫描过程本身就会进行大量 0xff 读取。事件通知notifier chain 与 workqueue 双机制一旦检测到冻结frozen的插槽arch/powerpc/platforms/pseries/eeh.c中的代码会向 syslog/var/log/messages打印一条栈回溯stack trace。这条回溯对设备驱动作者极有价值——因为错误本身通常发生在检测点稍早之前通过回溯可以定位 EEH 错误实际被检测到的位置。随后内核利用notifier chain / workqueue 机制让任何感兴趣的方获知故障。设备驱动或其他内核部件可以通过eeh_register_notifier(struct notifier_block *)订阅 EEH 事件。事件将包含指向 PCI 设备的指针、设备节点以及一些状态信息。事件的接收者可以按需处理默认处理器在下文描述。恢复辅助函数为协助设备恢复EEH 框架导出以下函数函数作用rtas_set_slot_reset()将 PCI #RST 线置位 1/8 秒rtas_configure_bridge()请求固件配置位于 PCI 插槽拓扑之下的任何 PCI 桥eeh_save_bars()/eeh_restore_bars()保存/恢复设备及其下所有设备的 PCI 配置空间信息从源码树看eeh_save_bars()实现在 arch/powerpc/kernel/eeh.c并在 pseriesarch/powerpc/platforms/pseries/eeh_pseries.c与 powernvarch/powerpc/platforms/powernv/eeh-powernv.c两个平台实现中均有调用。在现代实现中设备状态保存/恢复通过eeh_pe_dev_traverse()遍历 PE 上的所有设备并调用eeh_dev_save_state/eeh_dev_restore_state完成见 arch/powerpc/kernel/eeh_driver.c 中的eeh_pe_reset_and_recover()。默认事件处理器热插拔驱动的完整恢复序列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。在 pseries 平台的热插拔驱动中rpaphp_unconfig_pci_adapter()与rpaphp_enable_pci_slot()位于 drivers/pci/hotplug/rpaphp_pci.c。现代实现的事件处理框架当前源码树中恢复主流程集中在 arch/powerpc/kernel/eeh_driver.c 的eeh_handle_normal_event()#L836先通过eeh_pe_report(error_detected(IO frozen), ...)通知各设备驱动进入冻结状态若所有驱动都确认可继续PCI_ERS_RESULT_CAN_RECOVER则依次解冻 MMIOEEH_OPT_THAW_MMIO与 DMAEEH_OPT_THAW_DMA若任何驱动要求重置PCI_ERS_RESULT_NEED_RESET则执行eeh_reset_device()对插槽整体重置随后发送slot_reset与resume通知恢复成功后打印EEH: Recovery successful.失败则进入recover_failed分支。事件本身通过内核工作队列异步投递__eeh_send_failure_event()arch/powerpc/kernel/eeh_event.c可在中断上下文中调用将事件挂入eeh_eventlist链表并用 completion 唤醒守护线程eeh_event_handler()作为名为eehd的内核线程由eeh_event_init()通过kthread_run()启动见 arch/powerpc/kernel/eeh_event.c在普通内核上下文中取出事件并分发处理。这正是文档所述检测可能发生在异常处理器/中断上下文中而恢复处理需要异步地在普通上下文进行这一设计要点的现代实现形态。此外源码中还实现了冻结次数上限保护pe-freeze_count超过eeh_max_freezes后PE 将被永久禁用EEH: ... has failed %d times in the last hour and has been permanently disabled.这与文档中三到四次重置后仍无法恢复则判定卡已死亡的指导原则一脉相承。设备关闭与用户态事件两条调用链详解本节原文档以 pcnet32 网卡驱动为例给出了两条精确的调用链完整还原 PCI 插槽被卸载时内核内部与用户态的事件流动。这两条链至今仍有教学价值。驱动关闭链从 rpaphp 到 pcnet32_closeEEH 重置第一阶段导致设备驱动 close 函数被调用的示例序列以 pcnet32 为例rpa_php_unconfig_pci_adapter (struct slot *) // in rpaphp_pci.c └─ pci_remove_bus_device (struct pci_dev *) // in drivers/pci/remove.c └─ pci_destroy_dev (struct pci_dev *) └─ device_unregister (dev-dev) // in drivers/base/core.c └─ device_del (struct device *) └─ bus_remove_device() // in drivers/base/bus.c └─ device_release_driver() └─ struct device_driver-remove() 即 pci_device_remove() // in drivers/pci/pci_driver.c └─ struct pci_driver-remove() 即 pcnet32_remove_one() // in drivers/net/pcnet32.c └─ unregister_netdev() // in net/core/dev.c └─ dev_close() // in net/core/dev.c └─ dev-stop() 即 pcnet32_close() // in pcnet32.c └─ 执行你想要的设备停止操作简言之在 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()、dev-stop()即pcnet32_close()最终完成适当的关闭动作并释放 pcnet32 驱动内存。用户态事件链从 device_del 到 netlink uevent设备被卸载时发往用户态的事件栈追踪如下rpa_php_unconfig_pci_adapter() // in rpaphp_pci.c └─ pci_remove_bus_device (struct pci_dev *) // in drivers/pci/remove.c └─ pci_destroy_dev (struct pci_dev *) └─ device_unregister (dev-dev) // in drivers/base/core.c └─ device_del (struct device *dev) // in drivers/base/core.c └─ kobject_del() // in libs/kobject.c ├─ kobject_uevent() // in libs/kobject.c │ └─ kset_uevent() // in lib/kobject.c │ └─ kset-uevent_ops-uevent() 即 │ dev_uevent() // in drivers/base/core.c │ └─ dev-bus-uevent() 即 │ pci_uevent() // in drivers/pci/hotplug.c │ 打印设备名称等 │ 然后 kobject_uevent() 向用户态发送 netlink uevent │ -- userspace uevent │ 引导早期无人监听 netlink 事件时 │ kobject_uevent() 执行 uevent_helper[] │ 即运行事件处理进程 /sbin/hotplug └─ kobject_del() 随后调用 sysfs_remove_dir() 触发任何正在监视 /sysfs 的用户态守护进程 注意到删除事件这条链清晰展示了 EEH 恢复如何与 Linux 设备模型及 udev 生态无缝衔接内核事件经 kobject/kset 的 uevent 机制以 netlink 形式投递到用户空间驱动 udev 规则与热插拔脚本执行相应的配置操作如ifdown/ifup。当前设计方案的利弊分析文档作者明确指出当前 EEH 软件恢复设计的最大优点是无需修改任何单个设备驱动从而覆盖面很广throws a wide net最大的负面则是它可能扰动本不需要被扰动的网络守护进程和文件系统。具体包括1. 网络场景的轻微问题重置网卡会导致用户空间连续发生 ifdown/ifup 抖动可能干扰本不需要知道 PCI 卡被重启的网络守护进程。2. 更严重的 SCSI / 文件系统问题同样的重置对 SCSI 设备而言会给已挂载的文件系统带来灾难性影响脚本无法在不冲刷挂起缓冲区的情况下事后卸载文件系统——因为 I/O 已经停止冲刷不可能完成。因此理想情况下重置应该发生在块层block layer或更低层级这样文件系统就不会被打扰。文档记录的经验是Ext3fs 似乎较为宽容会重试读写直到成功但两者在该场景下都只经过了轻度测试。3. 现成的改进路径SCSI 子系统SCSI-generic 子系统已经内置了执行 SCSI 设备重置、SCSI 总线重置和 SCSI 主机总线适配器HBA重置的代码。当某个 SCSI 命令失败时这些重置会被级联成一条尝试重置链且对块层完全隐藏。将 EEH 重置自然加入这条事件链是一个很顺理成章的改进方向。4. 根设备故障的极端情况如果根设备发生 SCSI 错误那么一切都将丢失——除非系统管理员有先见之明把/bin、/sbin、/etc、/var等运行在 ramdisk/tmpfs 上。结论与展望正如原文档标题所言Theres forward progress……——EEH 机制自 2005 年文档撰写以来持续演进。从本文分析的当前源码树可以看到其核心设计理念依然稳固硬件层PHB 检测错误并隔离设备全 0xff 读取作为统一故障信号固件层RTAS 提供跨芯片组的统一抽象内核层eeh_init()启动注册 → 读宏检测 →eehd内核线程异步分发 → notifier/workqueue 通知 → 热插拔框架驱动的设备注销/重置/重配/重注册全流程用户态通过 uevent/netlink 与 udev 协同驱动ifdown/ifup等脚本动作。对于内核开发者与系统管理员而言理解 EEH 的检测与恢复路径是诊断 POWER 平台上 PCI 错误、评估设备驱动兼容性以及设计高可用存储/网络方案的重要基础。相关核心实现可继续深入阅读 arch/powerpc/kernel/eeh.c、arch/powerpc/kernel/eeh_driver.c、arch/powerpc/kernel/eeh_event.c 以及 pseries/powernv 平台实现。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考