看门狗任务设计:独立看门狗任务的设计模式
看门狗任务,说白了就是系统里的「最后一道防线」。
我刚开始做VxWorks项目时,总觉得看门狗就是个定时器,喂喂狗就完事了。直到有一次,系统在极端负载下死锁,看门狗却因为和业务任务绑在一起,也跟着「躺平」了。那次事故让我明白——看门狗任务必须独立,必须纯粹。
为什么需要独立看门狗任务?
你想想看,如果把喂狗操作放在业务任务里,业务任务一旦卡死,谁去喂狗?看门狗也就形同虚设了。
独立看门狗任务的核心思想就一句话:用一个最可靠的任务,去监控其他所有任务。
我个人习惯的做法是:
- 看门狗任务优先级设为系统最高(或次高)
- 看门狗任务不依赖任何业务模块
- 看门狗任务只做一件事——喂狗和超时处理
核心原则:看门狗任务必须是一个「孤岛」,不参与任何业务逻辑,不调用任何可能阻塞的API。
典型的设计模式
我在项目中常用的模式有两种,这里重点讲第一种——心跳检测模式。
心跳检测模式的工作流程:
- 每个业务任务定期向共享内存写一个「心跳时间戳」
- 看门狗任务定期扫描所有任务的心跳
- 如果某个任务的心跳超过阈值,触发超时处理
- 看门狗任务自己喂硬件看门狗
代码示例:
/* 心跳数据结构 */
typedef struct {
TASK_ID tid; /* 任务ID */
UINT64 last_heartbeat; /* 上次心跳时间 */
UINT32 timeout_ms; /* 超时阈值 */
BOOL is_alive; /* 存活标志 */
} HEARTBEAT_ENTRY;
/* 看门狗任务主循环 */
void watchdogTask(void)
{
HEARTBEAT_ENTRY *entry;
UINT64 now;
int i;
while (1) {
now = tickGet() * sysClkRateGet(); /* 获取当前时间ms */
for (i = 0; i < g_heartbeatCount; i++) {
entry = &g_heartbeatTable[i];
if ((now - entry->last_heartbeat) > entry->timeout_ms) {
/* 任务超时!触发回调 */
watchdogTimeoutHandler(entry->tid);
}
}
/* 所有任务正常,喂硬件狗 */
sysWdogFeed();
taskDelay(sysClkRateGet() / 10); /* 100ms扫描一次 */
}
}
我的经验:扫描周期不要设得太短。100ms是个不错的起点。太频繁反而增加系统负担,而且硬件看门狗通常有几百ms到几秒的超时窗口。
喂狗策略:什么时候喂?怎么喂?
喂狗不是「随便喂喂」就行的。喂早了、喂晚了、喂错了,都会出问题。
策略一:固定周期喂狗
最简单的方式。看门狗任务每隔固定时间喂一次。
void watchdogTask(void)
{
while (1) {
sysWdogFeed(); /* 喂狗 */
taskDelay(50); /* 50ms喂一次 */
}
}
嗯,这里要注意:喂狗周期必须小于硬件看门狗的超时时间。一般建议喂狗周期设为超时时间的1/3到1/2。
| 硬件看门狗超时 | 推荐喂狗周期 | 安全余量 |
|---|---|---|
| 1秒 | 200-300ms | 700-800ms |
| 2秒 | 500-800ms | 1.2-1.5秒 |
| 5秒 | 1-2秒 | 3-4秒 |
策略二:条件触发喂狗
不是所有系统都适合固定周期。有些场景下,业务任务完成一个关键步骤后才允许喂狗。
我曾经在一个工业控制器项目里用过这种策略:
- 主控任务完成一次控制周期后,发送「完成信号」
- 看门狗任务收到信号才喂狗
- 如果控制周期超时,看门狗不喂狗,系统复位
避坑指南:我曾经犯过一个错误——在中断服务程序里直接喂狗。结果中断频繁触发,看门狗永远不超时,系统死锁了都没人知道。记住:喂狗操作一定要放在任务上下文中,不要在ISR里喂狗!
策略三:分级喂狗
对于复杂系统,我建议用分级策略:
- 一级喂狗:看门狗任务自身存活证明
- 二级喂狗:关键业务任务的心跳正常
- 三级喂狗:系统整体健康状态良好
只有三级都通过,才真正喂硬件看门狗。这样能最大程度避免「假活」现象。
超时处理回调函数
看门狗超时了怎么办?不是只有「复位」一条路。
我习惯把超时处理设计成回调函数,这样灵活性更高。
回调函数的设计
/* 超时处理回调类型 */
typedef void (*WDOG_TIMEOUT_CB)(TASK_ID tid, UINT32 timeout_count);
/* 默认超时处理 */
void defaultTimeoutHandler(TASK_ID tid, UINT32 timeout_count)
{
logMsg("Task 0x%x timeout! count=%d\n", tid, timeout_count);
if (timeout_count >= 3) {
/* 连续超时3次,强制重启任务 */
taskRestart(tid);
logMsg("Task 0x%x restarted\n", tid);
}
}
/* 严重超时处理 */
void fatalTimeoutHandler(TASK_ID tid, UINT32 timeout_count)
{
logMsg("FATAL: Task 0x%x dead! System reset...\n", tid);
taskDelay(sysClkRateGet()); /* 留时间打印日志 */
sysReset(); /* 系统复位 */
}
回调函数的注册机制
每个任务可以注册自己的超时回调:
STATUS registerWatchdogCallback(TASK_ID tid,
WDOG_TIMEOUT_CB cb,
UINT32 timeout_ms)
{
HEARTBEAT_ENTRY *entry;
int i;
/* 查找空闲槽位 */
for (i = 0; i < MAX_HEARTBEAT_ENTRIES; i++) {
if (g_heartbeatTable[i].tid == 0) {
entry = &g_heartbeatTable[i];
entry->tid = tid;
entry->timeout_ms = timeout_ms;
entry->cb = cb;
entry->last_heartbeat = tickGet() * sysClkRateGet();
entry->is_alive = TRUE;
return OK;
}
}
return ERROR; /* 槽位满了 */
}
我的建议:回调函数里不要做复杂操作。我曾经见过有人在回调里做文件写入,结果文件系统也卡住了。回调里只做两件事:记录日志、触发恢复动作。复杂逻辑放到单独的任务里处理。
超时处理的三种策略
| 策略 | 适用场景 | 风险 |
|---|---|---|
| 任务重启 | 单个任务偶发卡死 | 重启后状态丢失 |
| 系统复位 | 严重故障、安全关键系统 | 所有任务中断 |
| 降级运行 | 非关键功能失效 | 功能不完整 |
我个人最常用的是「先尝试任务重启,连续失败再系统复位」的策略。这样既保证了可靠性,又避免了频繁复位带来的用户体验问题。
总结一下
独立看门狗任务的设计,核心就三点:
- 独立性:看门狗任务必须独立于业务任务
- 策略性:喂狗策略要根据系统特点选择
- 灵活性:超时处理用回调机制,支持不同场景
嗯,最后说一句:看门狗不是万能的。它只能解决任务卡死的问题,解决不了逻辑错误。但用好看门狗,至少能让你的系统在出问题时「死得明白」,甚至「起死回生」。