第3章:VxWorks内核基础——任务(Task)概念、任务状态机与任务控制块

好,咱们今天聊聊VxWorks里最核心的东西——任务。说白了,任务就是VxWorks里能独立运行的一段代码,一个执行流。你想想看,一个实时系统要同时干好几件事,比如一边采集传感器数据,一边控制电机,还得跟人机界面交互。这怎么做到?就是靠任务。

我刚开始接触VxWorks时,有个误区:以为任务就是函数调用。后来被坑过一次才明白,任务是有“生命”的,它有自己的状态、自己的栈、自己的优先级。嗯,咱们今天就把这事彻底说清楚。

3.1 任务到底是什么?

任务(Task),在VxWorks里是调度的基本单位。每个任务都对应一个独立的执行线程,拥有自己的上下文。你可以把它想象成一个“小工人”,专门负责干某一类活。

举个例子,一个无人机飞控系统里,我通常会创建这么几个任务:

  • 传感器采集任务:负责读取IMU、GPS数据,优先级最高
  • 姿态解算任务:根据传感器数据算出当前姿态,优先级次高
  • 控制输出任务:根据目标姿态和当前姿态,输出PWM给电机
  • 通信任务:跟地面站收发数据,优先级最低

每个任务各司其职,互不干扰。这就是多任务的好处。

核心要点:任务不是函数,函数是被调用的,任务是被调度的。任务有独立的栈空间和TCB(任务控制块),函数没有。

3.2 任务状态机——任务的“生老病死”

任务不是一直运行的。它会在几个状态之间来回切换。VxWorks的任务状态机,我习惯把它分成四个核心状态:

状态 英文 说明
就绪 READY 任务已准备好运行,只等CPU分配给它
阻塞 PEND 任务在等待某个资源(信号量、消息队列等)
挂起 SUSPEND 任务被强制暂停,不参与调度
延迟 DELAY 任务主动休眠一段时间

这几个状态之间怎么切换?我画个简单的逻辑:

  • 任务创建后,进入就绪状态
  • 调度器选中它,它就运行(运行态其实可以看作就绪态的一个子集)
  • 运行中如果等信号量,就进入阻塞状态
  • 运行中调用taskDelay(),就进入延迟状态
  • 其他任务调用taskSuspend(),它就进入挂起状态

我的经验:调试时最常用的就是挂起状态。我曾经遇到一个任务跑飞了,整个系统卡死。我通过taskSuspend()把可疑任务一个个挂起,最后定位到是某个中断服务程序里调用了阻塞函数。嗯,这个坑大家以后要注意——ISR里千万别调用pend类的函数。

3.3 任务控制块(TCB)——任务的“身份证”

每个任务都有一个TCB。TCB里存了什么?说白了,就是任务的所有“个人信息”。VxWorks的TCB结构体定义在taskLib.h里,我挑几个关键字段说说:

typedef struct windTcb {
    OBJ_CORE  objCore;       // 对象核心,用于对象管理
    WIND_TCB  *pTcbNext;     // 链表指针,指向下一个TCB
    WIND_TCB  *pTcbPrev;     // 链表指针,指向上一个TCB
    UINT16    status;        // 任务状态(就绪、阻塞等)
    UINT16    priority;      // 任务优先级(0-255,0最高)
    FUNCPTR   entry;         // 任务入口函数地址
    char      name[WIND_MAX_NAME]; // 任务名称
    void      *pStackBase;   // 栈基址
    void      *pStackLimit;  // 栈限制地址
    UINT      stackSize;     // 栈大小
    ...
} WIND_TCB;

你看,TCB里记录了任务的优先级、状态、栈信息、入口函数等。调度器就是靠这些信息来决定“下一个该谁跑”。

避坑指南:我曾经接手过一个项目,系统运行几天后莫名其妙死机。查了三天,最后发现是某个任务的栈溢出了,把相邻任务的TCB给覆盖了。从那以后,我每个任务都会预留至少20%的栈余量,并且在调试阶段打开栈溢出检测。

3.4 任务创建与删除——实战代码

光说不练假把式。咱们直接看代码。创建一个任务,用taskSpawn()函数:

#include <vxWorks.h>
#include <taskLib.h>

/* 任务入口函数 */
void myTask(int param1, int param2)
{
    while (1) {
        printf("Task running, param1=%d, param2=%d\n", param1, param2);
        taskDelay(100);  /* 延迟100个tick */
    }
}

/* 创建任务 */
STATUS createMyTask(void)
{
    int taskId;
    
    taskId = taskSpawn("tMyTask",      /* 任务名 */
                       100,            /* 优先级,100 */
                       0,              /* 选项,默认0 */
                       4096,           /* 栈大小,4KB */
                       (FUNCPTR)myTask, /* 入口函数 */
                       10,             /* 参数1 */
                       20,             /* 参数2 */
                       0, 0, 0, 0, 0, 0, 0); /* 剩余参数填0 */
    
    if (taskId == ERROR) {
        printf("Task spawn failed!\n");
        return ERROR;
    }
    
    printf("Task created, ID=%d\n", taskId);
    return OK;
}

删除任务用taskDelete()

STATUS deleteMyTask(int taskId)
{
    if (taskDelete(taskId) == ERROR) {
        printf("Delete task %d failed!\n", taskId);
        return ERROR;
    }
    return OK;
}

个人习惯:我一般不用taskDelete()直接删任务,而是用信号量或标志位让任务自己退出。为什么呢?因为taskDelete()如果删了一个正在使用共享资源的任务,可能导致死锁。我曾经在项目里吃过这个亏,后来就改成“优雅退出”的方式了。

3.5 任务状态查询——调试利器

调试时最常用的命令是i(显示所有任务信息)和ti(显示指定任务信息)。在VxWorks Shell里输入:

-> i

  NAME       ENTRY       TID    PRI   STATUS      PC       SP     ERRNO
  ---------- ----------- ------ ---- ----------- -------- ------- -----
  tMyTask    myTask      3f8a00 100   READY       3f8a10   3f8a00 0
  tExcTask   excTask     3f8b00 0     PEND        3f8b10   3f8b00 0
  tLogTask   logTask     3f8c00 0     PEND        3f8c10   3f8c00 0
  tShell     shellTask   3f8d00 1     READY       3f8d10   3f8d00 0

你看,STATUS列显示的就是任务当前状态。如果任务卡住了,先看看它是不是在PEND状态,等什么资源。如果是SUSPEND状态,看看是谁把它挂起了。

嗯,这里要注意:任务状态是动态变化的。你查一次是一个样,过一会儿可能就变了。所以调试时我习惯用watchdog定时打印任务状态,这样能抓到状态切换的瞬间。

3.6 本章小结

咱们今天把任务的三个核心概念捋了一遍:

  • 任务:VxWorks里独立执行的线程,有独立栈和TCB
  • 任务状态机:就绪、阻塞、挂起、延迟四个状态,调度器负责切换
  • 任务控制块:任务的“身份证”,记录了优先级、栈、入口等关键信息

下一章咱们聊聊任务调度——VxWorks是怎么决定“下一个该谁跑的”。这个很有意思,因为VxWorks用的是基于优先级的抢占式调度,跟Linux的CFS完全不一样。到时候我会拿一个实际项目里的调度问题来举例,保证让你印象深刻。