任务级异常处理:任务异常捕获与恢复

任务级异常处理,说白了就是给每个任务上个“保险”。

我在项目中遇到过不少次,一个任务因为野指针崩溃,结果整个系统都跟着遭殃。后来我学乖了——每个关键任务都得配异常捕获机制。今天咱们就聊聊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。太小的话,连续崩溃时前面的日志会被覆盖。另外,建议把日志输出到非易失存储,这样系统重启后还能查看。

最后说一句:异常处理不是银弹。它只能帮你从崩溃中恢复,但不能替代代码审查和测试。我见过太多团队把异常恢复当成万能药,结果系统越补越乱。记住:最好的异常处理,是让异常不发生。

好了,任务级异常处理就聊到这儿。下一章咱们聊聊系统级异常——那才是真正考验功底的地方。