第二章: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 内核的调度算法,看看任务到底是怎么被选中执行的。到时候我会分享一个我实际项目中遇到的调度延迟案例,挺有意思的。