4. 任务模型与状态机
在 RTOS 中,任务不是“一个函数”那么简单,而是一个被内核持续管理、可被调度、可被同步机制挂起与唤醒的运行实体。理解任务模型的关键,是把“任务代码”与“任务状态”分开看:代码是静态的,状态是动态的;调度器只关心哪些任务处于就绪态,以及谁的优先级最高。任务状态机、任务控制块(TCB)和优先级机制,共同构成了 RTOS 任务管理的三根支柱。
一、核心原理:任务状态机与状态转换
典型 RTOS 任务至少包含四种基本状态:就绪(Ready)、运行(Running)、阻塞(Blocked)、挂起(Suspended)。就绪表示任务已具备运行条件,只等 CPU;运行表示任务正占用 CPU;阻塞表示任务在等待某个事件(延时、信号量、队列、事件标志等),即使 CPU 空闲也不能运行;挂起表示任务被显式“冻结”,不参与调度,通常用于调试、低功耗或临时停用。
状态转换由内核 API 和中断共同驱动。例如,任务调用 vTaskDelay() 会从运行态进入阻塞态;延时到期后由时钟节拍中断将其恢复为就绪态;若此时它的优先级最高,调度器会立即切换它进入运行态。任务调用 vTaskSuspend() 会进入挂起态,必须由其他任务调用 vTaskResume() 才能回到就绪态。理解这些转换路径,是排查“任务为什么不跑”的第一把钥匙。
二、任务控制块:任务的“身份证”与“上下文仓库”
任务控制块(TCB,Task Control Block)是内核为每个任务维护的数据结构。它至少包含:任务栈指针、任务状态、优先级、任务入口地址、事件控制块指针、链表节点以及运行统计信息。当任务被切换出去时,CPU 寄存器现场被保存到该任务栈中,TCB 中的栈指针指向保存位置;当任务再次被调度时,内核从 TCB 恢复栈指针和寄存器,任务就像从未中断过一样继续执行。
因此,TCB 既是任务的“身份证”,也是它的“上下文仓库”。在 FreeRTOS 中,创建任务时通过 xTaskCreate() 动态分配 TCB 和栈;在静态创建方式下,则由用户提供 StaticTask_t 和栈数组。理解 TCB 的字段,有助于分析内存占用、栈溢出和优先级反转等问题。
typedef struct tskTaskControlBlock {
volatile StackType_t *pxTopOfStack; // 栈顶指针
ListItem_t xStateListItem; // 状态链表节点
ListItem_t xEventListItem; // 事件链表节点
UBaseType_t uxPriority; // 任务优先级
StackType_t *pxStack; // 栈起始地址
char pcTaskName[16]; // 任务名
// ... 其他统计与配置字段
} tskTCB;
三、优先级与调度:谁先运行,谁让出 CPU
RTOS 通常采用基于优先级的抢占式调度。每个任务有一个优先级数值,数值越大(或越小,取决于具体 RTOS)表示优先级越高。调度器在每次节拍中断、任务阻塞、事件唤醒或主动让出时,选择就绪队列中优先级最高的任务运行。若多个任务优先级相同,则按时间片轮转或按就绪顺序调度。
优先级设计要遵循“关键任务高优先级、计算密集任务低优先级、优先级数量适中”的原则。优先级过多会增加调度开销和调试难度;优先级过少则难以表达任务间的紧急程度差异。常见做法是把任务分为硬实时、软实时、后台处理三层,再在层内细分。
| 状态 | 是否参与调度 | 典型进入方式 | 典型退出方式 |
|---|---|---|---|
| 就绪 Ready | 是 | 任务创建、事件唤醒、挂起恢复 | 被调度器选中进入运行 |
| 运行 Running | 是(当前占用 CPU) | 调度器选中 | 阻塞、被抢占、挂起、主动让出 |
| 阻塞 Blocked | 否 | 延时、等待信号量/队列/事件 | 事件到达或超时,回到就绪 |
| 挂起 Suspended | 否 | vTaskSuspend() 显式调用 | vTaskResume() 恢复为就绪 |
四、任务生命周期管理与典型场景
任务生命周期通常包括:创建、就绪、运行、阻塞、挂起、恢复、删除。创建时内核分配 TCB 和栈,并把任务插入就绪链表;删除时释放资源,并从所有链表中移除。若任务在运行中删除自己,内核会在空闲任务中完成资源回收,因此空闲任务必须存在且不能被长期饿死。
以一个数据采集系统为例:采集任务优先级最高,周期到达时从传感器读取数据并写入队列;处理任务优先级中等,从队列取数据做滤波和打包;通信任务优先级较低,把打包数据发送到上位机。采集任务在等待周期时进入阻塞态,处理任务在队列为空时进入阻塞态,通信任务在发送完成后进入阻塞态。这样 CPU 在无事件时自动切换到空闲任务,既省电又保证实时性。
五、常见误区与复盘
- 误区一:把阻塞当挂起。阻塞是等待事件,事件到达会自动恢复;挂起是显式冻结,必须显式恢复。两者不能混用。
- 误区二:优先级越高越好。高优先级任务若长期占用 CPU,会饿死低优先级任务,甚至导致看门狗复位。
- 误区三:忽略 TCB 和栈的内存开销。每个任务都需要独立栈空间,任务数量多时内存压力显著,需结合
configMINIMAL_STACK_SIZE和实际调用深度评估。 - 误区四:在中断中调用非 FromISR 版本 API。中断中必须使用带
FromISR后缀的接口,否则可能破坏内核链表。