做MTK平台系统稳定性调试的人应该都遇到过这种让人抓狂的场景设备在老化房里跑了一晚第二天一查黑屏重启了三四次结果所有常规日志里干干净净没有kernel panic、没有异常调用栈只有一行孤零零的“WDT timeout”。如果这时候你不懂hang_detect机制这件悬案基本就没法往下查了。这些年我在MTK平台上处理过不少类似问题从系统挂死到偶发重启都有这篇文章想把hang_detect这套机制从原理到实战完整讲清楚包括它怎么判断系统挂了、现场信息存在哪、要怎样把这些信息变成可以定位问题的调用栈以及几个让我印象深刻的排查思路和避坑点。不管你是刚接手MTK项目的新人还是正被偶发死机折磨的稳定性工程师都可以把它当作一份上手手册。1. 为什么MTK平台需要一套独立的hang_detect机制1.1 一次“死机”背后到底发生了什么很多新手会把“系统死机”理解成硬件坏了但在MTK这类嵌入式Linux平台上绝大多数死机的本质是软件执行流失去控制。可能是某个驱动在关中断状态下跑了太久可能是某个内核线程拿着自旋锁进入死循环也可能是内存管理模块里发生了不可恢复的等待。无论哪一种最终表现都一样CPU不再按正常节奏调度任务触摸、按键、显示刷新全部停摆用户感觉到的就是“死机了”。关键问题在于系统挂死时又分为两种情况。一种是被动挂死也就是内核自己发现异常主动跳进panic流程这时我们通常能从日志里拿到漂亮的堆栈打印。另一种是主动挂死系统就像突然被抽走灵魂一样静悄悄失去响应内核根本来不及打日志。后面这种是最痛的因为没有任何异常现场除非有一套独立于主CPU运行机制能在系统挂死后强行截获现场否则这个bug就只能靠概率和运气去撞了。MTK平台上的hang_detect机制就是为这种“静默挂死”准备的兜底座舱。它并不依赖内核调度器正常工作而是从芯片底层独立运行专门监视“系统是否还在按预期呼吸”。如果发现系统已经处于濒死状态它会在重启前做两件事记录当前各CPU的现场信息以及推动系统进入异常处理流程。这才是它真正的价值所在。1.2 软件看门狗和硬件hang_detect的分工看门狗这个概念在嵌入式领域是老生常谈了但很多人不知道MTK平台是软件看门狗和硬件hang_detect配合工作的。软件看门狗这块内核里就有现成的实现比如每个CPU上的watchdog线程、hung task检测机制它们能发现某个任务长时间不调度、某个CPU长时间关中断。但这类软件机制有个致命的弱点检测线程本身跑在被检测的系统里如果系统已经彻底卡死检测线程自己也排不上CPU那就拿不到现场。硬件hang_detect解决的就是这个“最后一步”的问题。MTK SoC的RGU模块可以理解成一个完全独立于主CPU核的定时器它在芯片通电后就一直运行等着内核或者TEE侧定期来“喂狗”——向特定硬件寄存器写入周期值。一旦超过设定时间没人来喂RGU就判定系统挂了随之触发一整套硬件救济流程。这套流程甚至不需要主CPU配合因为主CPU已经失去响应能力了。打个比方软件看门狗像公司内部的考勤机员工每天自己打卡哪天有人没打卡HR能发现异常。而硬件hang_detect更像大楼门口的安保他根本不管公司内部谁在摸鱼只知道老板每天应该按固定频率进出如果连续一段时间见不到人他会直接启动应急预案。所以这两者不是重复的机制而是前后补位的两道防线。2. hang_detect的工作原理与触发链路拆解2.1 硬件侧的监控与救援动作MTK平台对hang_detect的硬件实现核心单元是RGUReset Generation Unit有些内部文档里也叫eRGU或WDT控制器。它本身就是一个32位的硬件定时器但和普通定时器最大的不同是它不受软件中断屏蔽寄存器影响也不依赖CPU时钟哪怕主CPU因为关中断而停下所有时间片RGU依然按照自己的节拍走。这个设计非常重要是它能在系统完全失联时兜底的原因。喂狗路径因人而异。在比较纯粹的Linux方案里内核的watchdog线程会周期性调用平台驱动写RGU寄存器在带TEE或者EVB方案的平台上也可能由安全世界的固件承担喂狗职责。常见的超时周期有10秒、20秒、30秒几档我记得有些项目组图省事直接把超时调到最大这其实非常危险系统挂死以后要等半分钟才触发恢复现场寄存器早被各种硬件行为覆盖了。提示喂狗周期不是越长越好也不是越短越好。我在实际项目里的经验是debug版本可以设置成5~10秒量产版本保持10~20秒同时确保喂狗线程优先级稳定避免因为系统瞬时高负载出现误触发。当喂狗超时发生后RGU内部的复位原因寄存器如RGU_REASON、RST_SOURCE会被写入对应的超时标志。接着硬件会按配置做两件事要么直接触发系统软复位要么先拉高异常引脚通知独立处理器来收集现场。支持后者的平台一般会在这段“尸体还没凉透”的窗口里把主CPU核当前的PC、LR、SP、关键寄存器组以及一段预定义内存地址范围的数据快照保存下来。这套黑白匣子一样的逻辑就是后面所有调试分析的原始素材。2.2 软件侧的配合喂狗线程与内核检测虽然RGU是硬件兜底但软件侧的配合必须足够可靠否则要么喂狗线程挂了自己都不知道要么系统明明正常但因为某个周期任务卡顿导致误复位。因此MTK平台在内核侧通常会把看门狗线程做成高优先级内核线程并且放在每个CPU上运行每个核各喂各的狗。有些平台还支持不同CPU核使用不同的超时窗口方便做异构调试。软件检测方面Linux内核本身提供的smpboot线程会检查hardlockup和softlockup。softlockup是检测某个任务关抢占时间过长hardlockup则利用NMI中断检测某个CPU长时间关中断。这些检测触发时一般会直接打印堆栈并且panic。注意它们的触发路径和硬件hang_detect不同但在用户视角里看到的都是“突然黑屏重启”所以排查时第一步就得区分是软件主动panic还是RGU超时复位。具体到代码层面MTK的watchdog驱动会注册平台看门狗设备对应的wdt ops里实现了start、stop、ping等回调。喂狗函数通常叫mtk_wdt_restart或者mtk_wdt_ping调用的核心是往WDT寄存器写一个Magic Key再写对应计数。如果某个版本上这个写寄存器操作被驱动内部的互斥锁堵住了或者中断被持续屏蔽那么即使系统总体还在运行RGU也会因为喂狗链路被阻塞而触发误复位。这种“假死”案例我在排查中遇到过不止一次。2.3 现场信息是怎么被保留下来的MTK平台把挂死现场留在独立分区里这个分区一般叫expdb全称是Exception Database。它的设计思路有点像飞机上的黑匣子平时不占用系统资源只有在异常复位发生后的冷启动过程中由Bootloader去把分区里的数据读出来再交给上层分析工具解析。expdb里保存的信息包括但不限于最后复位原因、每个CPU核的PC/LR/SP、异常发生时各通用寄存器值、一段最近的内核日志缓冲区以及根据项目配置dump下来的指定内存区域快照。如果是full dump配置整个异常相关的内存段都会被保留体积会大不少如果是mini dump只有精简的寄存器级信息。量产机上跑稳定性测试建议用mini dump出问题后先做快速定位不够用再切回full dump复现。这个分区能不能被正常读写是调试工作成败的基础。我见过有些项目的expdb分区在打包固件时没被正确初始化或者升级流程里被格式化掉了导致每次挂死都查不到现场。建议在项目稳定化初期就手动验证一下expdb的读写流程人为制造一次panic重启后看看expdb里有没有对应记录。3. 实战调试从抓日志到定位根因3.1 第一件事判断这次重启到底是不是hang_detect干的拿到一台挂死重启过的机器不要急着翻各种日志先回答一个问题这次复位是哪条路径触发的最快的办法是查看复位原因寄存器比如在root shell下读取对应的sysfs节点cat /proc/reboot_reason是比较常见的入口。如果显示的是watchdog复位或者类似字段说明是hang_detect这条路走到了最后如果显示的是kernel panic那说明内核自己先崩掉了RGU只是兜底复位的那只手。真正干过调试的人都知道这一步判断错了后面全是白忙活。因为如果问题根因是某次空指针异常导致panic你非要去翻看门狗超时链路方向就完全跑偏了。拿到整机日志后我最先搜的关键字是“WDT timeout”、“RGU reset reason”、“expdb”按时间线把复位前后的日志切开。如果能看到内核主动打印task stack多为软看门狗/hung task触发如果只有硬件复位标志那就直奔RGU超时这条线。日志块里通常会看到类似下面的记录不同内核版本和平台可能有差异但字段大意相同[ 1234.567890] mtk_wdt: WDT timeout, reset reason: 0x00000003 [ 1234.567901] mtk_hang_detect: hang detected on CPU0 [ 1234.567905] mtk_hang_detect: pc 0xffffffc000812345 [ 1234.567910] mtk_hang_detect: lr 0xffffffc000812abc看到“hang detected on CPU0”这类信息就说明系统确实在RGU超时前被识别为挂死状态了。接下来要做的是把这里的PC和LR变成你能看懂的调用栈。3.2 符号化堆栈把地址变成你能读懂的调用栈从expdb或者串口日志里拿到的PC值只是一个虚拟地址例如0xffffffc000812345。要把这个地址翻译成函数名、文件名和行号最直接的工具是addr2line。注意一定使用和当前固件完全匹配的vmlinux符号表对不上时你只会得到一堆乱码或者错误的行号。基本命令形如aarch64-linux-gnu-addr2line -e vmlinux -f -C 0xffffffc000812345输出可能是foo_irq_handler加对应的源码行号。如果手边没有vmlinux也可以用内核编译产物里的System.map做近似定位它至少能告诉你这个地址落在哪个函数的地址区间里。再懒一点的方法是把地址直接和/proc/kallsyms里的符号做对比但要注意KASLR影响有时候打印出来的虚拟地址并不是链接时的原始地址需要先做偏移校准。拿到函数名还不算完更有效的方法是进一步反汇编。用GDB加载vmlinux之后对PC地址附近的代码做反汇编(gdb) file vmlinux (gdb) x/20i 0xffffffc000812340这样能看到这个PC落在哪条指令上从而判断当时大概是死循环里、异常分支里还是在等待某个硬件状态的轮询里。我遇到过不少PC落在wfe等待指令上的案例看起来像死循环实际是等待中断唤醒问题核心就变成“这个中断为什么一直不来”。3.3 用expdb和串口日志交叉定位关键现场有了PC和调用栈下一步是把expdb里的信息和串口日志做交叉比对。读取expdb分区的姿势比较多最简单的方法是通过root shell把分区直接拷贝出来adb root adb shell dd if/dev/block/platform/bootdevice/by-name/expdb of/data/local/tmp/expdb.bin adb pull /data/local/tmp/expdb.bin拿到expdb.bin后用MTK配套的解析工具常见的是FRP或DataLog工具打开也能直接用文本方式查看其中保存的几个关键寄存器字段。解析工具不是每家都有如果没有可以退而求其次用串口日志里的最后几十行来还原现场。串口的优势在于它记录了完整的时间线和驱动打印expdb的优势在于不受系统卡死影响。两者结合往往能画出比单一数据源更完整的事故现场图。举个我自己的案例某外设DMA传输出bugexpdb显示CPU3的PC在一个DMA等待函数里但单看这个位置完全看不出问题。后来去翻串口日志发现最后十几行是同一个DMA中断被连续触发了上百次而驱动的状态机没有正确切换。把这两个信息放在一起根因立刻清晰中断风暴导致某个CPU上下文一直出不来最终被RGU识别为超时。所以我的习惯是每次拿到expdb都一定花时间整理最后一屏串口log交叉比对后再下结论。4. 高频挂死场景与排查思路实录4.1 中断风暴日志里全是同一条中断中断风暴是MTK平台挂死场景里出现频率最高的原因之一。症状非常有辨识度系统的hardlockup检测先触发日志周期性打印某个CPU卡在中断上下文随后RGU超时复位。PC地址通常落在同一个中断处理函数里而且你会在.last最后一段日志里看到同一个中断号被反复打印。排查的第一步是看/proc/interrupts对比挂死前后各中断号的触发次数增长趋势。如果某个中断的触发次数在几秒内暴涨到几十万次基本可以确定中断配置有问题。常见原因不外乎三种中断请求线被恒拉低/拉高导致沿触发不断置起设备驱动没有正确清除中断挂起位还有一种是ISR里做了耗时太长的I2C/SPI读操作导致中断还没处理完新中断又来了。修复方向上硬件修改通常是调整外设的电气逻辑软件修改则多半在ISR入口加防抖、检查中断状态位是否满足条件再执行后续操作。调试这种问题的关键技巧是在ISR最开始加一个专用计数器每次进入加一并把当前的计数和进入时间点通过trace保存下来。这样即使系统最终挂死最后一段日志也能清楚还原中断被触发的节奏。4.2 死锁与锁持有时间过长死锁和中断风暴的表现完全不同它往往是安静的。系统还在运行其他任务可能还能响应但某个关键路径被永久卡住比如文件系统、内存管理最终引发连锁反应。这时候你看到的可能是hung task日志某个任务的调用栈停留在等待锁的__mutex_lock_slowpath或者wait_for_completion上。排查死锁问题我强烈建议直接依赖内核的lockdep机制。只要把CONFIG_PROVE_LOCKING打开内核会在每次锁操作时检查锁的依赖图。AB-BA死锁这种经典问题lockdep可以在第一次出现时就打印出异常调用栈准确指出两把锁的获取顺序冲突点。如果项目里的内核没有开lockdep那么在稳定性测试前就要额外注意跑挂死问题复现时把smp_affinity、CPU调频这些干扰因素先关闭尽量稳定复现环境。也有一种比较难缠的“假死锁”某个驱动在关中断状态下持有锁后去等待硬件但硬件因为时序问题再也给不了回应锁被无限期保持。这种问题lockdep并不会报死锁因为你实际上只有一个持锁者。我的排查习惯是用echo t /proc/sysrq-trigger导出所有任务栈找到那个长期处于D状态的任务然后去翻它持锁后最后访问的设备寄存器。D状态说明该任务已经完成锁获取只是在等待某个硬件条件问题的根子往往在硬件状态没有正确恢复。4.3 内存踩踏与内核态死循环内存踩踏导致的挂死是最难从表象判断的。这类问题有时候表现成随机panic有时候表现成完全看不出关联的挂死重启。最典型的一种是某驱动用DMA写到缓冲区时越界把内核某个关键结构体给覆盖了系统带伤运行一会儿后出现异常行为随后进入hang_detect兜底复位。这类问题的排查第一步要做的是缩小范围。开KASAN如果平台支持能在内存访问越界时第一时间报出精确的读写地址和调用栈这是效率最高的手段。其次是打开CONFIG_PANIC_ON_OOPS让内核在第一次oops时就主动panic而不是继续带伤运行避免后续的现场信息被干扰。page_owner也可以开用于后续分析内存分配与释放的归属。另一种情况是内核态死循环典型的如某驱动在轮询硬件状态时没有添加超时退出逻辑状态寄存器异常后永远等不到预期值于是CPU就在一个while里空转。这种问题在expdb里看到的是PC始终落在一个很小的指令区间内并且是纯计算指令或ldr/cmp/b.ne的组合。修复思路是给所有硬件状态轮询都加上超时上限并且超时后打印寄存器现场退出而不是死等。5. 常见问题与排查技巧速查5.1 一张表快速对应“现象-原因-动作”调试排障的时候一张精炼的对照表比什么教程都好用。按照我在MTK平台上的经验最常见的几类挂死表现和应对思路整理如下现象可能原因优先排查动作重启后日志无panic只有WDT timeout硬件看门狗超时常见于驱动死循环、中断风暴查复位原因寄存器抓expdb看PC/最后日志某个任务长时间D状态后触发hung task死锁、持锁等待硬件echo t /proc/sysrq-trigger看调用栈开lockdepCPU占用100%但系统不挂用户态或内核态死循环top定位进程perf采样查PC是否落在固定区间随机偶发重启PC每次都不一样内存踩踏、UAF、DMA越界开KASAN、开PANIC_ON_OOPS按内存问题思路排查驱动加载后必现挂死中断配置错误、设备树问题检查interrupts属性单步验证ISR查dmesg早期日志这张表不追求覆盖所有可能性但足够应付大多数偶发或者必现的挂死问题。实际操作中一张纸、一台串口线、一条adb命令比一整套高大上的调试平台管用得多。5.2 调试环境的几个关键开关调试环境的好坏直接决定问题排查效率这里有几个开关是我跑稳定性之前必检查的kernel.hung_task_panic1让hung task直接panic而不是只打日志。这样能在现场还热的时候立即留证据。kernel.softlockup_panic1和kernel.unknown_nmi_panic1软锁检测和未知NMI触发后主动panic防止带伤运行。CONFIG_PANIC_ON_OOPSy第一次oops就panic避免后续状态干扰。CONFIG_PROVE_LOCKINGy刷死锁问题专用产品有性能开销但排查价值极高。expdb完整保留确保量产版本分区没有被错误格式化同时确认复位原因能写进分区。这些开关不一定每个项目都适合无条件打开比如lockdep在量产机上开着会影响性能所以我的习惯是量产版跑兼容性和压力测试debug版专门跑挂死复现。每次开测前把开关状态和固件hash记录进测试报告这样复测时能排除“环境差异导致结果不同”的干扰。6. 写在最后的调试心得干MTK系统稳定性调试这几年我最大的体会是不要一上来就对着PC地址死磕先花三分钟把复位路径梳理清楚。很多问题看似随机实际上有很强的规律性比如“某个外设跑完一次完整读写后几分钟内必挂”这种线索价值千金比任何日志都直观。现场信息不会骗人但工具解析会骗人。vmlinux版本不对、KASLR偏移没校准、expdb解析工具版本过旧都可能把一个正常地址翻译成完全无关的函数。我吃过这个亏所以现在每次接手一个问题第一件事就是确认固件hash和符号文件严格对应。最后分享一个“土办法”如果你怀疑某个驱动存在死循环但没有硬件调试器在手边可以在怀疑的循环体里加一个计数器定期把这个计数写入一块不会被覆盖的寄存器或者专用日志节点通过串口观察它是否还在增长。这个办法虽然笨但往往能在没有GDB、没有完整符号的条件下把问题范围压缩到很小的区间。调试是个手艺活工具是辅助真正起决定作用的还是分析和还原现场的能力。