3. 任务管理与调度:任务控制块(TCB)结构、任务状态与状态转换、优先级调度算法(基于优先级的抢占式调度)、时间片轮转调度、任务创建与删除API详解。
各位同学,咱们今天聊聊VxWorks里最核心的东西——任务管理与调度。说白了,一个实时操作系统好不好用,很大程度上就看它怎么管这些任务。我做了这么多年嵌入式,见过不少系统因为任务调度没设计好,最后现场跑飞了,那叫一个头疼。今天咱们就把这块掰开了揉碎了讲清楚。
3.1 任务控制块(TCB)——任务的“身份证”
每个任务在VxWorks里都有一个对应的数据结构,叫任务控制块(Task Control Block,TCB)。你可以把它想象成任务的“身份证”,系统就是靠它来识别和管理每个任务的。
TCB里都存了啥?我列一下关键字段:
- 任务ID(TID):每个任务的唯一标识,就像你的身份证号。
- 任务名称:方便人看的,比如“tShell”、“tLogTask”。
- 优先级:决定了谁先跑谁后跑,这个后面细说。
- 任务状态:当前是就绪、阻塞还是运行中。
- 上下文环境:包括CPU寄存器、栈指针等,用于任务切换时保存现场。
- 栈指针和栈大小:每个任务都有自己的栈,防止互相踩踏。
- 错误状态:记录任务执行过程中发生的错误。
- 时间片:用于时间片轮转调度。
嗯,这里要注意,TCB是系统内部维护的,咱们应用层开发者一般不会直接操作它,但理解它的结构对后面理解调度机制很有帮助。
核心要点:TCB是任务管理的基石。系统每创建一个任务,就会分配一个TCB;任务删除时,TCB也会被回收。
3.2 任务状态与状态转换
任务不是一直跑着的,它会在几个状态之间跳来跳去。VxWorks里主要有这么几个状态:
| 状态 | 说明 |
|---|---|
| 就绪(READY) | 任务已经准备好,随时可以运行,就等CPU空闲下来。 |
| 运行(RUNNING) | 任务正在占用CPU执行代码。 |
| 阻塞(PEND) | 任务在等待某个资源或事件,比如等待信号量、消息队列。 |
| 延迟(DELAY) | 任务主动调用taskDelay(),睡一会儿再起来干活。 |
| 挂起(SUSPEND) | 任务被强制暂停,不参与调度,直到被恢复。 |
状态之间怎么转换?我举个例子你就明白了:
- 任务刚创建完,进入就绪状态。
- 调度器一看,嗯,你优先级最高,CPU给你,于是变成运行状态。
- 运行中想等个信号量,没等到,那就乖乖进阻塞状态。
- 信号量来了,任务被唤醒,又回到就绪状态,等着再次被调度。
为什么会这样设计?说白了,就是为了让CPU别闲着。一个任务在等资源,那就让别的任务先跑,提高整体效率。
个人经验:我在项目中遇到过一个问题,一个任务因为信号量没释放,导致所有高优先级任务都卡在阻塞状态。后来排查发现是信号量超时设置不合理。所以啊,设计任务状态转换时,一定要考虑超时和异常处理。
3.3 优先级调度算法——谁优先级高谁先跑
VxWorks用的是基于优先级的抢占式调度。什么意思?就是每个任务都有一个优先级,数字越小优先级越高(0是最高,255是最低)。调度器每次都会从就绪队列里挑优先级最高的任务来跑。
抢占式体现在哪?假设当前任务A正在跑,优先级是50。突然,一个优先级为10的任务B就绪了。这时候调度器会立刻打断任务A,把CPU交给任务B。这就是“抢占”。
我刚开始做VxWorks开发时,总觉得抢占式调度很暴力,后来发现这才是实时系统的精髓。你想想看,如果高优先级的紧急任务来了,还得等低优先级的任务跑完,那还叫什么实时系统?
避坑指南:我曾经犯过一个错误,把几个关键任务的优先级设得太接近,结果导致频繁的任务切换,系统开销巨大。建议优先级之间至少留出5-10的间隔,避免不必要的抢占。
3.4 时间片轮转调度——给同优先级任务一个公平的机会
如果多个任务优先级相同怎么办?这时候就需要时间片轮转调度了。每个任务轮流跑一小段时间(时间片),时间到了就换下一个。
VxWorks里,时间片的大小可以通过kernelTimeSlice()函数来设置。默认情况下,时间片轮转是关闭的,需要你手动开启。
/* 开启时间片轮转,时间片为10个系统时钟tick */
kernelTimeSlice(10);
我个人习惯是,只在同优先级任务确实需要公平共享CPU时才开启时间片轮转。如果任务之间有明确的优先级差异,就别开,免得增加不必要的调度开销。
3.5 任务创建与删除API详解
好了,理论讲完了,咱们来看看实际怎么用。任务创建和删除是最常用的两个操作。
3.5.1 任务创建:taskSpawn()
taskSpawn()是VxWorks里创建任务的标准函数。它的原型长这样:
int taskSpawn
(
char *name, /* 任务名称 */
int priority, /* 优先级 */
int options, /* 选项 */
int stackSize, /* 栈大小(字节) */
FUNCPTR entryPt, /* 任务入口函数 */
int arg1, /* 参数1 */
int arg2, /* 参数2 */
...
);
举个例子:
/* 创建一个名为"myTask"的任务,优先级100,栈大小4096字节 */
int tid = taskSpawn("myTask", 100, 0, 4096, myFunction, 0, 0, 0, 0, 0, 0);
if (tid == ERROR) {
printf("任务创建失败!\n");
}
这里要注意几个点:
- 栈大小:给太小了容易栈溢出,给太大了浪费内存。我一般根据任务里局部变量和函数调用深度来估算,保守一点给个4096或8192。
- 参数个数:最多支持10个参数,每个都是int类型。如果需要传复杂结构,可以用指针。
- 选项:常用的有VX_FP_TASK(支持浮点运算),VX_NO_STACK_FILL(不填充栈,省点时间)。
3.5.2 任务删除:taskDelete()
任务跑完了或者不需要了,可以用taskDelete()把它干掉。
/* 删除指定ID的任务 */
status = taskDelete(tid);
if (status == ERROR) {
printf("任务删除失败!\n");
}
嗯,这里有个坑。我曾经在项目中直接删除了一个正在持有信号量的任务,结果导致其他等待该信号量的任务永远阻塞。所以啊,删除任务前,一定要确保它已经释放了所有占用的资源。
建议:如果只是想让任务暂停,可以用taskSuspend()和taskResume(),而不是直接删除。这样更安全,也方便后续恢复。
3.6 小结
今天咱们把任务管理和调度的核心内容过了一遍。TCB是任务的身份证,状态转换是任务的生命周期,优先级调度和时间片轮转是两种调度策略,而taskSpawn和taskDelete是咱们最常用的两个API。
下一章,咱们会深入讲讲任务间的同步与通信,信号量、消息队列这些好东西。到时候你会发现,任务调度只是基础,任务之间的协作才是真正的难点。
好,今天就到这儿。有什么问题,咱们课后交流。