在这里插入图片描述

[toc]

Linux 内核崩溃处理核心(kernel/panic.c)全面解析

[kernel/panic.c] [内核 Panic/Oops 终止路径] [在不可恢复错误时完成“停止系统/通知/转储/重启”等收尾流程]

介绍

kernel/panic.c 主要实现内核在致命错误时的统一收尾路径:

  • panic():不可继续运行时进入的终止流程(通常不返回)。
  • 与 Oops/BUG/WARN 的策略联动:例如 “Oops 是否升级为 panic”、是否自动重启、是否触发 kdump 等。
  • 提供 panic notifier、kmsg dump、停止其它 CPU、控制台输出解锁(bust spinlocks)等机制,尽量在系统已不稳定时仍完成关键动作。

历史与背景

诞生解决的问题

  • 内核遇到不可恢复错误时,继续运行可能导致数据破坏扩大;需要统一路径完成:

    • 记录信息(console/printk、dumpers)
    • 通知相关子系统(notifier)
    • 尝试转储(kdump/kmsg_dump/pstore 等)
    • 停止并发扰动(停其它 CPU/禁止中断)
    • 最终动作(停机/重启)

重要演进方向(你读文件时可重点关注的“常见结构”)

  • 从“只打印并死循环”演进到“可配置行为”:

    • panic 超时自动重启(panic_timeout)
    • oops/warn 升级为 panic 的策略(panic_on_oops / panic_on_warn)
    • 与 kexec/kdump 的联动(crash_kexec)
    • 更可控的输出与 dump(panic_print / kmsg_dump 等)

主流应用情况

  • 几乎所有架构与产品都依赖这条路径:服务器依赖 kdump,嵌入式常依赖 watchdog/自动重启与 pstore/ramoops 留痕。

核心原理与设计

核心流程(建议你按源码把每一步打上编号)

通常 panic() 会按以下“阶段化”组织(不同内核版本细节会略有差异):

  1. 进入不可恢复状态
  • 记录当前 CPU/状态,设置全局“已在 panic 中”的标志,避免递归/并发重复进入。
  • 关闭抢占/中断或进入更严格的上下文控制,尽量减少系统继续扰动。
  1. 保证关键输出尽可能可用
  • 触发类似 bust_spinlocks 的机制:当系统可能死在 console/锁上时,尝试“放宽锁保护”以便把 panic 信息打出去(代价是输出时序/一致性不再严格)。
  • 输出 panic 信息、调用栈、寄存器摘要等(其中部分信息来自其它文件,但 panic.c 负责组织终止阶段的打印策略)。
  1. 通知链(panic notifier)
  • 调用 panic notifier 链,让注册者做最后动作(例如:记录额外日志、切换硬件状态、触发特定 dump、点灯/蜂鸣等)。
  • 这一步常被产品化用来做“最后的板级动作”。
  1. dump / 转储
  • 触发 kmsg_dump(把日志交给不同 dumper,如 pstore/ramoops 等实现方)。
  • 如配置了 kdump:调用 crash_kexec 进入 crash kernel 做内存转储。
  1. 停止并发(尤其 SMP)
  • 尝试停止其它 CPU,避免其它核继续写内存/IO,降低 dump 过程中数据继续变化。
  1. 最终处置
  • 如果设置了 panic_timeout>0:等待超时后重启(或走架构相关重启路径)。
  • 否则通常进入死循环/停机等待外部看门狗复位(取决于配置与平台)。

主要优势

  • 统一终止语义:所有致命路径最终汇聚到明确的收尾逻辑。
  • 可配置策略:是否重启、是否升级 oops/warn、是否触发 kdump 等可由参数控制。
  • 可扩展:panic notifier 与 kmsg_dump 让平台/产品能在最后阶段插入必要动作。

局限性与不适用点

  • panic 环境不可信:锁、内存分配、调度、IO 都可能不可靠;因此很多代码必须避免睡眠与复杂依赖。
  • 输出不保证完整:console 可能卡死或丢日志;bust spinlocks 只是降低“完全打不出”的概率。
  • 并发与递归风险:NMI、双重故障、再次触发 panic 时,很多路径只能“尽力而为”。

使用场景

首选场景(它在系统中承担的角色)

  • 内核检测到不可恢复错误(内存破坏、严重 BUG、关键子系统不可继续)。
  • 产品希望:快速复位 + 留存最小诊断信息(串口日志/pstore/核心转储)。

不推荐“依赖 panic 解决”的场景

  • 把 panic 当作“正常错误处理机制”是不合适的:panic 是终止流程,不是恢复机制。
  • 如果你只是想在异常时收集信息但继续运行,应考虑更上层的容错(比如局部重启、隔离、降级),而不是依赖 panic。

对比分析

