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

系列目录第一篇:异常机制全景图 | [第二篇: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.cpanic("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 位翻转)、硬件虚焊
开机过程中重启 文件系统挂载、外设初始化

九、总结

  1. Kernel Panic 是内核的"最后防线":关中断 → 打印日志 → 停止其他 CPU → 保存日志到 pstore → 重启系统。

  2. panic() 的执行顺序至关重要 :先打印日志再停止其他 CPU,确保日志能输出;kmsg_dump()smp_send_stop() 之后执行,确保日志能写入 pstore。

  3. 内核 Watchdog 的双重保护:Hard lockup 通过 NMI 检测关中断死锁,Soft lockup 通过 hrtimer 检测调度死锁。

  4. pstore/ramoops 是抓住内核崩溃现场的关键:通过保留 DDR 区域让内核日志在重启后依然可读。

  5. 重启原因记录机制:配合 bootreason 属性快速判断是 panic、watchdog 还是用户主动重启。

  6. 定位三板斧 :抓 last_kmsg → 找 PC 地址 → addr2line 还原代码位置。

下一篇我们将进入 Native 层,深入分析 Tombstone 机制------当 native 进程崩溃时,debuggerd 如何生成 tombstone 文件,以及如何从中还原崩溃现场。


本文基于 AOSP 7(Android Nougat, Linux 3.18)源码编写

相关推荐
小田的博客4 小时前
SAP MM 供应商银行主数据更新报错!message R1228!
android·java·服务器
一笑的小酒馆5 小时前
Android智能猫砂盆视频加载慢卡顿问题分析
android
只会cv的小前端7 小时前
七巧低代码服务端脚本使用方法
android·低代码·rxjava
7 小时前
Android 自定义 View 实战:从零打造工业级全向摇杆控件(OmniJoystickView)
android·kotlin·自定义view·摇杆控件
雨白7 小时前
深入理解 Kotlin 协程 (十一):以逸待劳,探秘 select 多路复用与并发安全策略
android·kotlin
hunterandroid9 小时前
[Android 从零到一] Android 深度链接与 App Links:从 URI Scheme 到可验证的应用跳转
android
我命由我1234512 小时前
Jetpack Compose - Material Design 断点范围、WindowSizeClass、针对不同屏幕尺寸创建预览、四类导航栏
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
执明wa12 小时前
Android 开发中的设计模式入门:六大设计原则
android·设计模式
松仔log1 天前
Java中级——组合和继承
android·java·开发语言
我命由我123451 天前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime