第二章:VxWorks内核基础——任务管理与调度(Wind内核)

好,咱们直接进入正题。VxWorks 的核心,说白了就是它的 Wind 内核。你想想看,一个实时操作系统,最重要的本事是什么?就是管好一堆任务,让它们该跑的时候跑,该停的时候停。这就是任务管理与调度。

我个人习惯,拿到一个系统,先看它的任务是怎么组织的。任务管理搞不清楚,后面调试起来会非常痛苦。我记得刚入行那会儿,有一次系统莫名其妙地卡死,查了三天,最后发现是一个优先级搞反了。嗯,从那以后,我对任务调度这块就特别上心。

任务状态与优先级

任务在 Wind 内核里,不是一直处于运行状态的。它有好几种状态,就像人一样,有工作、休息、等待。VxWorks 的任务状态主要有这么几种:

  • 就绪态(READY):任务啥都准备好了,就等 CPU 来跑它。说白了,它在排队。
  • 运行态(RUNNING):CPU 正在执行这个任务的代码。同一时刻,只有一个任务能处于这个状态。
  • 阻塞态(PEND):任务在等某个资源,比如信号量、消息队列。资源没到,它就只能等着。
  • 挂起态(SUSPEND):任务被故意暂停了,不参与调度。这个状态比较特殊,一般用于调试或者临时停掉某个任务。
  • 延迟态(DELAY):任务调用 taskDelay() 后,进入睡眠状态,等时间到了再醒来。

这里有个关键点:一个任务可以同时处于多个状态。比如,它既被挂起,又在等待信号量。我在项目中遇到过这种情况,调试的时候看任务状态,发现是 PEND+SUSPEND,当时就懵了。后来才明白,这是两个独立的状态位。

优先级,是调度的核心。

VxWorks 使用基于优先级的抢占式调度。每个任务都有一个优先级,数值越小,优先级越高(0 是最高优先级)。高优先级的任务就绪后,会立即抢占正在运行的低优先级任务。

为什么会这样设计?因为实时系统要求响应快。一个紧急的中断处理任务,必须能立刻抢到 CPU。你想想看,如果高优先级任务还要等低优先级任务跑完,那还叫什么实时系统?

优先级范围通常是 0 到 255。但实际项目中,我建议不要用满。我个人习惯,把任务分成几个等级:中断级(0-31)、高实时级(32-63)、普通应用级(64-127)、后台级(128-255)。这样层次清晰,不容易乱。

避坑指南:优先级反转

我曾经在一个多任务系统中,遇到一个低优先级任务占着共享资源不放,中优先级任务一直在跑,导致高优先级任务拿不到资源,整个系统响应变慢。这就是经典的优先级反转。解决办法?用优先级继承协议,或者用互斥信号量(VxWorks 的 SEM_MUTEX 自带优先级继承)。

任务创建与删除

在 VxWorks 里,创建任务用 taskSpawn() 函数。这个函数很强大,参数也多。我刚开始用的时候,经常记不住参数顺序。后来写了个小模板,每次直接套用。

// 任务创建示例
int taskId;
taskId = taskSpawn(
    "tMyTask",      // 任务名,调试时很有用
    100,            // 优先级,100 是中等优先级
    0,              // 选项,一般填 0
    20000,          // 堆栈大小,单位是字节,我一般给 20KB
    myTaskFunc,     // 任务入口函数
    0, 0, 0, 0, 0, 0, 0, 0, 0  // 最多 10 个参数
);

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

这里要注意几个点:

  • 堆栈大小:给太小了,任务会栈溢出;给太大了,浪费内存。我一般根据任务里的局部变量和函数调用深度来估算。保守点,先给 20KB,跑起来后用 checkStack() 看看实际使用量。
  • 任务名:别偷懒,起个有意义的名字。调试的时候,用 i 命令看任务列表,一眼就能认出哪个是哪个。
  • 选项:常用的有 VX_FP_TASK(支持浮点运算),VX_PRIVATE_ENV(私有环境)。如果任务不用浮点,别加这个选项,能省点上下文切换开销。

删除任务用 taskDelete()。但要注意,千万别在任务自己正在用的资源还没释放的时候就删掉它。我曾经犯过这个错,删掉一个任务后,系统莫名其妙地崩溃。后来发现,那个任务正握着一个信号量,任务一删,信号量永远没人释放了。

我的建议:尽量让任务自己退出,而不是被别人强行删除。任务退出前,把该释放的资源都释放掉。实在需要强制删除,先用 taskSafe() 保护一下,再调用 taskDelete()。

任务控制块(TCB)结构

每个任务在 VxWorks 内核里,都有一个对应的数据结构,叫任务控制块(Task Control Block,TCB)。你可以把它理解成任务的身份证,里面记录了任务的所有信息。

TCB 的结构很复杂,但咱们调试时最关心的几个字段,我列出来:

字段 说明 调试用途
tcbName 任务名 识别任务身份
tcbPriority 当前优先级 检查优先级是否异常
tcbStatus 任务状态(就绪、阻塞等) 判断任务卡在哪儿了
tcbStackBase 堆栈基地址 查看堆栈使用情况
tcbStackSize 堆栈总大小 检查是否栈溢出
tcbStackCurrent 当前堆栈指针 计算已用堆栈
tcbPended 等待的资源对象 定位死锁原因
tcbDelay 延迟 tick 数 查看任务还要睡多久

在调试器里,你可以直接用命令查看 TCB。比如:

// 查看所有任务
i

// 查看某个任务的详细信息
i 任务ID

// 查看任务堆栈使用情况
checkStack 任务ID

我记得有一次,系统跑着跑着就重启了。用 i 命令一看,有个任务的堆栈使用率到了 99%。再查它的 TCB,发现 tcbStackCurrent 已经快碰到堆栈底了。这就是典型的栈溢出。后来把堆栈大小从 10KB 改成 20KB,问题解决。

核心要点:TCB 是调试的入口。学会看 TCB,你就能知道任务在干什么、卡在哪里、用了多少资源。这是排查故障的第一步。

嗯,任务管理这块,内容其实挺多的。但咱们先把这些基础打牢。优先级、状态、创建删除、TCB,这四个点搞明白了,后面讲调度算法、死锁排查、性能优化,你才能跟得上。

下一章,咱们聊聊 Wind 内核的调度算法,看看任务到底是怎么被选中执行的。到时候我会分享一个我实际项目中遇到的调度延迟案例,挺有意思的。