2. 任务管理核心:任务控制块(TCB)数据结构深度解析、任务状态与状态迁移图、任务创建与删除的源码实现
各位同学,欢迎来到第二章。这一章我们啃的是RTOS的「心脏」——任务管理。说白了,你写的所有应用代码,最终都要靠任务来承载。而任务控制块(TCB),就是操作系统用来管理每个任务的「身份证」和「档案袋」。
我个人习惯,在分析任何RTOS源码之前,先把这个TCB结构体彻底吃透。因为后面所有的调度、挂起、延时操作,本质上都是在操作这个结构体里的字段。你想想看,如果连身份证都搞不清楚,怎么去管理几十上百个任务?
2.1 任务控制块(TCB)数据结构深度解析
先看一个典型的TCB定义。我以FreeRTOS为例,但其他RTOS大同小异。核心思想是一样的:保存任务的运行上下文、状态、优先级和栈指针。
typedef struct tskTaskControlBlock {
volatile StackType_t *pxTopOfStack; // 当前栈顶指针,最关键!
ListItem_t xStateListItem; // 状态链表节点(就绪、阻塞、挂起)
ListItem_t xEventListItem; // 事件链表节点(等待信号量、队列)
UBaseType_t uxPriority; // 任务优先级
StackType_t *pxStack; // 栈起始地址
char pcTaskName[configMAX_TASK_NAME_LEN]; // 任务名字,调试用
// ... 还有其他成员,比如栈大小、临界区嵌套计数器等
} tskTCB;
这里我重点说三个字段,也是我当年调试时踩坑最多的地方。
第一,pxTopOfStack。 这是TCB的灵魂。每次任务切换时,CPU的寄存器(R0-R15、PSP、LR等)都会被压入当前任务的栈中,然后pxTopOfStack指向这个保存位置。下次恢复时,直接从这取出来弹回寄存器。我在项目中遇到过,如果栈溢出把pxTopOfStack指向的内存踩坏了,任务一恢复就跑飞。嗯,这种问题查起来特别痛苦。
第二,xStateListItem。 这个节点把任务挂到不同的链表里。比如就绪链表、阻塞链表、挂起链表。调度器其实就是在这些链表之间「搬砖」——把任务从一个链表移到另一个链表。说白了,任务状态的改变,就是链表节点的移动。
第三,uxPriority。 优先级决定了谁先跑。但要注意,RTOS通常支持同优先级任务轮询调度。我见过有人把所有任务设成同一个优先级,结果系统跑起来像单任务一样,因为忘了开时间片轮转。
2.2 任务状态与状态迁移图
任务有几种状态?很多初学者以为只有「运行」和「不运行」。其实RTOS里,任务有四种基本状态:运行态、就绪态、阻塞态、挂起态。
我画个简单的迁移图给你看:
创建
|
v
就绪态 <---> 运行态
| ^ | ^
| | | |
v | v |
阻塞态 挂起态
解释一下:
- 运行态:CPU正在执行这个任务。单核CPU同一时刻只有一个任务在运行。
- 就绪态:任务已经准备好,随时可以跑,但CPU被别的任务占着。
- 阻塞态:任务在等某个事件,比如等信号量、等队列消息、等延时结束。这时候它不消耗CPU。
- 挂起态:任务被强制暂停,只能通过其他任务调用
vTaskResume来恢复。
为什么会从运行态变成阻塞态?最常见的就是调用了vTaskDelay。我曾经有个项目,一个传感器采集任务用了vTaskDelay(10),结果发现采集周期不准。后来查资料才知道,vTaskDelay是相对延时,如果任务被中断打断,延时时间会漂移。换成vTaskDelayUntil才解决。这个坑我记了很久。
taskYIELD()主动让出CPU才解决。记住:高优先级任务如果一直不阻塞,低优先级任务会饿死。
2.3 任务创建与删除的源码实现
好,理论讲完了,我们看代码。任务创建的核心函数是xTaskCreate。我简化一下关键流程:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名
configSTACK_DEPTH_TYPE usStackDepth, // 栈大小(单位是字,不是字节)
void *pvParameters, // 任务参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
) {
TCB_t *pxNewTCB;
StackType_t *pxStack;
// 1. 分配栈空间
pxStack = pvPortMalloc(usStackDepth * sizeof(StackType_t));
if (pxStack == NULL) return errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
// 2. 分配TCB空间
pxNewTCB = pvPortMalloc(sizeof(TCB_t));
if (pxNewTCB == NULL) {
vPortFree(pxStack);
return errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
// 3. 初始化TCB字段
pxNewTCB->pxStack = pxStack;
pxNewTCB->pcTaskName[0] = '\0';
// ... 复制名字、设置优先级等
// 4. 初始化栈(模拟第一次启动时的寄存器状态)
pxNewTCB->pxTopOfStack = pxPortInitialiseStack(
pxTopOfStack, pvTaskCode, pvParameters
);
// 5. 把任务加入就绪链表
prvAddTaskToReadyList(pxNewTCB);
return pdPASS;
}
这里有个关键步骤:初始化栈。你想想看,任务第一次被调度时,CPU从哪开始执行?答案就在栈里。pxPortInitialiseStack这个函数会把任务函数的地址、参数、初始的xPSR寄存器值都压入栈中。这样调度器第一次切换到这个任务时,从栈里弹出这些值,CPU就直接跳转到任务函数入口了。是不是很巧妙?
删除任务相对简单,调用vTaskDelete:
void vTaskDelete(TaskHandle_t xTaskToDelete) {
TCB_t *pxTCB;
// 如果传入NULL,表示删除自己
if (xTaskToDelete == NULL) {
pxTCB = pxCurrentTCB;
} else {
pxTCB = (TCB_t *)xTaskToDelete;
}
// 1. 从所有链表中移除
uxListRemove(&(pxTCB->xStateListItem));
uxListRemove(&(pxTCB->xEventListItem));
// 2. 释放栈空间
vPortFree(pxTCB->pxStack);
// 3. 释放TCB空间
vPortFree(pxTCB);
// 4. 触发一次调度(如果删除的是当前任务)
if (xTaskToDelete == NULL) {
taskYIELD();
}
}
最后说一句,任务创建和删除是RTOS最基础的操作。但越是基础,越容易出问题。我建议你在自己的开发板上,用xTaskCreate创建几个不同优先级的任务,然后用vTaskDelete动态删除它们,观察系统行为。只有亲手调过,才能真正理解TCB是怎么工作的。
下一章,我们会深入调度器的实现。到时候你会看到,调度器是如何在就绪链表里找到最高优先级的任务,然后完成上下文切换的。嗯,那才是RTOS真正精彩的地方。