尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android 7系统异常问题排查(二)内核层—Kernel Panic与系统重启

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 7Linux 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-triggerSysRq 触发 panic三、panic() 函数源码分析panic()位于kernel/panic.c是内核 panic 的核心入口。理解其执行顺序对分析 panic 日志至关重要。源码路径kernel/msm-3.18/kernel/panic.cvoidpanic(constchar*fmt,...){staticDEFINE_SPINLOCK(panic_lock);staticcharbuf[1024];va_list args;longi,i_next0;intstate0;// 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_BUGVERBOSEif(!test_taint(TAINT_DIE)oops_in_progress1)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_timeout0){pr_emerg(Rebooting in %d seconds..,panic_timeout);for(i0;ipanic_timeout*1000;iPANIC_TIMER_STEP){touch_nmi_watchdog();mdelay(PANIC_TIMER_STEP);}}if(panic_timeout!0){emergency_restart();}// 如果 panic_timeout 0无限循环等待for(i0;;iPANIC_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_timeoutpanic_timeout控制 panic 后的行为通过内核命令行或 sysctl 配置源码路径kernel/msm-3.18/kernel/panic.c// 默认值来自内核配置 CONFIG_PANIC_TIMEOUTintpanic_timeoutCONFIG_PANIC_TIMEOUT;EXPORT_SYMBOL_GPL(panic_timeout);// 内核命令行参数panicNcore_param(panic,panic_timeout,int,0644);# 内核命令行配置androidboot.panic_timeout5# 5秒后重启# 运行时修改echo5/proc/sys/kernel/panic场景panic_timeout 值行为开发阶段0无限等待方便连接 JTAG 调试量产固件55 秒后自动重启高通平台默认值压力测试1快速重启收集更多 panic 样本关键设计panic_timeout0时系统会无限循环在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不可屏蔽中断来处理。四、内核 WatchdogHard 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_cpusmp_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 24.2 Soft Lockup Detector软死锁检测检测对象CPU 在开中断但长时间无法调度自旋锁持有过久、长时间循环。原理hrtimer 每 4 秒触发检查 CPU 是否有调度事件发生。超过阈值默认 20 秒则触发软锁死告警。源码路径kernel/msm-3.18/kernel/watchdog.c// Soft lockup 检测的核心逻辑staticintis_softlockup(unsignedlongtouch_ts){unsignedlongnowget_timestamp();// 如果当前时间 - 上次更新时间 阈值20秒if(time_after(now,touch_tsget_softlockup_thresh()))returnnow-touch_ts;// 返回死锁时长return0;}// hrtimer 处理函数staticenumhrtimer_restartwatchdog_timer_fn(structhrtimer*hrtimer){unsignedlongtouch_ts__this_cpu_read(watchdog_touch_ts);intduration;// 检查 soft lockupdurationis_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.cvoidemergency_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 寄存器或 IMEMBootLoader 读取后传递给内核命令行写入内核写 reboot reason 到 PMIC 寄存器 / IMEM 读取BootLoader → 内核命令行 androidboot.bootreasonkernel_panic 用户空间/sys/kernel/boot_reason 或 ro.boot.bootreason常见重启原因值值含义kernel_panic内核 panicwatchdog硬件/内核 watchdog 超时longkey长按电源键recovery进入 recovery 模式unknown未知原因六、现场保存pstore 与 ramoopsKernel panic 发生后重启会导致所有内核日志dmesg丢失。pstorePersistent 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_function0x18/0x50 [ 1234.567960] LR is at caller_function0x2c/0x48 [ 1234.567980] [c0123456] (my_function) from [c0234567] (caller_function0x2c/0x48) [ 1234.567990] [c0234567] (caller_function) from [c0345678] (top_function0x14/0x2c)关键解读点行含义Unable to handle kernel NULL pointer dereference错误类型空指针解引用at virtual address 00000000访问的地址为 0x0Internal error: Oops: 805Oops 错误码PC is at my_function0x18/0x50PC 位置偏移 0x18函数总长 0x50CPU: 1 PID: 234出问题的 CPU 和进程Backtrace:内核调用栈回溯八、定位工具与技巧8.1 快速抓取 last_kmsg# 方法1直接从 /proc 读取adb shellcat/proc/last_kmsglast_kmsg.txt# 方法2从 pstore 读取adb shellcat/sys/fs/pstore/console-ramoopslast_kmsg.txt# 方法3MTK 平台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-CPC地址# 3. 反汇编确认arm-eabi-objdump-dvmlinux|grep-A20函数名8.3 常见内核 panic 类型类型日志特征定位方法空指针解引用NULL pointer dereference at virtual address 00000000addr2line 还原 PC 地址内核 BUGkernel BUG at drivers/xxx/yyy.c:123!直接定位到源码行OOM PanicOut 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 7Android Nougat, Linux 3.18源码编写。
返回列表