3、VxWorks内核基础:任务管理、任务调度、任务状态、任务优先级、任务创建与删除

各位同学,咱们今天聊聊VxWorks内核最核心的东西——任务管理。说实话,我做了十几年嵌入式开发,接触过不少RTOS,但VxWorks的任务调度机制,至今仍是我觉得最干净利落的。你想想看,一个系统里跑着几十个任务,谁先执行、谁后执行、谁被踢出去睡大觉,全靠这套机制撑着。

3.1 任务到底是什么?

在VxWorks里,任务(Task)就是一段独立执行的代码流。每个任务都有自己的栈空间、程序计数器、寄存器上下文。说白了,任务就是CPU时间片的使用者。

我在项目中遇到过这样一个场景:一个数据采集系统,需要同时处理传感器数据、网络通信、人机交互。如果不用多任务,你得自己写一个超级大的状态机,维护起来简直要命。用了VxWorks的任务管理,每个功能拆成一个独立任务,清爽多了。

核心概念:VxWorks中的任务,本质上是一个函数加上它运行所需的上下文环境。任务之间通过信号量、消息队列等机制通信,互不干扰。

3.2 任务状态与状态转换

VxWorks的任务有五种基本状态。嗯,这里要注意,不同版本的VxWorks可能叫法略有差异,但本质是一样的。

状态 说明 典型场景
READY 就绪态,任务已准备好运行,等待CPU 任务刚创建,或从阻塞中恢复
RUNNING 运行态,任务正在占用CPU 当前正在执行的任务
PEND 阻塞态,任务在等待某个资源 等待信号量、消息队列、延时
SUSPEND 挂起态,任务被强制暂停 调试时手动挂起,或系统异常
DELAY 延时态,任务在等待时间到达 taskDelay()调用后

状态转换的路径是这样的:任务创建后进入READY态。调度器选中它,就变成RUNNING。如果它调用taskDelay(),就进入DELAY态。如果它等待一个还没释放的信号量,就进入PEND态。调试时你可以用taskSuspend()把它挂起来,变成SUSPEND态。

我曾经调试一个网络协议栈,发现某个任务莫名其妙不跑了。用windView一看,任务卡在PEND态,等一个永远不会来的信号量。查了半天,原来是另一个任务在释放信号量之前就崩溃了。这种问题,没有状态机分析工具,真的很难定位。

个人习惯:我建议你在每个任务的循环里加一个看门狗喂狗操作。如果任务长时间不喂狗,说明它可能卡在某个状态出不来了。这招帮我抓过不少bug。

3.3 任务调度机制

VxWorks默认使用基于优先级的抢占式调度。什么意思?就是高优先级的任务随时可以打断低优先级任务的执行。你想想看,如果紧急的中断处理任务要等一个普通任务跑完,那系统响应速度就完蛋了。

调度策略主要有两种:

  • 优先级抢占调度:每个任务有一个优先级(0-255,0最高)。高优先级任务就绪时,立即抢占低优先级任务。
  • 时间片轮转调度:同优先级的任务,按时间片轮流执行。每个任务跑完一个时间片,就换下一个。

我个人习惯,关键任务用高优先级,但不要滥用。我记得有个项目,新人把所有任务都设成优先级50,结果系统响应一塌糊涂。后来我帮他重新梳理,中断处理任务设成10,通信任务设成30,界面任务设成100,系统就稳了。

避坑指南:我曾经见过一个系统,高优先级任务里做了个死循环,结果所有低优先级任务永远得不到CPU。这就是典型的优先级反转问题。解决办法是:高优先级任务里不要做长时间阻塞操作,或者用优先级继承协议。

3.4 任务优先级详解

VxWorks的优先级范围是0到255,数值越小优先级越高。0是最高优先级,通常留给系统任务。255是最低优先级,给后台任务用。

实际项目中,我一般这样分配:

  • 0-50:中断处理、实时性要求极高的任务(比如电机控制)
  • 51-100:通信协议栈、数据采集任务
  • 101-150:普通业务逻辑任务
  • 151-200:界面显示、日志记录等非实时任务
  • 201-255:后台自检、统计等低优先级任务

这里有个坑:不要把所有任务都设成同一个优先级。如果优先级相同,VxWorks默认用时间片轮转。但如果你有10个同优先级任务,每个任务分到的时间片就很少,上下文切换开销会很大。

3.5 任务创建与删除

创建任务用taskSpawn()函数。这个函数参数比较多,我直接上代码:

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

/* 任务函数 */
void myTask(int arg1, int arg2, int arg3, int arg4,
            int arg5, int arg6, int arg7, int arg8,
            int arg9, int arg10)
{
    while (1)
    {
        printf("Task running, args: %d, %d\n", arg1, arg2);
        taskDelay(100);  /* 延时100个tick */
    }
}

/* 创建任务 */
STATUS createMyTask(void)
{
    int taskId;
    
    taskId = taskSpawn("tMyTask",      /* 任务名,建议以t开头 */
                       100,            /* 优先级 */
                       0,              /* 选项,0表示默认 */
                       20000,          /* 栈大小,单位字节 */
                       (FUNCPTR)myTask,/* 任务函数指针 */
                       1, 2, 0, 0,     /* 参数1-4 */
                       0, 0, 0, 0,     /* 参数5-8 */
                       0, 0);          /* 参数9-10 */
    
    if (taskId == ERROR)
    {
        printf("Task spawn failed! errno: 0x%x\n", errnoGet());
        return ERROR;
    }
    
    return OK;
}

删除任务用taskDelete():

STATUS deleteMyTask(int taskId)
{
    if (taskDelete(taskId) == ERROR)
    {
        printf("Task delete failed! errno: 0x%x\n", errnoGet());
        return ERROR;
    }
    return OK;
}

个人经验:创建任务时,栈大小一定要给够。我见过太多因为栈溢出导致系统崩溃的案例。怎么估算栈大小?看任务函数里局部变量的大小,加上函数调用链的深度,再留30%余量。如果实在不确定,先设大一点,跑起来后用checkStack()函数监控实际使用量。

3.6 任务管理的实用技巧

最后分享几个我在项目中积累的经验:

  • 任务命名规范:我习惯用"t"开头表示任务,比如tNetTask、tSensorTask。这样在shell里用"i"命令查看任务列表时,一眼就能认出哪些是任务。
  • 不要频繁创建删除任务:任务创建和删除是有开销的。如果某个功能需要频繁启停,考虑用信号量控制任务的挂起和恢复,而不是反复创建删除。
  • 任务栈溢出检测:VxWorks提供了taskStackCheck()函数,可以检查任务栈的使用情况。我建议在调试阶段定期调用这个函数,把结果打印出来。
  • 优先级反转的应对:如果多个任务共享同一个资源,考虑使用优先级继承协议。VxWorks的信号量支持SEM_INVERSION_SAFE选项,可以自动处理优先级反转。

重要提醒:千万不要在中断服务程序里调用taskDelete()或taskSpawn()!中断上下文不能执行可能引起阻塞的操作。我曾经犯过这个错,结果系统直接死机。正确的做法是:在中断里发一个信号量,让一个高优先级任务去处理任务的创建和删除。

好了,任务管理这块就讲到这里。下一章咱们聊聊VxWorks的内存管理,那可是另一个容易踩坑的地方。记住,任务管理是RTOS的基石,把这块搞透了,后面的驱动开发才能游刃有余。