4、喂狗策略:单点喂狗 vs 多点喂狗,任务调度中的喂狗点设计,为什么说在中断里喂狗是危险的?
看门狗这东西,说白了就是系统最后的「救命稻草」。但怎么喂这根稻草,学问可大了。我见过太多工程师,觉得喂狗嘛,定时清一下计数器就行了。结果呢?系统该挂还是挂,看门狗形同虚设。
今天咱们就聊聊喂狗策略。这章内容,是我在几个医疗设备项目中踩坑踩出来的经验。嗯,咱们一个一个说。
4.1 单点喂狗 vs 多点喂狗
先看两种最基础的喂狗方式。
单点喂狗,就是在主循环的某个固定位置,放一个喂狗指令。比如这样:
void main_loop(void)
{
while(1)
{
// 喂狗点
watchdog_feed();
// 任务A
task_a();
// 任务B
task_b();
// 任务C
task_c();
}
}
这种方式简单粗暴。但问题也很明显——如果任务A卡死了,看门狗根本不知道。因为喂狗指令在任务A之前就执行了。你想想看,任务A死锁了,但看门狗还在被定时清零,系统永远不会复位。这叫什么?这叫「假活」。
我在一个输液泵项目里就吃过这个亏。当时用的是单点喂狗,结果某个传感器读取任务因为I2C总线锁死而卡住。系统表面上看还在运行,实际上已经停止输液了。幸亏测试阶段发现了,不然...嗯,后果不敢想。
多点喂狗就不一样了。它在每个关键任务之后都放一个喂狗点:
void main_loop(void)
{
while(1)
{
// 任务A
task_a();
watchdog_feed(); // 喂狗点1
// 任务B
task_b();
watchdog_feed(); // 喂狗点2
// 任务C
task_c();
watchdog_feed(); // 喂狗点3
}
}
这样,任何一个任务卡死,看门狗都会在超时后触发复位。我个人习惯用这种方式,尤其是在医疗设备这种高可靠性场景下。
但多点喂狗也有代价——代码里到处都是喂狗指令,看着乱。而且喂狗次数多了,会稍微增加CPU开销。不过跟安全性比起来,这点代价完全可以接受。
核心原则:喂狗点的设计,要能覆盖所有关键执行路径。任何一个可能导致系统「假死」的路径,都必须有对应的喂狗点。
4.2 任务调度中的喂狗点设计
在RTOS环境下,喂狗策略就更讲究了。你不能简单地在每个任务里都喂狗,那样看门狗永远超时不了。
我建议的做法是:只在一个高优先级的监控任务里喂狗。这个任务负责检查其他任务的心跳。
看个例子:
void watchdog_monitor_task(void *param)
{
while(1)
{
// 检查所有关键任务的心跳
if(task_a_heartbeat_ok() &&
task_b_heartbeat_ok() &&
task_c_heartbeat_ok())
{
watchdog_feed();
}
else
{
// 某个任务挂了,不喂狗,等待复位
// 或者执行安全关断
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
每个关键任务需要定期更新自己的心跳标志:
void task_a(void *param)
{
while(1)
{
// 任务主体
do_something();
// 更新心跳
task_a_heartbeat = 1;
vTaskDelay(pdMS_TO_TICKS(50));
}
}
这种设计的好处是:喂狗点集中,但监控范围覆盖所有任务。任何一个任务死掉,心跳标志不再更新,监控任务就会停止喂狗,系统复位。
小技巧:我习惯给每个任务分配一个「超时阈值」。比如任务A每50ms更新一次心跳,那监控任务如果100ms没看到更新,就判定任务A挂了。这个阈值要留有余量,但也不能太大。
4.3 为什么说在中断里喂狗是危险的?
这个问题,我每次培训都要重点强调。很多新手觉得,在定时器中断里喂狗多方便啊,定时准确,代码也简单。但这是最危险的做法,没有之一。
为什么?因为中断的优先级最高。如果主程序已经死锁了,但定时器中断还在正常触发,看门狗就会被不断清零。系统永远不会复位。你想想看,这跟没装看门狗有什么区别?
我曾经在一个监护仪项目里接手过这样的代码。前任工程师在SysTick中断里喂狗,结果系统因为内存泄漏导致主循环卡死,但看门狗纹丝不动。设备死机了整整8小时,护士站那边完全收不到数据。嗯,后来那个工程师被调去维护老项目了。
看个反面教材:
// 危险!不要在中断里喂狗!
void SysTick_Handler(void)
{
watchdog_feed(); // 主程序死锁了,这里还在喂狗
// 其他中断处理...
}
正确的做法是:喂狗指令只能放在任务级别的代码中。中断里最多做一些标志位的更新,绝对不能直接喂狗。
警告:如果你必须在中断里处理喂狗相关逻辑,那至少要做到:中断只设置一个标志位,由主循环或监控任务根据这个标志位来决定是否喂狗。但说实话,我连这个都不推荐。喂狗这件事,就应该交给任务去管。
4.4 喂狗策略的避坑指南
最后,分享几个我踩过的坑:
- 喂狗周期太短:有些工程师把看门狗超时时间设得很短,比如10ms。结果系统稍微忙一点就复位了。我建议至少留出2-3倍的任务周期余量。
- 喂狗点太密集:在循环的每一行都喂狗,反而会掩盖问题。喂狗点应该放在「关键路径节点」上,而不是每个语句后面。
- 忘记考虑低功耗模式:系统进入休眠时,看门狗还在跑。这时候需要特殊处理,要么在休眠前喂一次狗,要么暂时关闭看门狗。
- 多核系统的喂狗同步:如果是多核MCU,每个核都要有独立的喂狗机制。不能指望一个核替所有核喂狗。
嗯,喂狗这件事,看似简单,实则处处是坑。但只要你理解了「看门狗是用来检测系统是否活着」这个本质,设计策略就不会跑偏。
记住一句话:看门狗不是用来保系统不死的,而是用来确保系统死了能活过来。这个「死」的检测,全靠喂狗策略来定义。