panic vs oops vs BUG vs WARN(从行为与代价角度)

  • WARN:提示性告警,通常继续运行;可配置升级为 panic(用于把“疑似内存破坏前兆”变成快速复位策略)。
  • BUG:通常意味着逻辑不可接受,常导致 oops 或直接触发更严重路径。
  • Oops:发生严重异常但内核可能尝试继续(风险是继续运行导致二次破坏);可配置 panic_on_oops 直接升级为 panic。
  • panic:明确终止系统运行;更倾向“止损 + 诊断 + 重启/停机”。

对嵌入式产品常见策略:

  • 关键设备:panic_on_oops=1 + 合理 panic_timeout + watchdog,保证快速恢复;
  • 研发阶段:保留更长日志与更完整 dump(pstore/ramoops/kdump),降低复现难度。

总结

关键特性

  • panic() 组织“打印/通知/dump/停核/重启”的终止流程。
  • 通过参数控制“是否自动重启、是否把 oops/warn 升级为 panic”等策略。
  • panic notifier 与 kmsg_dump 为平台/产品留出最后阶段扩展点。

vpanic / panic:内核进入不可恢复致命路径时的统一收敛点(输出诊断、调用 panic 通知链、可选 kdump/重启/最终停机)


vpanic:核心致命路径(禁止中断/抢占、打印、通知链、dump、可选重启、最终自旋/闪烁)

  1. 不可睡眠上下文收敛:先关本地中断与抢占,避免在 panic 期间被调度打断或被中断处理再次触发 panic。
  2. 避免递归放大panic_on_warn 在当前执行流上被清零,用于防止 panic 路径中再次触发 WARN 导致递归进入 panic。
  3. 最小依赖打印:使用静态缓冲区 buf[1024],避免 panic 阶段依赖动态内存分配。
  4. panic 通知链:通过 atomic_notifier_call_chain(&panic_notifier_list, ...) 调用已注册的“原子回调”,让关键子系统补充信息或切换到更安全状态(例如你前面看到的 heartbeat 触发器置位停止标志)。
  5. 不依赖常规定时器的延迟/闪烁:panic 后常规定时器与调度时序不可信,因此重启前等待使用 mdelay() 的忙等循环,并通过 panic_blink() 进行LED闪烁提示。
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/*
* Stop ourself in panic -- architecture code may override this
*/
void __weak __noreturn panic_smp_self_stop(void)
{
while (1)
cpu_relax();
}


