4. 任务与调度核心概念
在 S32K 这类基于 ARM Cortex-M 内核的 MCU 上运行 RTOS,任务并不是“一直跑到底”的函数,而是被内核反复切换、暂停、恢复的执行实体。理解任务状态机、任务控制块(TCB)、优先级、就绪表、调度时机与上下文切换,是写出稳定实时系统的前提。本节以 FreeRTOS 在 S32K144 上的典型实现为主线,把这些概念串成一条完整的运行链路。
一、任务状态机:任务的一生
一个任务从创建到删除,会在若干状态之间迁移。FreeRTOS 中最核心的状态有四种:运行态(Running)、就绪态(Ready)、阻塞态(Blocked)和挂起态(Suspended)。单核 MCU 上同一时刻只有一个任务处于运行态,其余就绪任务排队等待 CPU。
- 就绪态:任务已具备运行条件,只差 CPU 使用权,等待调度器选中。
- 运行态:当前占用 CPU 的任务,正在执行指令。
- 阻塞态:任务主动等待某个事件(延时到期、信号量、队列数据),带超时时间,超时后自动回到就绪态。
- 挂起态:任务被显式挂起,不参与调度,也不会因超时自动恢复,必须由
vTaskResume()唤醒。
状态迁移的触发点非常明确:调用 vTaskDelay() 进入阻塞;等待的信号量被释放则回到就绪;调用 vTaskSuspend() 进入挂起;被调度器选中则从就绪变为运行;时间片耗尽或被更高优先级任务抢占则从运行退回就绪。
二、任务控制块 TCB:任务的“身份证”
内核要管理任务,必须为每个任务保存一份数据结构,这就是任务控制块 TCB。在 FreeRTOS 中它由 TCB_t 定义,创建任务时由 xTaskCreate() 动态分配或使用静态分配。TCB 中保存了任务恢复运行所需的全部信息:
- 栈指针:指向该任务私有栈的栈顶,上下文切换时最先保存和恢复。
- 任务状态与优先级:当前状态、当前优先级、基础优先级。
- 事件链表项:任务阻塞在哪个事件列表上,用于超时管理。
- 就绪链表项:任务在就绪表中的挂接位置。
- 任务名与句柄:调试和 API 调用使用。
可以把 TCB 理解为任务的“存档文件”:任务被切走时,CPU 寄存器写入它的栈,栈指针存入 TCB;任务被切回时,从 TCB 取出栈指针,再从栈中恢复寄存器。
三、优先级与就绪表:谁先上 CPU
FreeRTOS 默认采用抢占式优先级调度,优先级数值越大越高。S32K144 上通常配置 configMAX_PRIORITIES 为 5~10,数值越大越紧急。相同优先级的任务按时间片轮转。
就绪表是调度器的“候选名单”。FreeRTOS 用 pxReadyTasksLists[] 数组实现,每个优先级一个就绪链表。调度器只需从最高非空优先级链表取第一个任务即可,无需遍历所有任务。为了快速找到最高非空优先级,内核还维护一个位图 uxTopReadyPriority,用位操作在常数时间内定位。
| 概念 | 数据结构/接口 | 作用 |
|---|---|---|
| 任务控制块 | TCB_t | 保存任务栈指针、状态、优先级等上下文 |
| 就绪表 | pxReadyTasksLists[] | 按优先级组织的就绪任务链表数组 |
| 最高就绪优先级 | uxTopReadyPriority | 位图快速定位最高非空优先级 |
| 阻塞链表 | xDelayedTaskList | 按唤醒时间排序的阻塞任务 |
| 调度器 | vTaskSwitchContext() | 选出下一个运行任务 |
四、调度时机与上下文切换
调度不是随时发生的,而是在明确的“调度点”触发。常见调度时机包括:
- 任务调用阻塞 API,如
vTaskDelay()、xQueueReceive()。 - 中断服务程序中释放了信号量或发送了队列消息,且唤醒了更高优先级任务。
- 任务主动调用
taskYIELD()让出 CPU。 - 系统节拍中断(SysTick)到来,检查时间片是否耗尽。
- 任务被创建、删除、挂起或恢复,导致就绪表变化。
上下文切换的本质是“保存现场、恢复现场”。以 Cortex-M 为例,硬件在异常入口自动压栈 R0-R3、R12、LR、PC、xPSR;PendSV 异常中,内核手动保存 R4-R11 到当前任务栈,把栈指针写入当前 TCB,然后调用调度器选出新任务,再从新任务 TCB 取出栈指针,恢复 R4-R11,最后异常返回自动恢复剩余寄存器。整个过程由 PendSV_Handler 完成,因此 PendSV 常被设置为最低优先级,避免打断其他中断。
/* S32K144 + FreeRTOS 典型任务创建与优先级配置 */
#define TASK_LED_PRIO 2
#define TASK_CAN_PRIO 4
#define TASK_LOG_PRIO 1
xTaskCreate(vTaskLed, "LED", 256, NULL, TASK_LED_PRIO, NULL);
xTaskCreate(vTaskCan, "CAN", 512, NULL, TASK_CAN_PRIO, NULL);
xTaskCreate(vTaskLog, "LOG", 384, NULL, TASK_LOG_PRIO, NULL);
/* CAN 任务优先级最高,收到报文后可立即抢占 LED 与 LOG 任务 */
vTaskStartScheduler();
关键结论:优先级决定“谁先运行”,就绪表决定“谁在等待”,TCB 决定“怎么恢复”,PendSV 决定“怎么切换”。四者共同构成 RTOS 调度的最小闭环。
五、案例与常见误区
假设 S32K144 上有一个 CAN 接收任务(优先级 4)、一个 LED 闪烁任务(优先级 2)和一个日志任务(优先级 1)。当 CAN 中断收到报文并释放信号量时,CAN 任务从阻塞态变为就绪态,由于优先级最高,PendSV 触发后立即抢占 LED 任务。LED 任务被切回就绪态,等 CAN 任务再次阻塞后才继续运行。这就是抢占式调度的典型表现。
常见误区一:在中断中直接调用非 FromISR 版本的 API,导致就绪表被破坏或系统崩溃。中断中必须使用 xQueueSendFromISR()、xSemaphoreGiveFromISR() 等带 FromISR 后缀的接口。
常见误区二:把耗时操作放在高优先级任务中,导致低优先级任务长期得不到执行,表现为“系统卡死”。应遵循“高优先级任务短小、快进快出”的原则。
常见误区三:任务栈分配过小。上下文切换需要保存寄存器,若栈溢出会破坏相邻 TCB 或内存,建议开启 configCHECK_FOR_STACK_OVERFLOW 并在调试阶段使用 uxTaskGetStackHighWaterMark() 检查余量。
复盘时可以用一个简单清单检查调度设计是否合理:每个任务是否有明确的阻塞点?最高优先级任务是否足够短?中断是否只做“释放信号量”这类轻量动作?就绪表中是否存在长期不被调度的低优先级任务?把这些问题逐一确认,S32K 上的 RTOS 任务调度就能稳定可控。