Android 7系统异常问题排查(二)内核层—Kernel Panic与系统重启
系列目录:第一篇:异常机制全景图 | 第二篇:Kernel Panic 与系统重启 | 第三篇:Tombstone 机制 | 第四篇:System Server Watchdog | 第五篇:System Server 崩溃 | 第六篇:ANR 机制 | 第七篇:Java 层崩溃 | 第八篇: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
voidpanic(constchar*fmt,...){staticDEFINE_SPINLOCK(panic_lock);staticcharbuf[1024];va_list args;longi,i_next=0;intstate=0;// 1. 禁用本地中断,防止死锁local_irq_disable();// 2. 获取 panic 锁,确保只有一个 CPU 执行 panicif(!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/ramoopskmsg_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
// 默认值来自内核配置 CONFIG_PANIC_TIMEOUTintpanic_timeout=CONFIG_PANIC_TIMEOUT;EXPORT_SYMBOL_GPL(panic_timeout);// 内核命令行参数:panic=Ncore_param(panic,panic_timeout,int,0644);# 内核命令行配置androidboot.panic_timeout=5# 5秒后重启# 运行时修改echo5>/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(架构相关实现)
// 简化的伪代码,实际实现在 arch/arm*/kernel/smp.cvoidsmp_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
// Hard lockup 检测的核心逻辑(简化)staticintis_hardlockup(void){unsignedlonghrint=__this_cpu_read(hrtimer_interrupts);// 如果 hrtimer 中断计数没有更新,说明 CPU 死锁if(__this_cpu_read(hrtimer_interrupts_saved)==hrint)return1;__this_cpu_write(hrtimer_interrupts_saved,hrint);return0;}// NMI 处理函数staticvoidwatchdog_overflow_callback(structperf_event*event,...){if(is_hardlockup()){intthis_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);elseWARN(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
// Soft lockup 检测的核心逻辑staticintis_softlockup(unsignedlongtouch_ts){unsignedlongnow=get_timestamp();// 如果当前时间 - 上次更新时间 > 阈值(20秒)if(time_after(now,touch_ts+get_softlockup_thresh()))returnnow-touch_ts;// 返回死锁时长return0;}// hrtimer 处理函数staticenumhrtimer_restartwatchdog_timer_fn(structhrtimer*hrtimer){unsignedlongtouch_ts=__this_cpu_read(watchdog_touch_ts);intduration;// 检查 soft lockupduration=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");}returnHRTIMER_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 运行时控制
# 查看 watchdog 状态cat/proc/sys/kernel/watchdog# 0:禁用, 1:启用cat/proc/sys/kernel/watchdog_thresh# 默认 10 秒# 手动触发所有 CPU 的 backtrace(调试用)echo1>/proc/sys/kernel/softlockup_all_cpu_backtrace五、重启流程与重启原因
5.1 emergency_restart() 调用链
panic 后最终调用emergency_restart()触发硬件重启:
源码路径:kernel/msm-3.18/kernel/reboot.c
voidemergency_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
# 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
# 方法1:直接从 /proc 读取adb shellcat/proc/last_kmsg>last_kmsg.txt# 方法2:从 pstore 读取adb shellcat/sys/fs/pstore/console-ramoops>last_kmsg.txt# 方法3:MTK 平台adb shellcat/data/aee_exp/*/db.fatal.*.txt8.2 内核栈回溯还原
# 1. 找到 vmlinux(未压缩内核镜像)# out/target/product/<device>/obj/KERNEL_OBJ/vmlinux# 2. 还原函数名arm-eabi-addr2line-evmlinux-f-C<PC地址># 3. 反汇编确认arm-eabi-objdump-dvmlinux|grep-A20<函数名>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)源码编写。