Linux do_sea流程

do_sea 是 Linux 内核中处理 Synchronous External Abort (SEA) 的关键函数,主要针对 ARM64 (AArch64) 架构。SEA 通常由访问存在问题的物理内存(如由 ECC 内存检测到的不可纠正错误、损坏的存储设备、PCIe 设备映射问题等)引起。

以下是 do_sea 的主要处理流程(基于较新内核版本,如 5.10+):

  1. 进入异常处理:

    • 当 CPU 执行一条指令并遇到导致 Synchronous External Abort (SEA) 的严重内存错误时,硬件会自动:
      • 保存现场(PC、PSTATE 等到异常栈)。
      • 切换到异常级别 (EL1)。
      • 跳转到内核预定义的异常向量表入口点 (entry.S)。
    • 汇编异常入口代码 (entry.S) 进行更详细的上下文保存(通用寄存器、SP 等),并设置 C 语言执行环境。
    • 汇编代码最终调用到 C 语言的中断/异常分发函数。
  2. 识别 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)。
  3. 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 = SIGBUSinfo.si_code = BUS_MCEERR_AR (Action Required)。
            • 如果 memory_failure() 失败 (返回负的错误码,如 -EFAULT 表示地址无效,-ENOMEM 等),或者内核配置不支持 CONFIG_MEMORY_FAILURE,则 do_sea 无法通过此机制处理错误。它会退回到更通用的处理:
              • 设置 info.si_signo = SIGBUSinfo.si_code = BUS_OBJERR (更一般的对象错误)。
    • 发送信号给用户进程:
      • 对于发生在用户空间的错误(无论 memory_failure 是否成功),do_sea 最终会调用 arm64_notify_die
      • arm64_notify_die 进一步调用 force_sig_info 或类似函数。
      • 将构造好的 siginfo_t info 发送给触发该异常的当前用户进程
      • 信号通常是 SIGBUS,携带的 si_code 指示具体错误类型 (BUS_MCEERR_ARBUS_OBJERR)。
    • 返回:
      • 函数返回到异常处理的汇编代码。
      • 汇编代码恢复之前保存的上下文(寄存器)。
      • 执行 eret 指令,返回到发生异常的指令处(或用户空间)。

