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)中EEH(Enhanced Error Handling,增强错误处理)的完整机制:从 PCI 总线错误如何被硬件隔离,到固件如何抽象屏蔽芯片差异,再到内核如何通过热插拔基础设施实现"只重启 PCI 卡、不重启操作系统"的设备级恢复。读完本文,你将掌握 EEH 错误的成因分类、检测与恢复的完整调用链、当前 Linux 实现的源码级工作流程,以及该设计在文件系统与网络栈场景下的已知局限。
EEH 概述:让操作系统免受 PCI 错误伤害
IBM 的 POWER 体系(pSeries 与 iSeries)计算机所搭载的 PCI 总线控制器芯片,具备检测并报告大量 PCI 总线错误条件的扩展能力,这些特性统称为EEH(Enhanced Error Handling)。EEH 硬件特性的核心价值在于:PCI 总线错误可以被清除、PCI 卡可以被"重启",而无需同时重启操作系统。
这与传统 PCI 错误处理方式形成鲜明对比:
- 传统方式一:PCI 芯片直接与 CPU 相连,一旦出错将触发 CPU 的 machine-check / check-stop 条件,导致整个 CPU 停机;
- 传统方式二:干脆忽略这类错误,但这可能导致用户数据或内核数据损坏、适配器挂起无响应,甚至系统崩溃或锁死。
因此,EEH 的设计初衷是让操作系统通过屏蔽 PCI 错误、赋予 OS"单独重启/恢复单个 PCI 设备"的能力,从而变得更加可靠和健壮。正如文档作者(IBM 的 Linas Vepstas)所指出的,未来其他厂商基于 PCI-E 规范的平台也可能包含类似特性——这一预言在当下 PCIe AER(Advanced Error Reporting)等机制中得到了印证。
EEH 错误的成因分析
EEH 最初是为防范硬件故障而设计,例如 PCI 卡因过热、潮湿、灰尘、振动以及不良电气连接而失效。但实际生产环境中看到的绝大多数 EEH 错误,其根源是:
| 成因类别 | 具体描述 |
|---|---|
| 插卡未插好 | PCI 卡未正确就位(poorly seated)是现场故障的最大来源 |
| 设备驱动 bug | 驱动代码错误导致非法 DMA 访问 |
| 设备固件 bug | 板卡固件逻辑缺陷 |
| 板卡硬件 bug | PCI 卡硬件设计或实现缺陷 |
其中最典型的软件 bug,是设备试图DMA 到系统内存中未为该卡预留 DMA 的区域。EEH 对这类问题的防护非常有价值——它阻止了原本可能发生的静默内存损坏(silent memory corruption)。过去数年间,正是借助 EEH 机制发现并修复了大量此类设备驱动 bug。
此外,EEH 错误的其他可能来源还包括:
- 数据线或地址线的奇偶校验错误(例如插卡接触不良导致的电气连通性问题);
- PCI-X split-completion 错误(由软件、设备固件或设备 PCI 硬件 bug 引发)。
文档同时给出了一个实用经验:绝大多数"真正的硬件故障"可以通过物理拔出并重新插好 PCI 卡来解决。
检测与恢复:从隔离到重启的工作流程
错误检测:全 0xff 读取与固件确认
当 PCI Host Bridge(PHB,即连接 PCI 总线与系统 CPU 电子复杂体的总线控制器)检测到 PCI 错误条件时,它会隔离(isolate)受影响的 PCI 卡。隔离会产生以下可观测效果:
- 阻塞所有写操作(无论是系统写往 PCI 卡,还是 PCI 卡写往系统);
- 使所有读操作返回全 1(8/16/32 位读分别返回
0xff、0xffff、0xffffffff)。
选择全 1 值的原因很巧妙:这与设备从插槽上被物理拔除时读到的值完全相同。该行为覆盖 PCI 内存空间、I/O 空间和 PCI 配置空间;唯一例外是中断仍然会继续投递,这正是后续恢复流程能够异步进行的前提。
固件抽象:RTAS 的介入
检测与恢复借助 ppc64 固件完成。Linux 内核中与固件交互的编程接口称为RTAS(Run-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,并在 pseries(arch/powerpc/platforms/pseries/eeh_pseries.c)与 powernv(arch/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),则依次解冻 MMIO(EEH_OPT_THAW_MMIO)与 DMA(EEH_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_close
EEH 重置第一阶段,导致设备驱动 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 上。
结论与展望
正如原文档标题所言:"There's 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),仅供参考