
做MTK平台BSP或者系统稳定性这一块几乎没人能绕开hang_detect。很多刚接手MTK项目的朋友第一次看到串口日志里刷出WDT timeout、看到系统直接重启第一反应就是“死机了抓不到日志”。实际上MTK这套异常检测机制设计得非常系统理解透了之后你会发现它不仅能告诉你“系统挂了”还能告诉你“挂在哪、为什么挂、是哪个线程在搞事”。这篇文章我就从原理到实战把hang_detect这个机制完整拆一遍附带我这些年调试中踩过的坑和排查思路。先说清楚本文适合谁看如果你在做MTK平台驱动开发、系统稳定性分析、或者正在被“偶现死机”“低概率重启”折磨这篇文章就是给你准备的。内容涉及ARM体系结构、Linux内核调度、看门狗机制的底层逻辑但我会尽量用通俗的方式讲保证有C语言和Linux基础的朋友都能跟上。1. 先从整体上理解 hang_detect 是干什么的1.1 两层看门狗硬件层和软件层各管一摊MTK平台的异常检测体系本质上是一套“分级看门狗”机制。市面上很多方案只有一颗硬件看门狗超时了就硬复位根本不给你留反应时间。MTK不一样它在硬件看门狗之外还加了一层软看门狗这就是hang_detect的核心价值所在。硬件层面MTK芯片内部有独立的硬件看门狗定时器WDT一旦软件不喂狗超过设定时间就强制复位整个系统。这个设计是为了兜底防止系统彻底卡死导致连异常处理代码都跑不动。软件层面MTK实现了XSWTSoftware Watchdog Timer软看门狗它做的事情更精细——不仅是检测系统有没有响应还会检测系统任务调度正不正常、中断有没有卡住、关键线程是否在运行。这两者之间的关系你可以理解成“小区保安”和“夜间巡逻队”。硬件看门狗是保安他只管到点锁门不管里面发生了什么软看门狗是巡逻队他会一层层检查每个房间有没有异常发现问题先尝试叫醒叫不醒再按程序上报。所以实际调试中绝大多数的死机现场都由软看门狗先记录下证据然后才触发硬件复位。1.2 触发后的处理流程从异常到落盘当软看门狗判定系统出现异常时它不会立刻复位而是先走一套异常处理流程这套流程在MTK平台上叫AEEAndroid Exception Engine。AEE会做这几件事将当前所有CPU的寄存器现场、栈回溯信息保存下来把当前正在运行的线程、以及系统所有任务的状态导出在串口和kernel log中输出关键异常信息将完整的异常现场写入/data/aee/db目录老平台是/sdcard/aee/db最后才触发系统重启这个设计非常关键。也就是说你看到的“死机重启”实际上是检测机制在完成取证后的既定动作而不是毫无征兆的硬件故障。明白了这一点你拿到日志后就知道该去哪里找证据了。注意如果异常发生在系统内存严重不足、存储不可写等极端情况下异常现场可能无法完整落盘这时候日志会不完整。所以遇到这类问题第一件事是确认/data/aee/db下有没有对应时间点的db文件。2. 核心机制细节拆解它到底怎么判断系统“挂”了2.1 任务调度监测与任务超时判断软看门狗最核心的监测对象是Linux内核的任务调度。内核里的每个CPU都有一个runqueue如果某个CPU上的任务长时间不让出CPU或者调度器本身出了死锁系统就会表现为“部分线程卡死但中断还在”。MTK的做法是启动一个内核线程周期性地去检查系统中各个任务的运行状态。它关心的核心指标包括// 简化描述实际代码更复杂 - 任务是否处于不可中断的D状态 - 任务是否长时间占据CPUR状态持续过久 - 任务的栈底标记是否被破坏栈溢出特征 - 自旋锁持有时间是否异常一旦发现异常它会在日志中输出类似这样的信息[R][D][-] kernel BUG at kernel/... // 或者 [S][D][-] CPU: 3 PID: 1234 Tainted: G这里我解释下日志格式R表示RunningD表示该任务处于不可中断睡眠态-表示可中断睡眠态。如果你看到大量D状态的任务堆积基本可以判定系统内部发生了锁死或者IO阻塞。2.2 中断与底半部监测没跑调度器不代表没病任务调度检查只能覆盖到有独立上下文的任务但很多死机情况发生在中断上下文。比如一个中断处理函数里做了太多事或者中断里关了抢占又死等某个锁这时候任务调度是正常的但系统响应已经完蛋了。针对这个场景MTK的hang_detect有专门的中断检测逻辑。它会监控每个CPU上中断的进入时间和退出时间如果某次中断处理时间超过了阈值就会输出该中断的调用栈同时标记为异常。这部分的调试信息通常长这样IRQ 53: 3.87s // 中断号53处理耗时3.87秒明显超标看到这种日志你就得往中断处理函数里去查了常见的坑有中断里调用了msleep、中断里做了耗时的IO操作、中断和某个任务互相死等资源。2.3 软看门狗喂狗超时看门狗不是无限期的需要特别强调一点MTK的软看门狗和硬件看门狗是联动的软看门狗会把心跳上报给硬件看门狗。如果软看门狗自己都跑不动了硬件看门狗就会直接触发复位这属于“最坏情况”的处理。在实际调试中区分这三种情况很重要现象可能原因证据位置系统卡死但串口还能输出软看门狗检测到任务异常正在走AEE流程kernel log / db文件串口完全无输出直接重启硬件看门狗超时软看门狗也失效仅能靠抓硬件复位原因寄存器反复重启无核日志异常发生在非常早期的启动阶段preloader日志 / 硬件复位原因这个区分决定了你的排查方向如果串口还能动优先去db文件里找线索如果完全没日志就得考虑是不是硬件复位导致要去看PMIC或者主控的复位状态寄存器。3. 实战调试第一步拿到并读懂一次完整的死机日志3.1 触发日志的第二种方式手动按键触发在实际项目中最大的痛点是复现困难死机是偶发的。MTK平台提供了一个手动触发hang_detect的调试手段这在联调阶段特别有用。你可以在系统运行正常时主动让软看门狗超时从而验证整个日志链路是否工作正常。常用的手动触发方式有以下几种# 方式一通过内核节点触发XSWT超时 echo 1 /proc/mtk_dbg/hang_detect # 方式二通过串口命令触发 # 在uart console输入以下命令 - hang_detect这里我要插一句很多新人不知道adb shell和uart console的区别。用adb只能看到Android上层的logcat有些早期的死机信息在内核阶段就已经输出了adb看不到。所以在做死机调试时强烈建议使用uart串口连接调试串口并且接好USB转串口工具用PuTTY或者minicom这类工具实时捕获串口输出。这也是我看热搜词里有人问“串口调试助手”怎么用的原因——硬件调试场景下串口工具是刚需。实际上我建议是serial串口工具 adb同时挂着两路日志互补排查效率翻倍。3.2 死机日志里最关键的几个字段我整理一份我在调试时最常看的字段清单拿到db包或者串口日志后按这个顺序查能节省大量时间tbase: 异常触发时的任务栈基址 db_tag: 异常类型标签标识是哪一类看门狗超时 Task: 当前正在运行的任务信息 CPU: 哪个CPU上发生的异常 Stack: 异常时的调用栈回溯 Backtrace: 同样调用栈但更详细 Open FD: 相关任务打开的文件描述符排查IO卡死很有用第一步永远是看db_tag它告诉你是哪类问题。db_tag常见值包括WDT硬件看门狗超时、XWDT软看门狗超时、KE内核异常、DW死锁等。根据不同的db_tag后续的排查路径完全不同。第二步是看CPU字段和Task字段。比如日志显示CPU: 2说明是第2个核上出现异常Task: kworker/u16:1说明是内核工作队列卡住了。这两个字段结合起来基本能把问题定位到具体执行单元。3.3 如何判断一次死机是“真死”还是“软死”这是我刚入行时最困惑的点也是很多人拿到日志后无从下手的原因。所谓“真死”是整个系统完全停止响应只有硬件看门狗能恢复所谓“软死”是部分线程卡住但系统还没完全崩溃中断还在运行最终被软看门狗检测到并触发AEE。判断方法其实很简单看kernel log的最后一段输出如果最后一段日志是卡在某个驱动的IO操作上然后出现了XWDT或者WDT字样这说明系统在正常调度时被“某个点”挡住了属于软死——问题在具体驱动或者某个资源竞争上。如果最后一段日志是中断输出了一半就嘎然而止然后直接出现硬件复位标志说明系统的中断路径出了问题属于硬死——问题可能在中断处理函数或者内存系统上。# 真实日志片段示例 4[ 1234.567890] xwdt: catch task hang on CPU3, task: kworker/u16:3 4[ 1234.567891] xwdt: backtrace: 4[ 1234.567892] [c0abcdef] __switch_to0x44/0x4c 4[ 1234.567893] [c0abc123] schedule0x30/0x70 4[ 1234.567894] [c0abc456] rpm_resume0x2c4/0x4d0 4[ 1234.567895] [c0abc789] __pm_runtime_resume0x34/0x48看到rpm_resume和__pm_runtime_resume同时出现在栈里基本可以判断是runtime PM的引用计数或者同步处理出了问题导致某个设备无法完成resumekworker一直卡在等待。这种问题在很多外设驱动的runtime_suspend/resume回调里常见比如Camera、Sensor、Display的电源管理。4. 实际调试路径我用过最有效的三板斧4.1 板斧一让抓取日志的环境足够稳定不要一上来就改代码先把日志链路打通。我见过太多案例因为存储空间不够db文件写不进去结果死机复现了但什么都没抓到。调试环境的准备我要给一个完整的checklist串口连接确认OK波特率设置正确MTK平台通常是921600内核日志开关打开printk级别调整到7包含debug级别/proc/sys/kernel/panic_on_oops设置为1让异常第一时间触发panic确保/data/aee/db有足够空间或者通过设备节点将日志重定向到外部存储用adb shell getprop persist.vendor.log.tel_dbg确认远程日志抓取功能开启有条件的强烈建议把日志通过以太网/UART实时输出到PC端用脚本做一个自动备份。我调的一个偶现几分钟才出现的问题就是靠PC端自动轮转保存串口日志连续挂了两天最终捕获到完整现场。4.2 板斧二用GDB和符号表离线解析调用栈MTK平台的kernel log输出的调用栈地址在release版本里是不带符号的你看到的是一串十六进制地址。这时候就需要用GDB来还原函数名。操作流程如下# 1. 找到对应内核版本和modem版本的符号表 # 通常在编译产物目录下 # 2. 用GDB加载vmlinux和符号表 gdb vmlinux # 3. 用info line还原地址对应的代码位置 (gdb) info line *0xc0abcdef # 4. 或者用addr2line快速转换 addr2line -e vmlinux 0xc0abcdef -f -C这个操作有一个前置条件——每次编译的代码必须和出厂的固件版本一致。如果你手头机器的kernel和符号表版本对不上还原出来的函数名是乱码或者根本对不上这时候会很痛苦。所以我每次给客户或者测试版本打固件时都会保留一份编译产物标注好hash值和日期后续排查直接对着找。4.3 板斧三构造三次必现的场景来验证修复找到了问题点改完代码后不能直接上量产得先构造一个可复现的测试场景。这里我拿最常见的runtime PM死锁问题举例。问题现象系统休眠唤醒几十次之后偶发死机。通过日志确认是某个外设的runtime_resume被调用后一直等不到完成最终软看门狗超时。验证修复的方式是写一个简单的shell脚本高频做外设的打开、关闭、休眠、唤醒操作#!/system/bin/sh # 高频开关测试脚本 i1 while [ $i -le 1000 ] do echo test loop $i # 打开外设 echo on /sys/devices/platform/xxx/power/control sleep 0.01 # 读取数据 cat /dev/xxx_node /dev/null # 关闭外设 echo off /sys/devices/platform/xxx/power/control sleep 0.02 # 触发休眠唤醒 echo mem /sys/power/state sleep 0.05 echo on /sys/power/state i$((i1)) done这个脚本跑过几百次如果不再出现死锁基本就说明问题修住了。如果还有问题脚本循环次数越大复现概率越高刚好可以用来验证代码是否彻底修复。这种压测方式对很多稳定性问题都适用本质上就是通过高频操作增加资源竞争的概率把偶发问题变成必现问题。经验之谈很多偶发死机本质上都是“时序”问题。代码评审时看不出问题是因为人在阅读代码时往往会忽略时序竞争压力测试能快速暴露这类问题。所以我每次调试完都会让测试同事写一个高频操作的脚本作为回归用例长期保留。5. 常见问题排查技巧实录5.1 问题一有堆栈但无有效信息现象日志里确实有调用栈回溯但栈里面全部是0xdead0000或者无符号的地址完全无法解析。原因和对策这种多半是栈已经被破坏或者当前任务的内核栈溢出导致回溯失败。遇到这种情况除了看当前任务的栈还要检查中断栈。可以搜索日志里的IRQ stack、overflow等关键字。另外一个常见的场景是编译器优化导致栈回溯不完整。release版本的编译优化级别通常比较高某些栈帧被优化掉回溯出来只有一层。碰到这种情况我通常会在怀疑的函数上手动加printk打印关键上下文信息重新编译验证。这个方法虽然土但非常有效。5.2 问题二死机只在高负载时出现现象手机跑PCMark、跑大型游戏、多任务切换时偶发重启待机状态完全正常。排查方向优先看是不是电压/温升问题导致的硬件复位这类问题往往不是软件hang_detect能解决的。需要通过以下手段排除# 查看CPU频率和调频状态看是否处于异常高压状态 cat /proc/cpufreq # 查看温度 cat /sys/class/thermal/thermal_zone*/temp # 检查复位原因 cat /proc/mtk_dbg/rgu如果确认复位原因是RGU硬件复位而不是软看门狗那就要跟硬件同事一起排查电源。尤其是新打的PCB板电源纹波大、电容漏电都可能导致高负载下电压跌落触发复位。软件上可以适当降低最高频率、优化调频策略来规避但治本还得看硬件。5.3 问题三日志文件完全不生成现象明显死机了重启后adb shell ls /data/aee/db/发现没有任何新文件或者文件生成到一半就没有了。排查步骤检查/data分区空间是否充足空间不足会导致文件写入失败检查persist.vendor.aee.mode属性设置有时候被设置成0关闭模式确认内核的AEE编译选项是否打开有些定制版本为了省空间会砍掉这个功能检查是否有其他服务提前杀掉了异常处理进程这个问题在OTG外接U盘调试时尤其常见——日志导到外设时存储链路本身就有问题死机时外设恰好也失效了日志自然飞了。常规做法是内置存储和外置存储同时保留一份互为备份。5.4 问题四唤醒源导致的反复死机这个场景在MTK平台上特别典型系统休眠后被某个中断唤醒但唤醒后没有正确关闭该中断或没有正确处理锁导致系统反复死机。排查技巧这类问题看最后的日志通常会发现唤醒源的中断线程一直在跑甚至CPU占用率异常。可以在日志里搜索wakelock、irq, 以及PM: suspend exit这些关键字段判断唤醒路径是否正常。如果反复死机间隔非常规律比如每次都精确到几十秒很有可能是某个高优先级中断或者内核线程在不断的竞争资源每次都在同一位置卡住。这种必现性死机反而好办直接在卡住的位置加日志一次就能定位。难办的是间隔不规律的那就回到第4节说的构造高频压力测试场景去逼它复现。5.5 实用速查表我把这些年调试MTK平台遇到的问题整理成了下表供参考日志关键字问题类型优先排查方向XWDT timeout软看门狗超时任务调度卡死、驱动死锁WDT timeout硬件看门狗超时系统级卡死、中断失效KE内核异常空指针、内存越界、非法指令DW死锁检测锁顺序问题、信号量泄漏RGU硬件复位电源问题、外部复位、硬件故障scheduler stall调度停滞自旋锁持有过久、中断风暴EMI内存接口异常内存频率过高、硬件走线问题这个表可以贴在工位上每次一拿到死机日志先对照关键字确定方向再深入排查能少走很多弯路。6. 复盘与延伸调试之外机制本身还能帮我们什么hang_detect机制表面上是一个“死机检测器”但实际上它是一套完整的内核健康监测体系。用久了你会发现它不仅能发现问题还能帮助验证驱动代码的质量。比如我在开发新的外设驱动时会刻意在挂载、休眠、唤醒的代码路径中引入一些随机延时然后用hang_detect去验证驱动在时序不确定的情况下是否还能稳定工作。这有点像给系统做“免疫训练”让它在真实环境的多变时序里也能站得住脚。另外一个容易被忽略的用法是利用hang_detect的日志来做驱动代码性能评估。死机日志里的调用栈回溯很多时候能意外暴露某些函数在关键路径上的耗时问题。比如我在一次死机排查中发现__pm_runtime_resume占了非常长的执行时间顺藤摸瓜发现某个I2C设备在每次resume时都会全量重新初始化一次耗时整整多出几十毫秒。这个时间在死机场景下是致命的但平时跑功能测试时完全感觉不到。所以每一次死机日志都是系统给你的一次“免费体检报告”别浪费了里面的堆栈和耗时信息。还有一点关于kmemleak和内存碎片问题的排查很多人不知道hang_detect日志里的内存信息也很有价值。当系统内存严重不足时日志里会输出内存压缩、回收相关的堆栈这些堆栈往往能直接定位到具体是哪个进程在疯狂申请内存。我之前排查过一个内存泄漏导致系统卡死的case就是靠hang_detect日志里输出的oom相关栈信息快速定位到一个native服务在持续申请内存不释放。所以拿到死机日志后不要只盯着栈回溯看内存、中断、文件系统相关的辅助信息同样重要。在当前这个项目上我把这整套排查思路沉淀成了团队内部的Hang Detect Debug Guide每次新同事接手死机问题我都会让他们先照着文档走一遍基础排查流程从确认日志链路、定位异常类型、解析栈信息到构造压测验证一套流程走完80%的问题都能有个初步结论。剩下20%的硬骨头再拉上芯片原厂一起联合调试。这篇文章里的内容也是基于这份内部指南整理出来的希望对正在被MTK平台死机问题折磨的你有所启发。