4、RTOS调试技巧:任务堆栈溢出检测、优先级反转排查、死锁定位方法
RTOS 调试,说难也难,说简单也简单。
我做了十几年航天嵌入式,最怕的不是硬件出问题,而是软件跑着跑着突然挂了。你想想看,卫星在天上,你没法拿 JTAG 去连。所以地面调试阶段,必须把 RTOS 的这几个坑填平。
今天咱们就聊三个最要命的问题:堆栈溢出、优先级反转、死锁。这三个问题,我每个都吃过亏。
4.1 任务堆栈溢出检测
堆栈溢出,说白了就是任务用的栈空间不够了。
为什么会这样?
很多新手喜欢把栈设得刚刚好。比如函数里有个 256 字节的局部数组,他就给任务分配 512 字节栈。嗯,这很危险。中断嵌套、函数调用压栈、局部变量,这些都会吃栈空间。
我个人的习惯是:先给大,再优化。
比如一个任务,我初步估算需要 1KB 栈,那我先给 4KB。跑一段时间后,通过 RTOS 自带的栈使用统计功能,看看峰值用了多少,再砍到 1.5 倍左右。
常用的检测方法有几种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 栈填充标记法 | 初始化时用固定模式填充栈空间,运行时检查边界 | 所有 RTOS |
| 硬件 MPU 保护 | 利用 MPU 给栈区域设置访问权限,溢出即触发异常 | 支持 MPU 的 MCU |
| 栈指针监控 | 周期性检查当前 SP 是否接近栈底 | 轻量级调试 |
我在项目中遇到过最典型的一次:一个遥测采集任务,平时跑得好好的,但一旦数据量暴增,系统就死机。查了三天,最后发现是栈溢出了。那个任务里有个临时缓冲区,平时只用到 100 字节,但极端情况下会用到 300 字节。栈设了 256,直接踩坏了相邻任务的信号量。
uxTaskGetStackHighWaterMark() 查看栈剩余空间。我建议每个任务都留一个调试接口,定期打印这个值。
4.2 优先级反转排查
优先级反转,这是个经典问题。
简单说:高优先级任务在等一个资源,而这个资源被低优先级任务占着。低优先级任务又被中优先级任务抢占了。结果高优先级任务反而被中优先级任务“卡住”了。
你想想看,这合理吗?
不合理。但现实中经常发生。
排查优先级反转,我一般分三步走:
- 看任务状态:用调试器或日志,看看高优先级任务在等什么。如果它在等一个信号量或互斥量,那就要查谁持有这个资源。
- 看持有者优先级:如果持有资源的任务优先级很低,而且它不在运行状态,那大概率是被中优先级任务抢占了。
- 看中优先级任务:看看有没有中优先级的任务在一直运行,不让低优先级任务释放资源。
我曾经在一个飞控项目中遇到过这个问题。一个高优先级的控制任务(优先级 10)要访问共享数据,低优先级的采集任务(优先级 2)正拿着锁。结果中间有个日志输出任务(优先级 5)一直在打印数据,占着 CPU 不放。控制任务死活拿不到锁,导致控制周期超时。
解决办法?
最直接的是用优先级继承协议。当低优先级任务持有高优先级任务需要的资源时,临时把低优先级任务的优先级提升到高优先级任务的级别。这样它就能尽快释放资源。
xSemaphoreCreateMutex() 创建的互斥量自带优先级继承。不要用二值信号量来做互斥,它不解决优先级反转。
4.3 死锁定位方法
死锁,比优先级反转更恶心。
两个任务互相等对方释放资源,谁也动不了。系统就像冻住了一样。
死锁发生的四个必要条件:
- 互斥:资源一次只能被一个任务占用
- 持有并等待:任务拿着一个资源,还在等另一个
- 不可剥夺:资源不能被强行拿走
- 循环等待:任务 A 等 B,B 等 A
定位死锁,我常用的方法:
- 看任务列表:所有任务都处于阻塞态,没有任务在运行。这是死锁的典型特征。
- 看资源持有关系:每个任务在等什么资源?谁拿着这个资源?画个图就清楚了。
- 看超时机制:如果所有等待都设了超时,那死锁会自己解开。但如果没有超时,那就永远卡死。
我记得有一次调试一个数据分发模块。任务 A 拿着锁 1,等锁 2。任务 B 拿着锁 2,等锁 1。两个任务都设了无限等待。系统跑起来不到 10 秒就死锁了。
我当时怎么查的?
在调试器里暂停系统,看每个任务的当前状态。发现两个任务都在 pend 状态。然后看它们等的是哪个信号量,再查信号量的持有者。一下子就定位到了。
xSemaphoreTake()。中断里不能阻塞等待,这是死锁的温床。记住:中断里只能用 xSemaphoreGiveFromISR(),不能 Take。
预防死锁,我有几个原则:
- 统一锁的顺序:所有任务获取多个锁时,按相同的顺序获取。比如先拿锁 A,再拿锁 B。不要有的先 A 后 B,有的先 B 后 A。
- 使用超时:所有等待都设一个合理的超时时间。超时后做异常处理,而不是死等。
- 减少锁的粒度:能用原子操作解决的问题,就别用锁。能用消息队列传递数据,就别共享内存。
好了,RTOS 调试的三个核心问题就聊到这儿。
堆栈溢出、优先级反转、死锁,这三个问题在航天嵌入式里尤其致命。因为系统一旦跑起来,你很难再干预。所以地面调试阶段,一定要把这些坑填平。
下次咱们聊聊中断延迟和任务切换抖动的问题。那个也很有意思。