2. 任务与任务控制块
在 RTOS 中,任务(Task) 是系统调度的基本单位,本质上是一个无限循环且具备独立栈空间的函数。每个任务都拥有自己的执行上下文(CPU 寄存器、栈指针、局部变量),通过任务控制块(TCB)统一管理。理解任务的状态迁移与 TCB 数据结构,是掌握 RTOS 调度机制的第一步。
关键概念: 任务 ≠ 函数调用。任务被调度器“看到”并管理,而普通函数只是任务执行过程中的一段代码。任务必须通过 TCB 注册到内核中才能参与调度。
2.1 任务状态模型
RTOS 内核通常将任务划分为四种基本状态,状态之间的转换由内核 API 或调度事件触发。下表列出了各状态的定义与典型触发条件:
| 状态 | 英文名称 | 定义 | 典型进入条件 |
|---|---|---|---|
| 就绪 | Ready | 任务已具备运行条件,等待调度器分配 CPU | 任务创建完成、阻塞超时返回、被唤醒 |
| 运行 | Running | 任务正在占用 CPU 执行指令 | 调度器从就绪队列中选中该任务 |
| 阻塞 | Blocked | 任务因等待某事件(信号量、消息队列、延时)而暂停 | 调用 pend 类 API、主动延时 |
| 挂起 | Suspended | 任务被强制暂停,不参与调度,需显式恢复 | 调用任务挂起 API(如 vTaskSuspend) |
状态对比要点:
- 阻塞 vs 挂起: 阻塞是任务主动等待资源,超时或资源可用后自动回到就绪;挂起是外部强制暂停,必须由其他任务或中断调用恢复 API 才能退出。
- 就绪 vs 运行: 单核 CPU 上同一时刻只有一个任务处于运行态,其余就绪任务按优先级排队。
任务状态迁移图
下图展示了四种状态之间的合法转换路径,箭头上的标注为触发动作:
常见误区: 挂起状态不能直接进入阻塞状态。如果任务在挂起期间需要等待资源,必须先恢复(进入就绪),再由调度器运行后执行阻塞调用。图中虚线路径表示这种间接转换。
2.2 任务控制块(TCB)数据结构
任务控制块是 RTOS 内核用于管理任务的核心数据结构,每个任务对应一个 TCB 实例。TCB 记录了任务的全部元信息,包括栈指针、优先级、状态、事件等待链表等。下面以 FreeRTOS 风格为例,展示 TCB 的典型字段:
typedef struct tskTaskControlBlock {
volatile StackType_t *pxTopOfStack; /* 当前栈顶指针,上下文切换时保存/恢复 */
ListItem_t xStateListItem; /* 状态链表节点(就绪/阻塞/挂起链表) */
ListItem_t xEventListItem; /* 事件链表节点(等待信号量/队列等) */
UBaseType_t uxPriority; /* 任务优先级(数值越大优先级越高) */
StackType_t *pxStack; /* 栈起始地址 */
uint16_t usStackDepth; /* 栈深度(单位:StackType_t) */
char pcTaskName[configMAX_TASK_NAME_LEN];/* 任务名称,用于调试 */
UBaseType_t uxBasePriority; /* 基础优先级(优先级继承时使用) */
uint32_t ulRunTimeCounter; /* 运行时间累计计数器 */
struct tskTaskControlBlock *pxNext; /* 用于某些内核内部链表 */
} tskTCB;
TCB 核心字段功能解析
| 字段 | 类型 | 作用 | 调度相关 |
|---|---|---|---|
pxTopOfStack |
栈指针 | 保存任务被切换时的 CPU 寄存器现场 | 上下文切换时直接读写 |
xStateListItem |
链表节点 | 将任务挂入就绪/阻塞/挂起链表 | 调度器通过链表遍历就绪任务 |
xEventListItem |
链表节点 | 将任务挂入等待事件(如信号量)的链表 | 事件发生时唤醒对应任务 |
uxPriority |
无符号整数 | 当前优先级,决定调度顺序 | 抢占调度时比较优先级 |
pxStack |
栈指针 | 指向任务私有栈的基地址 | 栈溢出检测时使用 |
ulRunTimeCounter |
32位计数器 | 累计任务占用 CPU 的时间 | 时间片调度 / 性能分析 |
设计要点:
- 链表驱动: TCB 中的
xStateListItem和xEventListItem使任务能够同时存在于“状态链表”和“事件等待链表”中,这是 RTOS 实现多事件等待的基础。 - 栈指针独立性: 每个任务拥有独立的
pxTopOfStack,确保任务切换时不会污染其他任务的寄存器现场。 - 优先级继承支持:
uxBasePriority字段用于在互斥量场景下临时提升任务优先级,防止优先级反转。
2.3 TCB 与内核链表的关系
TCB 通过链表节点字段嵌入到内核的各种链表中,形成调度框架。下图展示了 TCB 如何与就绪链表、阻塞链表和事件链表关联:
2.4 任务创建与 TCB 初始化流程
当调用任务创建 API(如 xTaskCreate)时,内核内部执行以下步骤:
- 分配 TCB 内存: 从堆中分配
tskTCB结构体空间。 - 分配任务栈: 分配用户指定的栈空间,并将栈指针初始化为栈顶。
- 初始化栈帧: 在栈中预置任务入口函数地址、返回地址、初始 CPU 寄存器值,使任务首次切换时能正确执行。
- 设置优先级与名称: 将传入的优先级、任务名写入 TCB 字段。
- 初始化链表节点: 调用
vListInitialiseItem初始化xStateListItem和xEventListItem,并将 TCB 指针存入节点。 - 挂入就绪链表: 根据优先级将任务插入就绪链表,等待调度。
性能提示: 任务栈大小必须在创建前仔细评估。栈溢出会导致相邻 TCB 或其它任务的数据被破坏。建议在调试阶段开启栈溢出检测钩子函数(如
vApplicationStackOverflowHook)。
2.5 本章小结
- 任务状态模型(就绪/运行/阻塞/挂起)是 RTOS 调度的基础,状态迁移由内核 API 和调度事件驱动。
- TCB 是任务管理的核心数据结构,包含栈指针、优先级、链表节点、运行时间等关键字段。
- TCB 通过链表节点嵌入内核链表,实现高效的任务查找与状态切换。
- 任务创建过程本质上是 TCB 初始化 + 栈帧预置 + 链表挂入的三步操作。