/**
* @brief vpanic:进入不可恢复致命路径并收敛系统状态(不返回)。
* @param fmt 格式化字符串。
* @param args 可变参数列表。
*
* 该函数面向“系统状态不可信”的阶段:优先保证不再睡眠/不再被调度打断,
* 尽可能输出诊断信息,执行 panic 通知链与必要的 dump,最后重启或永久停机。
*/
void vpanic(const char *fmt, va_list args)
{
static char buf[1024]; /**< 静态缓冲区:避免 panic 阶段依赖 kmalloc 等动态分配。 */
long i, i_next = 0, len;
int state = 0;
bool _crash_kexec_post_notifiers = crash_kexec_post_notifiers; /**< 控制 kdump 在通知链之前/之后执行的策略快照。 */

if (panic_on_warn) {
/**
* @note 防递归技巧:panic 路径中可能再次触发 WARN。
* 清零 panic_on_warn 可以避免当前执行流在 panic 过程中再次因 WARN 进入 panic。
*/
panic_on_warn = 0;
}

/**
* @note 关键约束:panic 阶段禁止本地中断与抢占,降低再入与死锁风险。
* 在 ARMv7-M 上通常对应屏蔽可屏蔽中断与禁止内核抢占。
*/
local_irq_disable();
preempt_disable_notrace();

/**
* @note 进入资格控制:只允许一个执行流进入 panic 核心区。
* 单核配置下主要用于防止“递归 panic/并发再入”,而不是解决多核并行。
*/
if (panic_try_start()) {
/* 允许继续执行 */
} else if (panic_on_other_cpu())
panic_smp_self_stop(); /**< 多核时用于自停;单核构建通常退化或不启用该路径。 */

console_verbose();
bust_spinlocks(1); /**< 解除可能阻塞控制台输出的锁/状态,优先保证 panic 信息可见。 */
len = vscnprintf(buf, sizeof(buf), fmt, args); /**< 写入静态缓冲区,避免溢出。 */

if (len && buf[len - 1] == '\n')
buf[len - 1] = '\0';

pr_emerg("Kernel panic - not syncing: %s\n", buf);

/**
* @note 防止重复堆栈转储:若已经处于 oops/panic 相关流程,避免再次 dump_stack 造成噪声与风险。
*/
if (test_taint(TAINT_DIE) || oops_in_progress > 1) {
panic_this_cpu_backtrace_printed = true;
} else if (IS_ENABLED(CONFIG_DEBUG_BUGVERBOSE)) {
dump_stack();
panic_this_cpu_backtrace_printed = true;
}

kgdb_panic(buf); /**< 若启用 KGDB,panic 前给调试入口一个机会;多数嵌入式裁剪配置会编译掉。 */

/**
* @note kdump 执行时机控制:
* - 若配置为“通知链之前执行”,此处直接进入 crash kexec。
* - 否则先走通知链与 dump,后续再进入 crash kexec。
*/
if (!_crash_kexec_post_notifiers)
__crash_kexec(NULL);

panic_other_cpus_shutdown(_crash_kexec_post_notifiers); /**< 多核停其他 CPU;单核配置通常为空或简化。 */

printk_legacy_allow_panic_sync();

/**
* @brief 调用 panic 原子通知链。
* @note 这是 panic_notifier_list 的实际触发点:回调应避免睡眠与复杂依赖。
*/
atomic_notifier_call_chain(&panic_notifier_list, 0, buf);

sys_info(panic_print);
kmsg_dump_desc(KMSG_DUMP_PANIC, buf);

if (_crash_kexec_post_notifiers)
__crash_kexec(NULL);

console_unblank();

/**
* @note 控制台缓冲刷新:panic 可能打断正常的 printk/console 提交流程,
* 这里尽量强制把重要信息刷出。
*/
debug_locks_off();
console_flush_on_panic(CONSOLE_FLUSH_PENDING);

if ((panic_print & SYS_INFO_PANIC_CONSOLE_REPLAY) ||
panic_console_replay)
console_flush_on_panic(CONSOLE_REPLAY_ALL);

if (!panic_blink)
panic_blink = no_blink; /**< 若平台未提供 blink 回调,用空实现保证流程可继续。 */

if (panic_timeout > 0) {
/**
* @note panic 后延时不使用常规定时器:
* panic 阶段定时器子系统可能不可信,因此用 mdelay 忙等。
*/
pr_emerg("Rebooting in %d seconds..\n", panic_timeout);

for (i = 0; i < panic_timeout * 1000; i += PANIC_TIMER_STEP) {
touch_nmi_watchdog();
if (i >= i_next) {
i += panic_blink(state ^= 1); /**< 通过平台 blink 回调闪烁(常用于 LED)。 */
i_next = i + 3600 / PANIC_BLINK_SPD;
}
mdelay(PANIC_TIMER_STEP);
}
}

if (panic_timeout != 0) {
/**
* @note 非干净重启:panic 状态下无法保证完整关机序列,
* 这里只尝试尽可能触发紧急重启。
*/
if (panic_reboot_mode != REBOOT_UNDEFINED)
reboot_mode = panic_reboot_mode;
emergency_restart();
}

#if defined(CONFIG_S390)
disabled_wait();
#endif

pr_emerg("---[ end Kernel panic - not syncing: %s ]---\n", buf);

suppress_printk = 1; /**< 抑制后续滚屏:优先保留已输出的重要 panic 信息可读性。 */

console_flush_on_panic(CONSOLE_FLUSH_PENDING);
nbcon_atomic_flush_unsafe(); /**< 强制刷新 nbcon 原子通道,适配某些延迟打印场景。 */

local_irq_enable(); /**< 进入最终循环前打开中断(实现细节依赖体系结构与配置)。 */

/**
* @brief 最终停机循环:周期性喂 watchdog 并按 blink 策略闪烁。
* @note 该循环不返回,作为 panic 的最终收敛状态。
*/
for (i = 0; ; i += PANIC_TIMER_STEP) {
touch_softlockup_watchdog();
if (i >= i_next) {
i += panic_blink(state ^= 1);
i_next = i + 3600 / PANIC_BLINK_SPD;
}
mdelay(PANIC_TIMER_STEP);
}
}
EXPORT_SYMBOL(vpanic);

panic:可变参数包装,转入 vpanic

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @brief panic:可变参数版本,最终进入 vpanic(不返回)。
* @param fmt 格式化字符串。
*/
void panic(const char *fmt, ...)
{
va_list args;

va_start(args, fmt);
vpanic(fmt, args);
va_end(args);
}
EXPORT_SYMBOL(panic);

panic_try_start / panic_reset / panic_in_progress / panic_on_this_cpu / panic_on_other_cpu:panic 进入资格与并发态判定

panic_cpu:panic 状态的全局发布点

1
2
3
4
5
6
7
8
/**
* @brief 记录当前正在执行 panic 路径的 CPU 编号;若为 PANIC_CPU_INVALID 表示未进入 panic。
*
* 设计要点:
* - 在 SMP 下用于选举“panic 主 CPU”,避免多个 CPU 并行进入 panic/崩溃转储路径互相干扰。
* - 在单核下主要用于发布“panic 已开始”的全局状态,供其他分支快速判定。
*/
atomic_t panic_cpu = ATOMIC_INIT(PANIC_CPU_INVALID);

