Linux 内核 PCI 错误恢复机制解析:PCI-ERS API、六步恢复流程与 AER/EEH 平台实现
Linux 内核 PCI 错误恢复机制解析PCI-ERS API、六步恢复流程与 AER/EEH 平台实现【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本篇技术指南以内核官方文档Documentation/PCI/pci-error-recovery.rst为主体完整讲解 Linux PCI 错误恢复子系统PCI-ERS的设计目标、pci_error_handlers回调 API、从错误检测到恢复的六步标准流程并结合当前内核源码中 AER 驱动的恢复主流程drivers/pci/pcie/err.c帮助读者掌握设备驱动如何接入 PCI 总线错误恢复这一实战技能理解error_detected、mmio_enabled、slot_reset、resume各回调的语义与返回值约定看懂多函数卡片的恢复协调策略以及 AER/EEH 平台在任务上下文中驱动整棵设备子树恢复的底层调用链。1. 为什么 PCI 错误恢复必须放在内核里完成PCI 总线控制器能够检测多种硬件错误包括数据总线与地址总线上的奇偶校验错误以及 SERR 和 PERR 信号更先进的 PCIe 芯片组进一步提供了高级错误报告AERPCIe 规范 r7.0 第 6.2 节与下游端口遏制DPCPCIe 规范 r7.0 第 6.2.11 节机制。这类错误的典型处理动作是断开disconnect受影响设备挂起所有发往它的 I/O目标是避免系统被进一步破坏——例如阻止设备向野地址发起 DMA 而损坏系统内存。多数平台还提供重连机制将设备复位并恢复为可工作状态而复位阶段需要设备驱动与 PCI 控制器芯片之间的协同这正是 PCI-ERS API 存在的意义。原文档自 2.6.16 内核起实现给出了选择内核态实现而非用户态实现的最关键理由根文件系统所在的存储设备可能断开。如果持有根文件系统的设备发生总线断开用户态机制需要经过大量复杂的扭曲才能完成恢复而当前几乎所有 Linux 文件系统都不容忍底层块设备被断开/重连。相比之下总线错误在设备驱动层更容易管理——大多数驱动本来就在处理极为相似的恢复流程例如 SCSI 通用层早已提供处理 SCSI 总线错误与总线复位的成熟机制。多函数设备multi-function device天然复杂。一块卡上有多个 PCI function、对应多个驱动实例时谁先复位、谁做全局初始化都需要内核层的统一协调。因此原文档定义的 API 目标是向设备驱动通知总线断开事件随后执行错误恢复。这套 API 以struct pci_driver中的函数指针结构体形式暴露给驱动不提供该结构体的驱动被称为 non-aware无感知此时采取什么恢复步骤由平台自行决定例如 arch/powerpc 会模拟一次 PCI 热插拔 remove/add。2. PCI-ERS API结构体、通道状态与结果码2.1 回调结构体pci_error_handlers当前内核中该结构体定义于 include/linux/pci.h在原文档版本error_detected/mmio_enabled/slot_reset/resume/cor_error_detected五个回调基础上又新增了reset_prepare/reset_done两个用于 PCI 函数级复位的回调/* PCI bus error event callbacks */ struct pci_error_handlers { /* PCI bus error detected on this device */ pci_ers_result_t (*error_detected)(struct pci_dev *dev, pci_channel_state_t error); /* MMIO has been re-enabled, but not DMA */ pci_ers_result_t (*mmio_enabled)(struct pci_dev *dev); /* PCI slot has been reset */ pci_ers_result_t (*slot_reset)(struct pci_dev *dev); /* PCI function reset prepare or completed */ void (*reset_prepare)(struct pci_dev *dev); void (*reset_done)(struct pci_dev *dev); /* Device driver may resume normal operations */ void (*resume)(struct pci_dev *dev); /* Allow device driver to record more details of a correctable error */ void (*cor_error_detected)(struct pci_dev *dev); };实现约束必须遵守驱动不必实现全部回调但只要实现了其中任意一个就必须实现error_detected()未实现的回调对应功能视为不支持。例如未提供mmio_enabled()与resume()即表示该驱动恢复时不需要这两个回调而slot_reset()通常是驱动希望感知的重要回调。此外cor_error_detected()是可选的它在错误严重级别为 correctable可纠正时被调用允许驱动做额外的日志记录可参考 drivers/cxl/pci.c 中的用法。2.2 通道状态pci_channel_state_t通道状态描述CPU 到 PCI 设备之间的连通性源码定义见 include/linux/pci.h与原文档完全一致enum { /* I/O channel is in normal state */ pci_channel_io_normal (__force pci_channel_state_t) 1, /* I/O to channel is blocked */ pci_channel_io_frozen (__force pci_channel_state_t) 2, /* PCI card is dead */ pci_channel_io_perm_failure (__force pci_channel_state_t) 3, };状态含义典型触发场景pci_channel_io_normalI/O 通道处于正常状态非致命错误、未隔离的错误pci_channel_io_frozen发往该通道的 I/O 已被阻塞槽位隔离 / DPC 使子层级失效pci_channel_io_perm_failurePCI 卡已死亡平台无法恢复进入永久失败2.3 结果码enum pci_ers_result定义于 include/linux/pci.h。与原文档相比当前内核在五种结果码之外新增了一个内部值PCI_ERS_RESULT_NO_AER_DRIVER未为驱动注册 AER 能力用于子树中某设备缺少error_detected回调时中止整棵子树的恢复见第 5 节enum pci_ers_result { PCI_ERS_RESULT_NONE, /* no result/none/not supported in device driver */ PCI_ERS_RESULT_CAN_RECOVER, /* Device driver can recover without slot reset */ PCI_ERS_RESULT_NEED_RESET, /* Device driver wants slot to be reset. */ PCI_ERS_RESULT_DISCONNECT, /* Device has completely failed, is unrecoverable */ PCI_ERS_RESULT_RECOVERED, /* Device driver is fully recovered and operational */ PCI_ERS_RESULT_NO_AER_DRIVER,/* No AER capabilities registered for the driver */ };3. 六步恢复流程详解恢复遵循一条固定的状态推进链。原文档将其归纳为 STEP 0STEP 6下面逐步展开每步都注明驱动的职责边界与返回值约定。STEP 0错误事件Error EventPCI 硬件检测到总线错误。平台行为分两类powerpcEEH槽位被隔离——所有 I/O 被阻塞所有读返回0xffffffff所有写被丢弃。支持 DPC 的平台到含故障设备子层级的链路被禁用该子层级内所有设备不可访问。STEP 1通知Notification平台对每一个受影响的驱动实例多函数卡上的每个 function 都算一个实例调用error_detected()。这是驱动的同步点此时设备可能已经不可访问powerpc 上槽位已隔离驱动可能因失败的 I/O 早已察觉错误但本回调才是正式的 quiesce静默点——驱动应在此完成清理、等待挂起的事务定时器之类结束可以拿信号量、可以调度除了不要碰设备。回调返回后驱动也不应发起任何新 I/O。本回调运行在任务上下文。驱动必须返回以下结果码之一PCI_ERS_RESULT_RECOVERED认为设备虽报错但仍可用无需进一步干预PCI_ERS_RESULT_CAN_RECOVER认为仅靠敲打 I/O 可能就能恢复硬件或希望有机会提取诊断信息见 STEP 2 的mmio_enabledPCI_ERS_RESULT_NEED_RESET不经过槽位复位就无法恢复PCI_ERS_RESULT_DISCONNECT完全不想恢复。后续走向由全体驱动的结果码决定段/槽上所有驱动都返回CAN_RECOVER→ 平台重新使能该槽 I/O若平台不隔离槽位则什么都不做进入STEP 2任一驱动返回NEED_RESET→ 直接进入STEP 4槽位复位平台无法恢复该槽 → 进入STEP 6永久失败。原文档还记录了 powerpc 实现的两条重要约束当前 powerpc 实现用一个内核线程通知所有设备因此驱动在此例程中不应调度/睡眠否则会阻塞其他所有设备的通知若驱动在冻结的适配器上继续尝试 I/O读返回0xff、写被丢弃对同一冻结适配器尝试超过EEH_MAX_FAILS次 I/O 后EEH 会认为驱动陷入死循环向日志打印错误此后必须重启系统才能再次使用该设备。STEP 2MMIO 使能MMIO Enabled平台重新启用 MMIO通常不包括 DMA然后对全部受影响驱动调用mmio_enabled()。这是早期恢复回调I/O 被再次允许但 DMA 尚未恢复。该回调不是用来重启设备操作的只用于 peek/poke 设备、提取诊断信息、必要时触发设备本地复位等动作只有当同一子段上所有驱动都同意尝试恢复、且硬件未自动执行链路复位时才会调用此回调若平台不做槽位复位或链路复位就无法重新使能 I/O则直接跳到 STEP 3链路复位或 STEP 4槽位复位。驱动返回码约定PCI_ERS_RESULT_RECOVERED认为设备已完全可用、可以恢复正常工作。注意没有保证驱动一定被放行——同一子段上另一驱动失败可能触发槽位复位PCI_ERS_RESULT_NEED_RESET认为当前状态不可恢复需要槽位复位PCI_ERS_RESULT_DISCONNECT彻底失败即便复位也无法恢复。结果决定下一步全部RECOVERED→ STEP 3 或 STEP 5任一NEED_RESET→ STEP 4。原文档在此处还给出三条跨平台兼容性提示非常值得驱动开发者牢记在支持 AER 的平台上设备在 STEP 1 时可能已经可访问但为了兼容 powerpc EEH 与 s390两者设备都要到 STEP 2 才可访问驱动仍应把访问推迟到 STEP 2支持 DPC 的平台上到故障子层级的链路在 STEP 3 才被重新使能因此子层级设备在 STEP 4 之前不可访问对于Surprise DownPCIe 规范 6.2.7类错误设备即使在 STEP 4 也可能不可访问驱动可通过检查读操作是否返回全 1PCI_POSSIBLE_ERROR()宏来判断可访问性。STEP 3链路复位Link Reset平台复位链路。这是 PCIe 特有步骤仅当检测到可以通过复位链路解决的致命错误时执行。STEP 4槽位复位Slot Reset针对PCI_ERS_RESULT_NEED_RESET返回码平台对请求复位的 PCI 设备执行槽位复位具体手段平台相关复位完成后平台调用驱动的slot_reset()回调。原文档详细描述了 powerpc 的两级槽位复位soft reset默认又称 hot-reset拉低适配器#RST线然后将 PCI BAR 与配置头恢复到系统冷启动 BIOS/固件初始化之后的等价状态。对大多数 PCI 设备soft reset 就足以完成恢复fundamental reset可选仅 PCIe 卡支持设备的状态机、硬件逻辑、端口状态与配置寄存器全部回到默认条件。它专为少数 soft reset 不足以恢复的 PCIe 设备提供驱动需要在 probe 中设置pci_dev结构体里的needs_freset位。原文档给出的示例是 QLogic qla2xxx 驱动/* Set EEH reset type to fundamental if required by hba */ if (IS_QLA24XX(ha) || IS_QLA25XX(ha) || IS_QLA81XX(ha)) (pdev-needs_freset 1;槽位复位阶段的几条硬性要求平台必须把配置空间恢复到刚上电状态而不是最后一次状态。槽位复位后驱动几乎总会走标准设备初始化流程异常的配置空间会导致设备挂死、内核恐慌或静默数据损坏本回调是驱动重新初始化硬件重下固件等的时机此时可以认为卡处于全新状态且功能完整槽位已解冻驱动可完全访问 PCI 配置空间、MMIO 与 DMA中断Legacy、MSI、MSI-X也可用驱动此时不应重启正常 I/O 处理只有当所有驱动都在本回调上报成功时平台才会调用resume()收尾驱动仍可返回严重失败PCI_ERS_RESULT_DISCONNECT如果平台先前执行的是 soft reset它可能再尝试一次 hard reset电源循环并再次调用slot_reset()若设备仍无法恢复平台将上报永久失败设备被视为死亡。配置空间保存/恢复驱动通常需要在复位后调用pci_restore_state()重新初始化设备配置空间寄存器把设备从 D0未初始化带回到 D0活动状态PCIe 规范 5.3.1.1。PCI 核心在枚举完成、初始化完配置空间后就调用pci_save_state()确保后续错误恢复有可用状态在 probe 中修改过配置空间的驱动需要再次调用pci_save_state()记录这些改动。进入系统挂起时PCI 核心会为每个 PCI 设备调用pci_save_state()该状态不仅用于 resume也会用于此后任何一次错误恢复万一挂起时保存的状态不适合错误恢复驱动应在 resume 时重新调用pci_save_state()。多函数卡片协调多功能卡的各驱动实例需要自行约定谁来做一次性/全局性初始化。原文档给出的实例是 Symbios sym53c8xx_2 驱动只在 PCI function 0 上执行设备初始化if (PCI_FUNC(pdev-devfn) 0) sym_reset_scsi_bus(np, 0);STEP 5恢复运行Resume Operations当子段上所有驱动在前述三个回调中均返回PCI_ERS_RESULT_RECOVERED时平台对全部受影响驱动调用resume()。其语义是通知驱动一切就绪可以重启活动该回调不返回结果码。此后若再发生新错误平台会重新启动一轮完整的错误恢复序列。STEP 6永久失败Permanent Failure平台无法恢复设备时会再次调用error_detected()但通道状态为pci_channel_io_perm_failure。此时驱动应当假设最坏情况取消所有挂起的 I/O拒绝所有新 I/O向更高层返回-EIO像系统关机清理一样释放全部内存、退出内核运行路径。平台通常会以某种方式通知系统管理员永久失败若设备支持热插拔运维人员多半需要拔下并更换设备。但注意并非所有失败都是真永久有些源于过热有些源于卡没插紧许多 PCI 错误事件实际上由软件 bug 引起如向野地址发起 DMA、编程错误导致的错误拆分事务。4. 平台实现AER 驱动的恢复主流程上述六步在支持 AER 的 x86/通用 PCIe 平台上由内核 AER 驱动实现。中断入口是 drivers/pci/pcie/aer.c 中的aer_irq()该驱动以aerdrv名义注册中断处理核心恢复逻辑则收敛在 drivers/pci/pcie/err.c 的pcie_do_recovery()L210-L294。从源码结构看主流程与原文档的步骤一一对应pci_ers_result_t pcie_do_recovery(struct pci_dev *dev, pci_channel_state_t state, pci_ers_result_t (*reset_subordinates)(struct pci_dev *pdev)) { ... pci_dbg(bridge, broadcast error_detected message\n); if (state pci_channel_io_frozen) pci_walk_bridge(bridge, report_frozen_detected, status); else pci_walk_bridge(bridge, report_normal_detected, status); if (status PCI_ERS_RESULT_CAN_RECOVER) { status PCI_ERS_RESULT_RECOVERED; pci_walk_bridge(bridge, report_mmio_enabled, status); /* STEP 2 */ } if (status PCI_ERS_RESULT_NEED_RESET || state pci_channel_io_frozen) { if (reset_subordinates(bridge) ! PCI_ERS_RESULT_RECOVERED) goto failed; /* STEP 3/4 */ } if (status PCI_ERS_RESULT_NEED_RESET) { status PCI_ERS_RESULT_RECOVERED; pci_walk_bridge(bridge, report_slot_reset, status); /* STEP 4 */ } if (status ! PCI_ERS_RESULT_RECOVERED) goto failed; pci_walk_bridge(bridge, report_resume, status); /* STEP 5 */ ... failed: pci_walk_bridge(bridge, report_perm_failure_detected, NULL); /* STEP 6 */ ... }几个实现要点值得注意恢复范围由检测者决定若错误由 Root Port、Downstream Port、RCEC 或 RCiEP 检测到恢复运行在该设备自身及其整个下游子树上若是 Endpoint 等其他设备检测到恢复范围是其上游桥下的整段设备。这正是原文档多函数设备/同段子段协调思想在 AER 上的落地。恢复前先唤醒电源状态pci_walk_bridge(bridge, pci_pm_runtime_get_sync, ...)先对子树做 runtime PM 唤醒结束成功或失败路径再pci_pm_runtime_put。结果码合并语义每次广播后通过merge_result()drivers/pci/pcie/err.c聚合各驱动投票。合并规则体现了文档的决策链DISCONNECT与NEED_RESET合并为NEED_RESET断开意图降级为至少尝试复位CAN_RECOVER/RECOVERED类被更严重的结果覆盖。无error_detected回调的端点会中止整棵子树在report_error_detected()drivers/pci/pcie/err.c中若非桥类型设备没有注册error_detected投票为PCI_ERS_RESULT_NO_AER_DRIVER源码注释明确说明会阻止子树中任何设备的后续错误回调并以断开状态退出。这就是原文档non-aware 驱动行为平台相关在 AER 平台上的具体策略——与 powerpc 模拟热插拔的策略不同。每步都向外广播 uevent每次投票后调用pci_uevent_ers(dev, vote)用户态如mcelog/udev 规则可据此感知错误与恢复进展失败路径统一走到report_perm_failure_detected()即以pci_channel_io_perm_failure再调一次error_detected()与原文档 STEP 6 描述完全一致。状态机防非法跃迁pci_dev_set_io_state()拒绝非法状态转换如已frozen后重复frozen并打印 cant recover (state transition %u - %u invalid)辅助判断函数pci_dev_is_disconnected()读dev-error_state pci_channel_io_perm_failure供驱动在回调中判断设备是否已判死。powerpc 平台的 EEH 实现powerpc 平台的具体实现细节在 Documentation/arch/powerpc/eeh-pci-error-recovery.rst 中有专门讨论原文档中引用的 STEP 1/STEP 4 的平台约束单线程通知、EEH_MAX_FAILS熔断、soft/fundamental 复位均出自该实现。从当前内核源码结构看原文档列举的早期示例驱动中drivers/scsi/ipr.c、drivers/scsi/qla2xxx/qla_os.c、drivers/scsi/lpfc/lpfc_els.c、drivers/net/ethernet/intel/e1000e 等仍保留着pci_error_handlers实现可作为驱动接入 PCI-ERS 的现实参考。5. 中断处理与总体策略原文档结论部分给出了两条必须写进驱动设计的中断策略从错误检测到slot_reset()被调用之前不保证段上任何设备的中断投递能正常进行slot_reset()返回之后中断才被期望完全可用。也不保证中断投递一定停止驱动在检测到错误之后收到中断、或在 ISR 内部检测到错误导致无法正常 ack无法清除中断源时应当返回IRQ_NOTHANDLED由平台负责处理——典型手段是在错误处理期间屏蔽该 IRQ 源。平台应该知道哪些中断路由到支持错误管理的槽位并能在错误处理期间临时禁用对应 IRQ 号。这会给共享该中断的其他设备带来一些中断延迟但没有别的办法高端平台本来就不应让许多设备共享中断。总体策略上回调如何被调用是平台策略不具备槽位复位能力的平台可能选择忽略无法恢复的驱动将其断开尽量让同段子段上的其他卡恢复。不过实际场景中一个子段通常只有一个驱动。6. 小结驱动接入 PCI-ERS 的清单结合文档与当前内核源码一个设备驱动接入 PCI 错误恢复的最小实践路径是在struct pci_driver中挂接err_handlerstruct pci_error_handlers见 include/linux/pci.h且必须实现error_detected()其余回调按需实现error_detected()中做 quiesce 清理取消挂起 I/O、停工作队列/定时器按设备受损程度返回RECOVERED/CAN_RECOVER/NEED_RESET/DISCONNECT之一不要假设 AER 平台上设备始终可访问——统一在mmio_enabled()及之后再做配置空间/寄存器访问Surprise Down 场景用PCI_POSSIBLE_ERROR()判断slot_reset()中走标准初始化路径并用pci_restore_state()恢复配置空间probe 中改过配置空间的记得补一次pci_save_state()多函数卡约定由 function 0 执行一次性初始化恢复 I/O 一律推迟到resume()收到pci_channel_io_perm_failure时按设备死亡做-EIO 资源清理异常中断一律IRQ_NOTHANDLED交平台处置不自行清中断源。这套STEP 0 隔离 → STEP 1 通知 → STEP 2 MMIO 使能 → STEP 3/4 链路/槽位复位 → STEP 5 resume → STEP 6 永久失败的流程构成了 Linux 内核对 PCIe AER 与 powerpc EEH 等硬件错误上报机制的统一软件抽象平台负责错误检测与复位动作驱动负责各阶段的自检与状态重建PCI 核心pcie_do_recovery()负责在多设备子树上按投票结果编排整个恢复序列。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考