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 上同一时刻只有一个任务处于运行态,其余就绪任务按优先级排队。

任务状态迁移图

下图展示了四种状态之间的合法转换路径,箭头上的标注为触发动作:

任务状态迁移图 任务状态迁移图 就绪 (Ready) 运行 (Running) 阻塞 (Blocked) 挂起 (Suspended) 调度器选中 时间片耗尽 / 被抢占 等待资源 / 主动延时 资源可用 / 超时返回 vTaskSuspend vTaskResume vTaskSuspend 先恢复再阻塞(间接路径)
常见误区: 挂起状态不能直接进入阻塞状态。如果任务在挂起期间需要等待资源,必须先恢复(进入就绪),再由调度器运行后执行阻塞调用。图中虚线路径表示这种间接转换。

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 如何与就绪链表、阻塞链表和事件链表关联:

TCB 与内核链表关系图 TCB 与内核链表关系图 就绪链表头 (pxReadyTasksLists) 阻塞链表头 (xDelayedTaskList) 事件链表头 (xPendingReadyList) TCB 任务 A xStateListItem → 就绪链表 TCB 任务 B xStateListItem → 阻塞链表 TCB 任务 C xEventListItem → 事件链表 下一个就绪任务 下一个阻塞任务 下一个等待事件任务 每个 TCB 通过 xStateListItem 挂入状态链表,通过 xEventListItem 挂入事件链表,实现“一个任务可同时等待多个事件”

2.4 任务创建与 TCB 初始化流程

当调用任务创建 API(如 xTaskCreate)时,内核内部执行以下步骤:

  1. 分配 TCB 内存: 从堆中分配 tskTCB 结构体空间。
  2. 分配任务栈: 分配用户指定的栈空间,并将栈指针初始化为栈顶。
  3. 初始化栈帧: 在栈中预置任务入口函数地址、返回地址、初始 CPU 寄存器值,使任务首次切换时能正确执行。
  4. 设置优先级与名称: 将传入的优先级、任务名写入 TCB 字段。
  5. 初始化链表节点: 调用 vListInitialiseItem 初始化 xStateListItem 和 xEventListItem,并将 TCB 指针存入节点。
  6. 挂入就绪链表: 根据优先级将任务插入就绪链表,等待调度。
性能提示: 任务栈大小必须在创建前仔细评估。栈溢出会导致相邻 TCB 或其它任务的数据被破坏。建议在调试阶段开启栈溢出检测钩子函数(如 vApplicationStackOverflowHook)。

2.5 本章小结

  • 任务状态模型(就绪/运行/阻塞/挂起)是 RTOS 调度的基础,状态迁移由内核 API 和调度事件驱动。
  • TCB 是任务管理的核心数据结构,包含栈指针、优先级、链表节点、运行时间等关键字段。
  • TCB 通过链表节点嵌入内核链表,实现高效的任务查找与状态切换。
  • 任务创建过程本质上是 TCB 初始化 + 栈帧预置 + 链表挂入的三步操作。