总结流程图 (简化版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
+--------------------------------------------------+
| CPU 执行指令访问故障物理内存 |
| -> 触发 Synchronous External Abort (SEA) |
| -> 硬件保存现场,跳转到异常向量表入口 (entry.S) |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| entry.S: 保存完整上下文,调用 C 异常分发器 |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| 异常分发器: 检查 ESR.EC |
| -> 若 ESR.EC 指示 SEA (0x10/0x11) -> 调用 do_sea|
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| do_sea (arch/arm64/mm/fault.c): |
| 1. 读取 ESR, FAR |
| 2. 检查 ESR.AET: |
| +----------------------------------------+ |
| | AET == UC (Uncontainable): | |
| | -> arm64_serror_panic() -> Kernel Panic|
| +----------------------------------------+ |
| | AET == CE/UEO & 发生在内核空间: | |
| | -> 通常也 Panic (保守) | |
| +----------------------------------------+ |
| | 发生在用户空间: | |
| | a. 尝试调用 memory_failure(pfn, flags)| |
| | - 成功: si_signo=SIGBUS, si_code=BUS_MCEERR_AR|
| | - 失败/不支持: si_signo=SIGBUS, si_code=BUS_OBJERR|
| | b. 调用 arm64_notify_die() | |
| | c. force_sig_info(SIGBUS) 发送给进程 | |
| 3. 返回 |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| arm64_notify_die / force_sig_info: |
| -> 向用户进程递送 SIGBUS 信号 |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| entry.S: 恢复上下文 -> eret 返回用户空间 |
| -> 用户进程收到 SIGBUS,默认崩溃 (core dump) |
+--------------------------------------------------+

关键点与注意事项:

  1. 严重性: SEA 是同步错误,发生在执行特定指令时。与异步错误(如 SError/异步中止)相比,更容易定位根源(far 寄存器给出地址),但也可能直接导致进程崩溃甚至系统 Panic。
  2. memory_failure() 是核心: 对于用户空间错误,memory_failure() 是 Linux 处理可恢复/可报告的硬件内存错误(尤其是 DRAM ECC UE)的标准机制。它通过隔离坏页和发送 SIGBUS 来防止进程进一步访问损坏数据。
  3. 用户空间处理: 用户进程可以捕获 SIGBUS 信号(特别是 BUS_MCEERR_AR)进行自定义处理(如清理状态、优雅退出、重启特定任务)。但通常进程会终止。
  4. CONFIG_MEMORY_FAILURE: 此内核配置选项必须启用 (y),do_sea 才能使用 memory_failure() 机制处理用户空间的 SEA。否则,所有用户空间 SEA 都只能退化成发送 SIGBUS/BUS_OBJERR
  5. 区分 SError/异步中止: do_sea 只处理同步外部中止 (SEA)。异步外部中止 (SError) 有独立的处理路径(如 do_serror),通常更难调试。
  6. 物理地址: do_seamemory_failure() 操作的是物理地址/页帧号 (PFN)。需要将 FAR 中的虚拟地址转换为物理地址 (fault_addr_to_pfn)。
  7. 保守策略: 内核处理硬件错误通常比较保守。即使硬件报告错误可恢复 (AET=CE/UEO),发生在内核空间或 memory_failure() 失败的场景,内核仍可能选择 Panic 以确保系统状态安全。

理解 do_sea 流程对于诊断 ARM64 服务器上因内存故障、PCIe 设备问题等引起的系统 Panic (Unhandled fault) 或进程崩溃 (SIGBUS) 至关重要。结合内核日志中的 ESRFAR 值以及可能的 Call Trace 是分析这类问题的起点。

Linux内核中do_sea是否可以调用rmap_walk接口

在 Linux 内核中,do_sea 可以间接调用 rmap_walk 接口,但这并非由 do_sea 直接调用,而是通过其核心处理函数 memory_failure() 实现的。以下是详细分析:


关键流程与调用关系

  1. do_sea 的职责

    • 处理 ARM64 的 Synchronous External Abort (SEA)。
    • 区分错误严重性(Uncontainable vs. Recoverable)。
    • 若错误发生在用户空间,调用 memory_failure() 处理可恢复错误。
  2. memory_failure() 的核心操作

    • 标记物理页为 HWPoison(通过 SetPageHWPoison(page))。
    • 解除所有映射到该页的进程页表项(Unmapping),确保后续访问触发 SIGBUS。
    • 通知文件系统/驱动。
  3. rmap_walk 的调用链

    1
    2
    3
    4
    5
    6
    do_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 为 SWAPOUTPOISON 状态,使后续访问触发缺页异常或 SIGBUS。

约束与注意事项

  1. 中断上下文安全性

    • do_sea 在中断上下文中执行,不能睡眠或调用可能阻塞的函数。
    • rmap_walk 本身是非阻塞的,但某些回调函数(如 try_to_unmap_one())可能涉及锁操作。
    • 内核通过以下方式确保安全:
      • 使用 try_lock 机制(如 trylock_page())避免阻塞。
      • 若锁不可用,则跳过该进程的映射处理(部分隔离),保证中断上下文安全。
  2. CONFIG_MEMORY_FAILURE 依赖

    • 只有启用 CONFIG_MEMORY_FAILURE 时,do_sea 才会调用 memory_failure(),进而触发 rmap_walk
    • 若未启用此配置,do_sea 直接发送 SIGBUS 而不进行页隔离。

代码示例(简化版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// arch/arm64/mm/fault.c
void do_sea(...) {
...
if (user_mode(regs)) {
// 用户空间错误:调用 memory_failure()
if (memory_failure(pfn, flags) == 0)
arm64_notify_die(..., SIGBUS, BUS_MCEERR_AR, ...);
else
arm64_notify_die(..., SIGBUS, BUS_OBJERR, ...);
} else {
// 内核空间错误:触发 Panic
arm64_serror_panic(...);
}
}

// memory_failure() 调用链
int memory_failure(unsigned long pfn, int flags) {
...
__memory_failure(pfn, flags);
}

int __memory_failure(...) {
...
hwpoison_user_mappings(pfn, flags, page);
}

int hwpoison_user_mappings(...) {
...
try_to_unmap(page, TTU_IGNORE_MLOCK | TTU_SYNC);
}

void try_to_unmap(...) {
...
rmap_walk(page, &rwc); // 遍历所有映射此页的进程
}

结论

  • do_sea 通过调用 memory_failure() 间接触发了 rmap_walk 接口。
  • 目的:利用反向映射机制解除所有进程对损坏物理页的映射,实现内存隔离。
  • 安全性:通过非阻塞锁机制(如 trylock_page())确保在中断上下文中安全执行。
  • 依赖:仅当 CONFIG_MEMORY_FAILURE=y 时生效,否则退化到简单 SIGBUS 处理。