3. VxWorks内核基础:任务(Task)的概念、任务状态机、优先级与调度策略

好,咱们今天聊聊VxWorks里最核心的东西——任务。说白了,任务就是一段可以独立运行的代码,它有自己的栈、自己的上下文。你写驱动,本质上就是在跟任务打交道。

我记得刚接触VxWorks时,有个老工程师跟我说:“你写的每一个驱动函数,最终都是被某个任务调用的。”这句话我琢磨了很久,后来才真正理解。任务就是VxWorks世界的“人”,每个“人”都在干自己的活。

3.1 任务到底是什么?

任务在VxWorks里是一个轻量级的执行单元。它不像Linux进程那么重,切换开销很小。每个任务有:

  • 任务控制块(TCB):存放任务的所有信息
  • 栈空间:存放局部变量和函数调用信息
  • 优先级:决定谁先跑
  • 上下文:CPU寄存器的值

你创建一个任务,内核就给它分配这些资源。我习惯用taskSpawn来创建任务,简单直接:

#include <taskLib.h>

/* 创建一个任务 */
int taskId = taskSpawn(
    "tMyTask",      /* 任务名,调试时有用 */
    100,            /* 优先级,数值越小优先级越高 */
    0,              /* 选项,一般填0 */
    20000,          /* 栈大小,单位字节 */
    myTaskFunc,     /* 任务函数入口 */
    0, 0, 0, 0, 0, 0, 0, 0, 0  /* 最多10个参数 */
);

if (taskId == ERROR) {
    printf("任务创建失败!\n");
}

嗯,这里要注意:栈大小别给太小。我在项目中遇到过驱动因为栈溢出导致系统随机崩溃,查了两天才发现是某个回调函数里开了个大数组。你想想看,一个局部变量char buf[4096],如果任务栈只有4096字节,那基本就炸了。

3.2 任务状态机:Ready / Suspend / Delay

任务不是一直在跑的。它会在几个状态之间切换。VxWorks的任务状态机比Linux简单得多,核心就三个状态:

状态 含义 典型场景
READY 任务就绪,等待CPU 刚创建、被唤醒、时间片用完
SUSPEND 任务挂起,不参与调度 调试时手动挂起、等待资源
DELAY 任务延时,等待时间到 调用taskDelay()、等待信号量超时

为什么会这样设计?说白了,就是为了让CPU别闲着。READY状态的任务排着队等CPU,SUSPEND的任务靠边站,DELAY的任务睡大觉。调度器只从READY队列里选任务执行。

我举个例子你就明白了:

void myTaskFunc(void)
{
    while (1) {
        /* 干活 */
        doSomething();
        
        /* 睡100个tick,进入DELAY状态 */
        taskDelay(100);
        
        /* 睡醒后回到READY状态 */
    }
}

调用taskDelay(100)后,任务从READY变成DELAY。100个tick后,内核把它重新放回READY队列。这期间CPU可以去干别的活。

关键点:SUSPEND状态的任务不会自动回到READY。你必须显式调用taskResume()才能唤醒它。我曾经在调试一个串口驱动时,不小心把接收任务挂起了,结果串口再也收不到数据。排查了半天才发现是taskSuspend()调用时机不对。

3.3 任务优先级与调度策略

VxWorks支持256个优先级(0~255),0最高,255最低。这个跟Linux不一样,Linux是数值越大优先级越高。你写驱动时,一般用100~150之间的优先级。

调度策略有三种,我一个个说:

3.3.1 优先级抢占调度(Preemptive Priority)

这是VxWorks默认的调度方式。规则很简单:高优先级的任务一旦就绪,立刻抢占低优先级任务的CPU。你想想看,如果中断处理完唤醒了一个高优先级任务,那当前运行的低优先级任务就得让路。

我在项目中遇到过一个问题:一个低优先级的任务正在写Flash,突然被高优先级任务抢占。高优先级任务也要写Flash,结果两个任务同时操作Flash控制器,数据全乱了。解决方案是加互斥信号量,这个后面会讲。

3.3.2 轮转调度(Round-Robin)

同优先级的任务,大家轮流用CPU。每个任务跑一个时间片(默认4个tick),时间到了就换下一个。这个策略适合多个同等重要的任务。

启用轮转调度很简单:

/* 启用轮转调度 */
kernelTimeSlice(4);  /* 时间片设为4个tick */

嗯,这里要注意:轮转调度只对同优先级的任务有效。如果高优先级任务一直就绪,低优先级任务永远拿不到CPU。这就是优先级反转的根源之一。

3.3.3 先来先服务(FIFO)

这个最简单。同优先级的任务,谁先就绪谁先跑。除非任务主动让出CPU(比如调用taskDelay()或等待信号量),否则它一直占着CPU。

FIFO调度适合那些需要连续执行、不能被中断的任务。比如:

  • 批量数据处理
  • 硬件初始化序列
  • 关键事务处理

我的建议:驱动开发中,中断服务程序(ISR)里不要做复杂操作。ISR应该只做最必要的事,比如读取硬件状态、唤醒一个高优先级任务。真正的处理逻辑放在任务里。这样既保证了实时性,又避免了ISR占用太多时间。

3.4 避坑指南:任务优先级设计

优先级设计是驱动开发中最容易出问题的地方。我总结了几条经验:

  1. 中断处理任务优先级要高:一般用80~100,确保能及时响应硬件事件
  2. 普通驱动任务用100~150:比如数据采集、状态监控
  3. 后台任务优先级要低:比如日志记录、统计上报,用200左右
  4. 避免优先级反转:高优先级任务等低优先级任务释放资源,这是经典问题

我曾经在一个网络驱动项目中,把接收任务的优先级设得太高,结果它一直抢占CPU,导致发送任务饿死。网络吞吐量上不去,排查了好久才发现是优先级设置不合理。

警告:不要把所有任务都设成同一个优先级!如果必须这么做,一定要启用轮转调度。否则一个任务死循环,整个系统就卡死了。我见过一个团队把所有任务优先级都设成100,结果一个任务里的while循环忘了加延时,其他任务全饿死了。

3.5 任务相关的实用API

写驱动时常用的任务API,我列几个:

API 功能 使用场景
taskSpawn 创建并启动任务 驱动初始化时创建处理任务
taskDelete 删除任务 驱动卸载时清理任务
taskSuspend 挂起任务 暂停某个处理流程
taskResume 恢复任务 继续执行挂起的任务
taskDelay 延时指定tick数 轮询等待、定时操作
taskPrioritySet 动态修改优先级 运行时调整任务优先级

我个人习惯在驱动初始化时创建任务,在驱动卸载时删除任务。这样资源管理清晰,不会出现任务泄漏。

好了,任务的基础概念就这些。说白了,任务就是VxWorks世界的“线程”,你理解了任务的状态切换和调度策略,写驱动就成功了一半。下一章咱们聊聊任务间通信,那是驱动开发中更精彩的部分。