系列目录 :第一篇:异常机制全景图 | [第二篇:Kernel Panic 与系统重启](#第二篇:Kernel Panic 与系统重启) | [第三篇:Tombstone 机制](#第三篇:Tombstone 机制) | [第四篇:System Server Watchdog](#第四篇:System Server Watchdog) | [第五篇:System Server 崩溃](#第五篇:System Server 崩溃) | [第六篇:ANR 机制](#第六篇:ANR 机制) | [第七篇:Java 层崩溃](#第七篇:Java 层崩溃) | [第八篇:Trace 机制](#第八篇:Trace 机制) | 第九篇:日志系统 | 第十篇:实战方法论
一、为什么需要深入内核异常?
你可能遇到过这些棘手问题:
- 设备突然黑屏重启,logcat 里什么都没有,只有
/proc/last_kmsg留下一句Kernel panic - not syncing - 压力测试时系统随机重启,日志显示
Watchdog detected hard LOCKUP on cpu 2 - 充电时设备重启,内核日志显示是电源管理驱动触发了 panic
- 内核日志显示
BUG: soft lockup - CPU#1 stuck for 23s,但不知道是什么卡住了 CPU
这些问题都发生在 Linux 内核层,是 Android 系统最底层的异常。如果缺乏对内核异常机制的理解,面对 last_kmsg 时只会一头雾水。
本文基于 AOSP 7(Linux 3.18) 源码,深入分析内核层的两大异常机制:Kernel Panic 与内核 Watchdog,以及 panic 后的现场保存与重启流程。
二、Kernel Panic 的本质
Kernel Panic 是 Linux 内核在遇到无法安全继续运行的致命错误时,主动终止系统运行的行为。在 Android 设备上表现为:屏幕卡死 → 系统自动重启(如果 panic_timeout > 0)。
触发 Kernel Panic 的典型场景
| 场景类型 | 具体例子 | 触发路径 |
|---|---|---|
| 硬件故障 | 内存位翻转、CPU 过热、总线错误 | ARM 异常向量 → die() → panic() |
| 内核 BUG | 空指针解引用、自旋锁死锁、栈溢出 | BUG()/BUG_ON() → panic() |
| 驱动异常 | 设备驱动访问非法地址、DMA 错误 | 驱动中 panic() 调用 |
| 文件系统损坏 | 关键文件系统元数据损坏,无法挂载 | mount() 失败 → panic() |
| init 进程死亡 | init 进程(PID 1)意外退出 | kernel/exit.c 中 panic("Attempted to kill init!") |
| 手动触发 | echo c > /proc/sysrq-trigger |
SysRq 触发 panic |
三、panic() 函数源码分析
panic() 位于 kernel/panic.c,是内核 panic 的核心入口。理解其执行顺序对分析 panic 日志至关重要。
源码路径 :kernel/msm-3.18/kernel/panic.c
c
void panic(const char *fmt, ...)
{
static DEFINE_SPINLOCK(panic_lock);
static char buf[1024];
va_list args;
long i, i_next = 0;
int state = 0;
// 1. 禁用本地中断,防止死锁
local_irq_disable();
// 2. 获取 panic 锁,确保只有一个 CPU 执行 panic
if (!spin_trylock(&panic_lock))
panic_smp_self_stop();
console_verbose();
bust_spinlocks(1);
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
// 3. 打印 panic 信息
pr_emerg("Kernel panic - not syncing: %s\n", buf);
// 4. 打印调用栈(如果配置了 CONFIG_DEBUG_BUGVERBOSE)
if (!test_taint(TAINT_DIE) && oops_in_progress <= 1)
dump_stack();
// 5. 停止其他 CPU 核心
smp_send_stop();
// 6. 调用 panic 通知链
atomic_notifier_call_chain(&panic_notifier_list, 0, buf);
// 7. 将内核日志写入 pstore/ramoops
kmsg_dump(KMSG_DUMP_PANIC);
// 8. 根据 panic_timeout 决定是否重启
if (panic_timeout > 0) {
pr_emerg("Rebooting in %d seconds..", panic_timeout);
for (i = 0; i < panic_timeout * 1000; i += PANIC_TIMER_STEP) {
touch_nmi_watchdog();
mdelay(PANIC_TIMER_STEP);
}
}
if (panic_timeout != 0) {
emergency_restart();
}
// 如果 panic_timeout == 0,无限循环等待
for (i = 0; ; i += PANIC_TIMER_STEP) {
touch_softlockup_watchdog();
mdelay(PANIC_TIMER_STEP);
}
}
关键设计 :panic() 的执行顺序是精心设计的------先打印日志(步骤 3-4),再停止其他 CPU(步骤 5)。如果先停 CPU,当前 CPU 可能无法输出日志。
kmsg_dump()在smp_send_stop()之后执行,确保日志能写入 pstore。
panic() 执行流程图
panic(const char *fmt, ...)
│
├─ 1. local_irq_disable() --- 禁用本地中断
├─ 2. spin_trylock(&panic_lock) --- 获取 panic 锁
├─ 3. pr_emerg("Kernel panic...") --- 打印 panic 信息
├─ 4. dump_stack() --- 打印调用栈
├─ 5. smp_send_stop() --- 停止其他 CPU 核心
├─ 6. atomic_notifier_call_chain() --- 调用 panic 通知链
├─ 7. kmsg_dump(KMSG_DUMP_PANIC) --- 日志写入 pstore/ramoops
└─ 8. 根据 panic_timeout 决定行为
├─ > 0: mdelay(panic_timeout*1000) → emergency_restart()
├─ = 0: 无限等待 (调试用)
└─ < 0: 立即重启
关键参数:panic_timeout
panic_timeout 控制 panic 后的行为,通过内核命令行或 sysctl 配置:
源码路径 :kernel/msm-3.18/kernel/panic.c
c
// 默认值来自内核配置 CONFIG_PANIC_TIMEOUT
int panic_timeout = CONFIG_PANIC_TIMEOUT;
EXPORT_SYMBOL_GPL(panic_timeout);
// 内核命令行参数:panic=N
core_param(panic, panic_timeout, int, 0644);
bash
# 内核命令行配置
androidboot.panic_timeout=5 # 5秒后重启
# 运行时修改
echo 5 > /proc/sys/kernel/panic
| 场景 | panic_timeout 值 | 行为 |
|---|---|---|
| 开发阶段 | 0 | 无限等待,方便连接 JTAG 调试 |
| 量产固件 | 5 | 5 秒后自动重启(高通平台默认值) |
| 压力测试 | 1 | 快速重启,收集更多 panic 样本 |
关键设计 :
panic_timeout=0时系统会无限循环在for (i = 0; ; ...)中,不断调用touch_softlockup_watchdog()防止 watchdog 触发,让开发者有时间连接调试器。
smp_send_stop() ------ 停止其他 CPU
当某个 CPU 触发 panic 后,必须立即停止其他所有 CPU,否则它们可能继续修改内存,破坏 crash dump 的准确性。
源码路径 :kernel/msm-3.18/kernel/smp.c(架构相关实现)
c
// 简化的伪代码,实际实现在 arch/arm*/kernel/smp.c
void smp_send_stop(void)
{
// 向其他 CPU 发送 IPI(核间中断)
// 其他 CPU 收到 IPI 后执行 cpu_panic_stop()
// cpu_panic_stop() 会无限循环,等待重启
}
注意:如果其他 CPU 在关中断状态下死锁,IPI 无法到达------这就是"hard lockup"场景,需要 NMI(不可屏蔽中断)来处理。
四、内核 Watchdog:Hard Lockup 与 Soft Lockup
内核 Watchdog 用于检测 CPU 死锁,分为两类:Hard Lockup(硬死锁)和 Soft Lockup(软死锁)。
4.1 Hard Lockup Detector(硬死锁检测)
检测对象 :CPU 在关中断状态下长时间无响应。
原理 :利用 NMI(不可屏蔽中断)------即使 CPU 关中断了,NMI 仍能到达。
源码路径 :kernel/msm-3.18/kernel/watchdog.c
c
// Hard lockup 检测的核心逻辑(简化)
static int is_hardlockup(void)
{
unsigned long hrint = __this_cpu_read(hrtimer_interrupts);
// 如果 hrtimer 中断计数没有更新,说明 CPU 死锁
if (__this_cpu_read(hrtimer_interrupts_saved) == hrint)
return 1;
__this_cpu_write(hrtimer_interrupts_saved, hrint);
return 0;
}
// NMI 处理函数
static void watchdog_overflow_callback(struct perf_event *event, ...)
{
if (is_hardlockup()) {
int this_cpu = smp_processor_id();
// 只打印一次
if (__this_cpu_read(hard_watchdog_warn) == true)
return;
if (hardlockup_panic)
panic("Watchdog detected hard LOCKUP on cpu %d", this_cpu);
else
WARN(1, "Watchdog detected hard LOCKUP on cpu %d", this_cpu);
__this_cpu_write(hard_watchdog_warn, true);
}
}
Hard Lockup 检测原理:
1. 每个 CPU 有一个 hrtimer(高精度定时器),每 4 秒触发一次
2. hrtimer 触发时递增 hrtimer_interrupts 计数器
3. NMI watchdog 通过 perf event 监控 CPU 周期
4. 如果 NMI 触发时发现 hrtimer_interrupts 没有更新
└─ 说明 hrtimer 被阻塞 → CPU 关中断死锁 → 触发 panic
日志特征:Kernel panic - not syncing: Watchdog detected hard LOCKUP on cpu 2
4.2 Soft Lockup Detector(软死锁检测)
检测对象 :CPU 在开中断但长时间无法调度(自旋锁持有过久、长时间循环)。
原理:hrtimer 每 4 秒触发,检查 CPU 是否有调度事件发生。超过阈值(默认 20 秒)则触发软锁死告警。
源码路径 :kernel/msm-3.18/kernel/watchdog.c
c
// Soft lockup 检测的核心逻辑
static int is_softlockup(unsigned long touch_ts)
{
unsigned long now = get_timestamp();
// 如果当前时间 - 上次更新时间 > 阈值(20秒)
if (time_after(now, touch_ts + get_softlockup_thresh()))
return now - touch_ts; // 返回死锁时长
return 0;
}
// hrtimer 处理函数
static enum hrtimer_restart watchdog_timer_fn(struct hrtimer *hrtimer)
{
unsigned long touch_ts = __this_cpu_read(watchdog_touch_ts);
int duration;
// 检查 soft lockup
duration = is_softlockup(touch_ts);
if (unlikely(duration)) {
pr_emerg("BUG: soft lockup - CPU#%d stuck for %us! [%s:%d]\n",
smp_processor_id(), duration,
current->comm, task_pid_nr(current));
dump_stack();
if (softlockup_panic)
panic("softlockup: hung tasks");
}
return HRTIMER_RESTART;
}
Soft Lockup 检测原理:
1. 每个 CPU 有一个 watchdog 内核线程
2. watchdog 线程定期调用 __touch_watchdog() 更新时间戳
3. hrtimer 每 4 秒检查一次时间戳
4. 如果时间戳超过 20 秒未更新
└─ 说明 watchdog 线程被阻塞 → CPU 无法调度 → 触发告警
日志特征:BUG: soft lockup - CPU#1 stuck for 23s! [swapper/1:0]
4.3 运行时控制
bash
# 查看 watchdog 状态
cat /proc/sys/kernel/watchdog # 0:禁用, 1:启用
cat /proc/sys/kernel/watchdog_thresh # 默认 10 秒
# 手动触发所有 CPU 的 backtrace(调试用)
echo 1 > /proc/sys/kernel/softlockup_all_cpu_backtrace
五、重启流程与重启原因
5.1 emergency_restart() 调用链
panic 后最终调用 emergency_restart() 触发硬件重启:
源码路径 :kernel/msm-3.18/kernel/reboot.c
c
void emergency_restart(void)
{
kmsg_dump(KMSG_DUMP_EMERG);
machine_emergency_restart();
}
EXPORT_SYMBOL_GPL(emergency_restart);
panic() → emergency_restart() → machine_emergency_restart()
└─ 架构实现(arch/arm*/kernel/reboot.c):
├─ 写入 PMIC 复位寄存器
├─ 触发硬件看门狗后死循环等待复位
└─ 写 PS_HOLD(高通平台)
5.2 重启原因记录(Reboot Reason)
内核在 panic 时会将重启原因写入 PMIC 寄存器或 IMEM,BootLoader 读取后传递给内核命令行:
写入:内核写 reboot reason 到 PMIC 寄存器 / IMEM
读取:BootLoader → 内核命令行 androidboot.bootreason=kernel_panic
用户空间:/sys/kernel/boot_reason 或 ro.boot.bootreason
常见重启原因值:
| 值 | 含义 |
|---|---|
kernel_panic |
内核 panic |
watchdog |
硬件/内核 watchdog 超时 |
longkey |
长按电源键 |
recovery |
进入 recovery 模式 |
unknown |
未知原因 |
六、现场保存:pstore 与 ramoops
Kernel panic 发生后重启会导致所有内核日志(dmesg)丢失。pstore(Persistent Store)框架通过保留 DDR 区域来解决此问题。
6.1 原理
DDR 内存布局:
┌───────────────────────────────┐
│ 常规内存(重启后被清零) │
├───────────────────────────────┤
│ ramoops 保留区域 │ ← 内核参数 mem= 保留
│ ├─ console-ramoops (控制台) │
│ ├─ pmsg-ramoops (用户态) │
│ └─ ftrace-ramoops (ftrace) │
└───────────────────────────────┘
重启后 BootLoader 不会触碰这个区域
6.2 AOSP 7 中的挂载
源码路径 :system/core/rootdir/init.rc
ini
# init.rc 第 228-233 行
# pstore/ramoops previous console log
mount pstore pstore /sys/fs/pstore
chown system log /sys/fs/pstore/console-ramoops
chmod 0440 /sys/fs/pstore/console-ramoops
chown system log /sys/fs/pstore/pmsg-ramoops-0
chmod 0440 /sys/fs/pstore/pmsg-ramoops-0
注意 :pstore 挂载在
on init阶段(第 31 行开始),而非on post-fs。挂载后/sys/fs/pstore/console-ramoops即上次 panic 的内核日志。
6.3 平台差异
| 平台 | ramoops 配置方式 | last_kmsg 路径 |
|---|---|---|
| 高通 (Qualcomm) | ramoops_memreserve= 命令行 |
/sys/fs/pstore/console-ramoops |
| 联发科 (MTK) | MTK 自定义 aee 框架 | /data/aee_exp/ 或 /proc/last_kmsg |
| 展讯 (Spreadtrum) | 展讯自定义 dump | /data/log/dump/ |
七、last_kmsg 解读
典型的内核 panic 日志片段:
[ 1234.567890] Unable to handle kernel NULL pointer dereference at virtual address 00000000
[ 1234.567900] pgd = c0004000
[ 1234.567920] Internal error: Oops: 805 [#1] PREEMPT SMP ARM
[ 1234.567930] CPU: 1 PID: 234 Comm: Binder:234_1
[ 1234.567950] PC is at my_function+0x18/0x50
[ 1234.567960] LR is at caller_function+0x2c/0x48
[ 1234.567980] [<c0123456>] (my_function) from [<c0234567>] (caller_function+0x2c/0x48)
[ 1234.567990] [<c0234567>] (caller_function) from [<c0345678>] (top_function+0x14/0x2c)
关键解读点:
| 行 | 含义 |
|---|---|
Unable to handle kernel NULL pointer dereference |
错误类型:空指针解引用 |
at virtual address 00000000 |
访问的地址为 0x0 |
Internal error: Oops: 805 |
Oops 错误码 |
PC is at my_function+0x18/0x50 |
PC 位置(偏移 0x18,函数总长 0x50) |
CPU: 1 PID: 234 |
出问题的 CPU 和进程 |
Backtrace: |
内核调用栈回溯 |
八、定位工具与技巧
8.1 快速抓取 last_kmsg
bash
# 方法1:直接从 /proc 读取
adb shell cat /proc/last_kmsg > last_kmsg.txt
# 方法2:从 pstore 读取
adb shell cat /sys/fs/pstore/console-ramoops > last_kmsg.txt
# 方法3:MTK 平台
adb shell cat /data/aee_exp/*/db.fatal.*.txt
8.2 内核栈回溯还原
bash
# 1. 找到 vmlinux(未压缩内核镜像)
# out/target/product/<device>/obj/KERNEL_OBJ/vmlinux
# 2. 还原函数名
arm-eabi-addr2line -e vmlinux -f -C <PC地址>
# 3. 反汇编确认
arm-eabi-objdump -d vmlinux | grep -A 20 <函数名>
8.3 常见内核 panic 类型
| 类型 | 日志特征 | 定位方法 |
|---|---|---|
| 空指针解引用 | NULL pointer dereference at virtual address 00000000 |
addr2line 还原 PC 地址 |
| 内核 BUG | kernel BUG at drivers/xxx/yyy.c:123! |
直接定位到源码行 |
| OOM Panic | Out of memory and no killable processes |
检查内存使用趋势 |
| 文件系统错误 | VFS: Unable to mount root fs |
检查 eMMC/UFS、分区表 |
8.4 常见问题排查清单
| 症状 | 优先检查 |
|---|---|
| 插拔充电器重启 | 充电驱动、电源管理(drivers/power/) |
| 特定 App 操作后重启 | 该操作触发的内核路径(GPU、Camera 驱动) |
| 低电量重启 | 电池电量检测、电压保护 |
| 高负载压力测试重启 | 散热/Thermal、DVFS 调频 |
| 随机无规律重启 | 内存问题(DDR 位翻转)、硬件虚焊 |
| 开机过程中重启 | 文件系统挂载、外设初始化 |
九、总结
-
Kernel Panic 是内核的"最后防线":关中断 → 打印日志 → 停止其他 CPU → 保存日志到 pstore → 重启系统。
-
panic() 的执行顺序至关重要 :先打印日志再停止其他 CPU,确保日志能输出;
kmsg_dump()在smp_send_stop()之后执行,确保日志能写入 pstore。 -
内核 Watchdog 的双重保护:Hard lockup 通过 NMI 检测关中断死锁,Soft lockup 通过 hrtimer 检测调度死锁。
-
pstore/ramoops 是抓住内核崩溃现场的关键:通过保留 DDR 区域让内核日志在重启后依然可读。
-
重启原因记录机制:配合 bootreason 属性快速判断是 panic、watchdog 还是用户主动重启。
-
定位三板斧 :抓
last_kmsg→ 找 PC 地址 →addr2line还原代码位置。
下一篇我们将进入 Native 层,深入分析 Tombstone 机制------当 native 进程崩溃时,debuggerd 如何生成 tombstone 文件,以及如何从中还原崩溃现场。
本文基于 AOSP 7(Android Nougat, Linux 3.18)源码编写。