Linux内核SEA处理流程详解
Linux do_sea流程
do_sea
是 Linux 内核中处理 Synchronous External Abort (SEA) 的关键函数,主要针对 ARM64 (AArch64) 架构。SEA 通常由访问存在问题的物理内存(如由 ECC 内存检测到的不可纠正错误、损坏的存储设备、PCIe 设备映射问题等)引起。
以下是 do_sea
的主要处理流程(基于较新内核版本,如 5.10+):
进入异常处理:
- 当 CPU 执行一条指令并遇到导致 Synchronous External Abort (SEA) 的严重内存错误时,硬件会自动:
- 保存现场(PC、PSTATE 等到异常栈)。
- 切换到异常级别 (EL1)。
- 跳转到内核预定义的异常向量表入口点 (
entry.S
)。
- 汇编异常入口代码 (
entry.S
) 进行更详细的上下文保存(通用寄存器、SP 等),并设置 C 语言执行环境。 - 汇编代码最终调用到 C 语言的中断/异常分发函数。
- 当 CPU 执行一条指令并遇到导致 Synchronous External Abort (SEA) 的严重内存错误时,硬件会自动:
识别 SEA 并路由到
do_sea
:- 异常分发器根据 ESR (Exception Syndrome Register) 中的
EC
(Exception Class) 字段判断异常类型。对于 SEA,EC
通常是0x10
(Data Abort from a lower Exception level) 或0x11
(Data Abort taken without a change in Exception level)。 - 分发器将控制权交给
do_sea
函数 (arch/arm64/mm/fault.c
)。
- 异常分发器根据 ESR (Exception Syndrome Register) 中的
do_sea
函数内部处理 (arch/arm64/mm/fault.c
):- 获取错误信息:
- 读取
ESR_EL1
寄存器 (esr
),解析出FSC
(Fault Status Code) 字段。对于 SEA,FSC
通常是0x10
(Synchronous External Abort on translation table walk) 或0x11
(Synchronous External Abort)。 - 读取触发异常的虚拟地址
far
(Fault Address Register)。 - 获取当前进程的信息 (
current
)。
- 读取
- 区分错误严重性:
- 严重错误 (Uncontainable/Unrecoverable):
- 检查
ESR
中的AET
(Architectural Exceptions Type) 字段。如果AET == ESR_ELx_AET_UC
(Uncontainable),这表示错误极其严重,无法隔离或恢复,可能影响系统完整性。 - 在这种情况下,
do_sea
通常会调用arm64_serror_panic
:- 打印详细的错误信息 (CPU, ESR, FAR, Call Trace)。
- 触发内核 panic,导致系统崩溃。这是为了防止数据损坏或不可预测行为蔓延。
- 检查
- 可恢复错误 (Restartable/Corrected):
- 如果
AET == ESR_ELx_AET_CE
(Corrected) 或ESR_ELx_AET_UEO
(Recoverable state),理论上错误已被硬件纠正或可恢复。但内核通常对 SEA 持保守态度,即使标记为可恢复,也可能将其视为潜在严重错误。
- 如果
- 内核空间错误:
- 如果错误发生在内核模式 (
user_mode(regs)
为 false):- 检查是否是栈溢出等特殊错误。
- 如果错误发生在特权区域或无法安全处理,通常也会进入 panic (
arm64_serror_panic
)。
- 如果错误发生在内核模式 (
- 用户空间错误:
- 如果错误发生在用户模式 (
user_mode(regs)
为 true):- 这是
do_sea
最常见的处理路径。 - 内核尝试将此错误视为一个可传递给用户空间处理的 **Machine Check Exception (MCE)**。这是通过
memory_failure()
机制实现的。 - 调用
memory_failure()
:- 传递参数:物理页帧号 (通过
fault_addr_to_pfn
从 FAR 转换得到) 和flags
(如MF_ACTION_REQUIRED
)。 memory_failure()
的核心任务:- 识别出错的物理内存页。
- 尝试将受影响的页面标记为
poisoned
(在struct page
中设置HWPoison
标志)。 - 解除所有映射到该物理页的进程页表项 (Unmap)。后续对这些页面的访问将触发 SIGBUS。
- 如果出错页是文件页,尝试通知底层文件系统或存储驱动。
- 传递参数:物理页帧号 (通过
- 处理
memory_failure()
返回值:- 如果
memory_failure()
成功处理 (0
或-EHWPOISON
),do_sea
认为错误已“处理”(即用户空间进程将被通知)。它设置info.si_signo = SIGBUS
和info.si_code = BUS_MCEERR_AR
(Action Required)。 - 如果
memory_failure()
失败 (返回负的错误码,如-EFAULT
表示地址无效,-ENOMEM
等),或者内核配置不支持CONFIG_MEMORY_FAILURE
,则do_sea
无法通过此机制处理错误。它会退回到更通用的处理:- 设置
info.si_signo = SIGBUS
和info.si_code = BUS_OBJERR
(更一般的对象错误)。
- 设置
- 如果
- 这是
- 如果错误发生在用户模式 (
- 严重错误 (Uncontainable/Unrecoverable):
- 发送信号给用户进程:
- 对于发生在用户空间的错误(无论
memory_failure
是否成功),do_sea
最终会调用arm64_notify_die
。 arm64_notify_die
进一步调用force_sig_info
或类似函数。- 将构造好的
siginfo_t info
发送给触发该异常的当前用户进程。 - 信号通常是
SIGBUS
,携带的si_code
指示具体错误类型 (BUS_MCEERR_AR
或BUS_OBJERR
)。
- 对于发生在用户空间的错误(无论
- 返回:
- 函数返回到异常处理的汇编代码。
- 汇编代码恢复之前保存的上下文(寄存器)。
- 执行
eret
指令,返回到发生异常的指令处(或用户空间)。
- 获取错误信息:
总结流程图 (简化版):
1 | +--------------------------------------------------+ |
关键点与注意事项:
- 严重性: SEA 是同步错误,发生在执行特定指令时。与异步错误(如 SError/异步中止)相比,更容易定位根源(
far
寄存器给出地址),但也可能直接导致进程崩溃甚至系统 Panic。 memory_failure()
是核心: 对于用户空间错误,memory_failure()
是 Linux 处理可恢复/可报告的硬件内存错误(尤其是 DRAM ECC UE)的标准机制。它通过隔离坏页和发送 SIGBUS 来防止进程进一步访问损坏数据。- 用户空间处理: 用户进程可以捕获
SIGBUS
信号(特别是BUS_MCEERR_AR
)进行自定义处理(如清理状态、优雅退出、重启特定任务)。但通常进程会终止。 CONFIG_MEMORY_FAILURE
: 此内核配置选项必须启用 (y
),do_sea
才能使用memory_failure()
机制处理用户空间的 SEA。否则,所有用户空间 SEA 都只能退化成发送SIGBUS/BUS_OBJERR
。- 区分 SError/异步中止:
do_sea
只处理同步外部中止 (SEA)。异步外部中止 (SError) 有独立的处理路径(如do_serror
),通常更难调试。 - 物理地址:
do_sea
和memory_failure()
操作的是物理地址/页帧号 (PFN)。需要将 FAR 中的虚拟地址转换为物理地址 (fault_addr_to_pfn
)。 - 保守策略: 内核处理硬件错误通常比较保守。即使硬件报告错误可恢复 (
AET=CE/UEO
),发生在内核空间或memory_failure()
失败的场景,内核仍可能选择 Panic 以确保系统状态安全。
理解 do_sea
流程对于诊断 ARM64 服务器上因内存故障、PCIe 设备问题等引起的系统 Panic (Unhandled fault
) 或进程崩溃 (SIGBUS
) 至关重要。结合内核日志中的 ESR
、FAR
值以及可能的 Call Trace
是分析这类问题的起点。
Linux内核中do_sea是否可以调用rmap_walk接口
在 Linux 内核中,do_sea
可以间接调用 rmap_walk
接口,但这并非由 do_sea
直接调用,而是通过其核心处理函数 memory_failure()
实现的。以下是详细分析:
关键流程与调用关系
do_sea
的职责:- 处理 ARM64 的 Synchronous External Abort (SEA)。
- 区分错误严重性(Uncontainable vs. Recoverable)。
- 若错误发生在用户空间,调用
memory_failure()
处理可恢复错误。
memory_failure()
的核心操作:- 标记物理页为
HWPoison
(通过SetPageHWPoison(page)
)。 - 解除所有映射到该页的进程页表项(Unmapping),确保后续访问触发 SIGBUS。
- 通知文件系统/驱动。
- 标记物理页为
rmap_walk
的调用链:1
2
3
4
5
6do_sea()
└→ memory_failure(pfn, flags) // 处理物理页错误
└→ __memory_failure()
└→ hwpoison_user_mappings() // 处理用户空间映射
└→ try_to_unmap() // 解除页表映射
└→ rmap_walk(page, ...) // 遍历所有映射此页的进程
rmap_walk
的作用
- 功能:通过反向映射(Reverse Mapping)机制遍历所有映射了指定物理页的进程页表项(PTE)。
- 在 SEA 上下文中的用途:
- 当
memory_failure()
需要隔离一个损坏的物理页时,需找到所有映射了该页的进程。 - 通过
rmap_walk
遍历每个映射,并修改其 PTE 为SWAPOUT
或POISON
状态,使后续访问触发缺页异常或 SIGBUS。
- 当
约束与注意事项
中断上下文安全性:
do_sea
在中断上下文中执行,不能睡眠或调用可能阻塞的函数。rmap_walk
本身是非阻塞的,但某些回调函数(如try_to_unmap_one()
)可能涉及锁操作。- 内核通过以下方式确保安全:
- 使用
try_lock
机制(如trylock_page()
)避免阻塞。 - 若锁不可用,则跳过该进程的映射处理(部分隔离),保证中断上下文安全。
- 使用
CONFIG_MEMORY_FAILURE
依赖:- 只有启用
CONFIG_MEMORY_FAILURE
时,do_sea
才会调用memory_failure()
,进而触发rmap_walk
。 - 若未启用此配置,
do_sea
直接发送SIGBUS
而不进行页隔离。
- 只有启用
代码示例(简化版)
1 | // arch/arm64/mm/fault.c |
结论
- 是:
do_sea
通过调用memory_failure()
间接触发了rmap_walk
接口。 - 目的:利用反向映射机制解除所有进程对损坏物理页的映射,实现内存隔离。
- 安全性:通过非阻塞锁机制(如
trylock_page()
)确保在中断上下文中安全执行。 - 依赖:仅当
CONFIG_MEMORY_FAILURE=y
时生效,否则退化到简单 SIGBUS 处理。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Helloeuler!