panic_try_start:尝试成为“panic 主执行 CPU”

作用与实现原理

该函数使用 atomic_try_cmpxchg()panic_cpuPANIC_CPU_INVALID 原子地改写为当前 CPU 号。

  • 成功:表示当前执行流成为“第一个宣布 panic 的 CPU”。
  • 失败:表示此前已有 CPU(或同一 CPU 的某个更早路径,例如 NMI/异常路径)宣布进入 panic。

关键技巧是:用 CAS(比较并交换) 实现一次性“资格获取”,从而让 panic 与 crash_kexec 之类路径共享同一互斥条件,避免并行执行导致的相互停顿或资源争用。

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
/**
* @brief 尝试启动 panic 关键区(获取 panic 主执行资格)。
*
* @return true 表示成功把 panic_cpu 从无效值更新为当前 CPU 编号;
* false 表示 panic 已由其他 CPU 或更早路径宣布开始。
*/
bool panic_try_start(void)
{
int old_cpu, this_cpu;

/**
* 使用同一个 panic_cpu 仲裁 panic 与 crash_kexec 等崩溃相关路径,
* 在 SMP 下避免并行进入造成互相停止或资源竞争。
*/
old_cpu = PANIC_CPU_INVALID; /**< 期望的旧值:仅当尚未进入 panic 才允许写入。 */
this_cpu = raw_smp_processor_id(); /**< 获取当前 CPU 编号;在单核配置通常恒为 0。 */

/**
* @note atomic_try_cmpxchg 语义:
* - 若 *panic_cpu == old_cpu,则写入 this_cpu 并返回 true;
* - 否则不写入并返回 false,同时会把 old_cpu 更新为当时读到的值(便于调试/分支判断)。
*/
return atomic_try_cmpxchg(&panic_cpu, &old_cpu, this_cpu);
}
EXPORT_SYMBOL(panic_try_start);

panic_reset:清除 panic 状态(恢复为“未进入 panic”)

作用与实现原理

panic_cpu 重置为无效值,用于某些场景下的测试/恢复流程或特殊控制路径。其本质是一次原子写入发布。

1
2
3
4
5
6
7
8
/**
* @brief 重置 panic 状态,使 panic_cpu 回到 PANIC_CPU_INVALID。
*/
void panic_reset(void)
{
atomic_set(&panic_cpu, PANIC_CPU_INVALID);
}
EXPORT_SYMBOL(panic_reset);

panic_in_progress:判定系统是否处于 panic 状态

作用与实现原理

读取 panic_cpu 是否仍为无效值,从而获得“panic 是否已开始”的全局判定。这里用 unlikely() 仅是性能提示(不改变语义)。

1
2
3
4
5
6
7
8
9
10
/**
* @brief 判断是否已进入 panic(任意 CPU 宣布过 panic)。
*
* @return true 表示 panic_cpu 已被设置为某个有效 CPU 编号。
*/
bool panic_in_progress(void)
{
return unlikely(atomic_read(&panic_cpu) != PANIC_CPU_INVALID);
}
EXPORT_SYMBOL(panic_in_progress);

panic_on_this_cpu:判定“panic 是否发生在当前 CPU”

作用与实现原理

该函数判断 panic_cpu 是否等于当前 CPU 号。其关键前提是:一旦 panic_cpu 被设置,任务迁移在逻辑上不再成立(panic 路径通常会很快禁止抢占/中断并停止其他 CPU),因此可以使用 raw_smp_processor_id() 做判定,而不要求常规的迁移安全约束。

单核 上它退化为:panic_cpu == 0 时为 true(只要进入 panic 基本就成立)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @brief 判断当前 CPU 是否为宣布/执行 panic 的 CPU。
*
* @return true 表示 panic_cpu 与当前 CPU 编号一致。
*/
bool panic_on_this_cpu(void)
{
/**
* 依赖的关键假设:
* - panic_cpu 一旦被设置,后续不会发生“迁移到/迁移离 panic_cpu”的正常调度语义;
* - 因此可直接使用 raw_smp_processor_id() 做一致性判断。
*/
return unlikely(atomic_read(&panic_cpu) == raw_smp_processor_id());
}
EXPORT_SYMBOL(panic_on_this_cpu);

panic_on_other_cpu:判定“panic 是否发生在远端 CPU”

作用与实现原理

1
2
3
4
5
6
7
8
9
10
/**
* @brief 判断 panic 是否发生在其他 CPU(远端 CPU)。
*
* @return true 表示已进入 panic 且当前 CPU 不是 panic 主 CPU。
*/
bool panic_on_other_cpu(void)
{
return (panic_in_progress() && !panic_on_this_cpu());
}
EXPORT_SYMBOL(panic_on_other_cpu);