任务级异常处理:任务异常捕获与恢复
任务级异常处理,说白了就是给每个任务上个“保险”。
我在项目中遇到过不少次,一个任务因为野指针崩溃,结果整个系统都跟着遭殃。后来我学乖了——每个关键任务都得配异常捕获机制。今天咱们就聊聊VxWorks里怎么玩转任务异常捕获、恢复和日志记录。
任务异常捕获:taskExcHookSet
VxWorks提供了一套很实用的API——taskExcHookSet。它的作用很简单:给指定任务注册一个异常处理回调函数。
#include <vxWorks.h>
#include <taskLib.h>
#include <excLib.h>
STATUS taskExcHookSet
(
TASK_ID tid, /* 目标任务ID */
FUNCPTR excHook, /* 异常处理函数指针 */
int arg1, /* 参数1 */
int arg2 /* 参数2 */
);
嗯,这里要注意:excHook会在异常发生时被调用。它运行在异常上下文中,所以不能做太多复杂操作。
核心要点:异常钩子函数执行时,系统处于异常模式。你不能在里面调用阻塞API,也不能申请大块内存。我建议只做三件事:记录现场、保存关键数据、触发任务恢复流程。
举个例子,我曾经在一个无人机飞控项目里这么用:
void myTaskExcHook(int taskId, int excType, REG_SET *pRegs)
{
/* 记录异常类型和任务ID */
logMsg("Task 0x%x crashed! Exception type: %d\n",
taskId, excType, 0, 0, 0, 0);
/* 保存关键寄存器现场 */
bcopy(pRegs, &savedRegs, sizeof(REG_SET));
/* 触发任务恢复 */
taskRestart(taskId);
}
个人经验:我习惯在异常钩子里只做最轻量级的操作。曾经有个同事在钩子里调了printf,结果死锁了——因为printf本身可能触发异常。记住:钩子函数越短越好。
任务异常恢复机制
捕获异常只是第一步。怎么恢复?这才是真功夫。
VxWorks里常用的恢复策略有三种:
| 恢复策略 | 适用场景 | 风险等级 |
|---|---|---|
| taskRestart | 任务状态可重建 | 低 |
| 任务重置+状态恢复 | 需要保留部分上下文 | 中 |
| 任务删除+重建 | 任务资源严重损坏 | 高 |
taskRestart是最简单粗暴的方式。它会重新执行任务的入口函数,所有局部变量都会重新初始化。我在项目中遇到的大部分异常,用这个就够了。
但有些场景不行。比如任务持有互斥锁时崩溃了,直接restart会导致锁永远不释放。这时候就需要更精细的恢复策略。
避坑指南:我曾经在一个通信协议栈项目里,任务崩溃时正握着信号量。直接taskRestart后,其他任务永远等不到这个信号量。后来我改成在异常钩子里先释放所有资源,再restart。记住:恢复前一定要清理资源。
更高级的做法是“状态机恢复”。我会在任务里维护一个状态变量:
typedef enum {
STATE_INIT,
STATE_RUNNING,
STATE_ERROR,
STATE_RECOVERING
} TaskState;
void myTask(void)
{
TaskState state = STATE_INIT;
while(1)
{
switch(state)
{
case STATE_INIT:
/* 初始化资源 */
state = STATE_RUNNING;
break;
case STATE_RUNNING:
/* 正常业务逻辑 */
break;
case STATE_ERROR:
/* 异常恢复逻辑 */
cleanUpResources();
state = STATE_RECOVERING;
break;
case STATE_RECOVERING:
/* 重新初始化 */
reInitModule();
state = STATE_RUNNING;
break;
}
}
}
这样,异常发生后,任务可以从STATE_ERROR状态继续执行,而不是从头开始。你想想看,这比直接restart优雅多了。
任务异常日志记录
日志记录是调试的命根子。没有日志,异常恢复就是盲人摸象。
我推荐用VxWorks的logMsg函数。它比printf安全,不会因为异常而阻塞:
void excLogHandler(int taskId, int excType, REG_SET *pRegs)
{
/* 记录异常基本信息 */
logMsg("=== TASK EXCEPTION ===\n", 0, 0, 0, 0, 0);
logMsg("Task ID: 0x%x\n", taskId, 0, 0, 0, 0, 0);
logMsg("Exception Type: %d\n", excType, 0, 0, 0, 0, 0);
/* 记录程序计数器 */
logMsg("PC: 0x%x\n", pRegs->pc, 0, 0, 0, 0, 0);
/* 记录堆栈指针 */
logMsg("SP: 0x%x\n", pRegs->sp, 0, 0, 0, 0, 0);
/* 记录关键寄存器 */
logMsg("LR: 0x%x\n", pRegs->lr, 0, 0, 0, 0, 0);
}
日志记录原则:
- 记录异常类型和任务ID——这是定位问题的起点
- 记录程序计数器(PC)——知道崩溃在哪行代码
- 记录堆栈指针(SP)——方便回溯调用栈
- 记录关键寄存器——分析崩溃原因
- 加上时间戳——了解异常发生的时间点
我个人习惯在日志里加个“异常编号”。每次异常都递增这个编号,方便追踪:
static int excCounter = 0;
void excLogWithCounter(int taskId, int excType)
{
excCounter++;
logMsg("[EXC#%d] Task 0x%x, Type %d\n",
excCounter, taskId, excType, 0, 0, 0);
}
为什么这么做?有一次我在现场调试,系统反复崩溃。但日志里只有“Task crashed”这样的信息,根本看不出是第几次崩溃。加上计数器后,我立刻发现每次崩溃前都有特定的操作序列——问题很快就定位了。
小技巧:日志缓冲区要够大。我一般设成至少4KB。太小的话,连续崩溃时前面的日志会被覆盖。另外,建议把日志输出到非易失存储,这样系统重启后还能查看。
最后说一句:异常处理不是银弹。它只能帮你从崩溃中恢复,但不能替代代码审查和测试。我见过太多团队把异常恢复当成万能药,结果系统越补越乱。记住:最好的异常处理,是让异常不发生。
好了,任务级异常处理就聊到这儿。下一章咱们聊聊系统级异常——那才是真正考验功底的地方。