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。

下一章,咱们会深入讲讲任务间的同步与通信,信号量、消息队列这些好东西。到时候你会发现,任务调度只是基础,任务之间的协作才是真正的难点。

好,今天就到这儿。有什么问题,咱们课后交流。