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,每个核都要有独立的喂狗机制。不能指望一个核替所有核喂狗。

嗯,喂狗这件事,看似简单,实则处处是坑。但只要你理解了「看门狗是用来检测系统是否活着」这个本质,设计策略就不会跑偏。

记住一句话:看门狗不是用来保系统不死的,而是用来确保系统死了能活过来。这个「死」的检测,全靠喂狗策略来定义。