第二课:看门狗定时器实战
好,咱们直接进入正题。看门狗定时器(Watchdog Timer)在VxWorks里,说白了就是一个「倒计时炸弹」。你必须在它爆炸前喂狗,否则系统就重启。但今天我要讲的,不是那个系统级的看门狗,而是VxWorks提供的一个更灵活的工具——看门狗定时器库(wdLib)。
我个人习惯把它叫做「软看门狗」。它不重启系统,而是到期后执行一个你指定的回调函数。这个特性,用来做周期性任务调度器,简直不要太顺手。
2.1 创建周期性任务调度器
先说说思路。你想让一个任务每隔100ms执行一次,怎么做?
最笨的办法是在任务里写个死循环,调用taskDelay()。但这样有个问题:如果任务被阻塞了,或者优先级被抢占了,周期就不准了。看门狗定时器不一样,它依赖系统时钟中断,精度高得多。
来看代码:
#include <vxWorks.h>
#include <wdLib.h>
#include <sysLib.h>
WDOG_ID wdId;
int counter = 0;
void myPeriodicTask(void)
{
counter++;
printf("第 %d 次执行\n", counter);
// 重新启动看门狗,实现周期性
if (wdStart(wdId, sysClkRateGet() / 10, (FUNCPTR)myPeriodicTask, 0) != OK)
{
printf("看门狗重启失败\n");
}
}
void initScheduler(void)
{
wdId = wdCreate();
if (wdId == NULL)
{
printf("创建看门狗失败\n");
return;
}
// 启动,100ms后执行第一次
wdStart(wdId, sysClkRateGet() / 10, (FUNCPTR)myPeriodicTask, 0);
}
这里有个关键点:wdStart()的第二个参数是tick数。我用了sysClkRateGet() / 10,假设系统时钟是1000Hz,那就是100个tick,对应100ms。你想想看,如果系统时钟改了,这个代码依然能自适应,是不是很优雅?
2.2 实现超时处理回调函数
回调函数的设计,其实挺有讲究的。我见过不少新手,把回调函数写得跟普通函数一样,结果踩了坑。
看门狗回调的原型是:
void callback(WDOG_ID wdId, int parameter);
注意,它只有一个参数,就是你在wdStart()里传的那个。如果你想传多个参数,怎么办?
嗯,这里有个土办法。我习惯用一个全局结构体,把需要的数据都塞进去,然后传结构体的指针:
typedef struct {
int taskId;
char taskName[32];
int timeoutCount;
} TaskInfo;
TaskInfo g_taskInfo;
void timeoutHandler(WDOG_ID wdId, int parameter)
{
TaskInfo *pInfo = (TaskInfo *)parameter;
// 检查超时次数
pInfo->timeoutCount++;
if (pInfo->timeoutCount > 3)
{
printf("任务 %s 连续超时 %d 次,需要处理!\n",
pInfo->taskName, pInfo->timeoutCount);
// 这里可以触发错误处理逻辑
}
}
我曾经在一个项目中,用这个机制实现了「心跳检测」。每个任务定期喂狗,如果某个任务连续3次没喂,我就认为它挂了,然后自动重启它。这个方案后来成了那个产品的标配功能。
2.3 多定时器并发管理
一个看门狗不够用?那就创建多个。VxWorks支持同时创建多个看门狗定时器,只要系统内存够。
但问题来了:多个定时器同时跑,你怎么管理它们?
我建议用数组或者链表来管理:
#define MAX_TIMERS 10
typedef struct {
WDOG_ID wdId;
BOOL inUse;
char name[16];
int interval; // 周期,单位tick
} TimerEntry;
TimerEntry timerPool[MAX_TIMERS];
STATUS createTimer(char *name, int interval, FUNCPTR callback, int arg)
{
int i;
for (i = 0; i < MAX_TIMERS; i++)
{
if (!timerPool[i].inUse)
{
timerPool[i].wdId = wdCreate();
if (timerPool[i].wdId == NULL)
return ERROR;
timerPool[i].inUse = TRUE;
strncpy(timerPool[i].name, name, 15);
timerPool[i].interval = interval;
wdStart(timerPool[i].wdId, interval, callback, arg);
return OK;
}
}
return ERROR; // 池子满了
}
你看,这样管理起来就清晰多了。每个定时器有自己的名字、周期和状态。调试的时候,打印一下这个数组,哪个定时器在跑、哪个是空闲的,一目了然。
2.4 定时器资源泄漏排查
这是个大坑。我见过太多人,创建了看门狗不删除,结果系统跑着跑着就崩了。
为什么会这样?因为每个wdCreate()都会分配内存。如果你在循环里不断创建,又不删除,内存迟早被吃光。
排查资源泄漏,我有三板斧:
- 用
wdShow()查看当前看门狗数量——VxWorks Shell里直接敲这个命令,能看到当前有多少个看门狗在活动。 - 代码审查时重点检查
wdCreate()和wdDelete()的配对——我习惯在创建时记录日志,删除时也记录日志,跑一段时间后grep一下,看数量是否匹配。 - 使用内存检测工具——比如Wind River的MemScope,或者自己写一个简单的内存分配计数器。
我曾经接手过一个项目,系统运行72小时后必死。排查了三天,最后发现是一个任务在异常处理分支里忘了调用wdDelete()。每次异常发生,就泄漏一个看门狗。72小时,刚好把内存耗光。
那之后,我给自己定了个规矩:所有看门狗的创建和删除,必须成对出现,而且要在同一个函数层级。比如在init()里创建,就在cleanup()里删除,绝不跨函数。
wdDelete()删除自己,结果系统直接挂掉。原因是看门狗回调执行时,定时器本身还在活跃状态,你删它等于「自杀」。正确的做法是:在回调里设置一个标志,让外部任务来删除。
好了,这一课的内容就这些。总结一下:看门狗定时器是个好东西,但用不好就是定时炸弹。记住三点——回调要轻量、管理要规范、删除要彻底。下一课,咱们聊聊POSIX定时器,那又是另一番天地了。