2. 任务创建与管理:TCB、任务栈与状态机
任务管理,说白了就是RTOS的心脏。你想想看,一个嵌入式系统里跑着好几个任务,谁先执行、谁等着、谁被踢出去睡觉——这些都得靠一套严谨的机制来管。今天我就带你把这套机制拆开看看。
2.1 任务控制块(TCB)——任务的身份证
每个任务在RTOS里都有一个“档案”,这就是TCB。我习惯把TCB想象成任务的身份证,里面存着任务的所有关键信息。
| TCB成员 | 作用 | 我踩过的坑 |
|---|---|---|
| 任务栈指针 | 指向任务私有栈顶 | 栈指针算错一位,直接硬 fault |
| 任务状态 | 就绪/运行/阻塞/挂起 | 状态切换漏了,任务“消失” |
| 优先级 | 决定调度顺序 | 两个任务同优先级,轮询卡死 |
| 任务入口函数 | 任务执行的代码地址 | 函数返回了,系统崩了 |
| 任务名称 | 调试用标识 | 名字太短,查log一脸懵 |
核心要点:TCB是RTOS调度器的操作对象。调度器不直接碰你的任务函数,它只操作TCB。所以TCB结构设计得是否合理,直接决定了系统的稳定性和可扩展性。
2.2 任务栈——每个任务的私人空间
任务栈是每个任务独享的内存区域。为什么需要私有栈?因为任务切换时,CPU的寄存器、局部变量、函数调用返回地址都得找个地方存起来。这个“地方”就是任务栈。
我在项目中遇到过一个问题:一个任务栈只给了256字节,结果这个任务里有个递归函数,栈溢出了,把相邻任务的TCB给覆盖了。排查了整整两天才找到原因。嗯,从那以后我定了个规矩——任务栈大小至少给足,宁多勿少。
栈大小估算建议:
- 简单任务(只做标志位翻转):64-128字节
- 中等任务(有局部数组、调用2-3层函数):256-512字节
- 复杂任务(有协议解析、文件操作):1024字节起步
我曾经因为栈开小了,产品在客户现场偶发死机,后来统一翻倍,问题消失。
2.3 任务状态机——就绪/运行/阻塞/挂起
任务在RTOS里不是一直跑着的。它有四种状态,来回切换。我画了一张图,你看一眼就明白了。
避坑指南:我曾经在项目里犯过一个低级错误——在中断服务函数里调用了阻塞型API(比如等待信号量)。结果中断一触发,任务就被阻塞了,但中断上下文没有调度器来切换任务,系统直接死锁。记住:中断里别阻塞!
2.4 任务创建函数详解
任务创建是使用RTOS的第一步。不同RTOS的API名字不一样,但核心参数大同小异。我以最常用的FreeRTOS为例,给你拆解一下。
// FreeRTOS 任务创建函数原型
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度(单位:字)
void *pvParameters, // 任务参数
UBaseType_t uxPriority, // 优先级(0最低,数值越大优先级越高)
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
每个参数都有讲究。我重点说几个容易出问题的:
- 栈深度:单位是“字”,不是字节。32位处理器上一个字是4字节。所以填128,实际分配512字节。
- 优先级:别把0留给空闲任务。我习惯把用户任务优先级从1开始分配,0留给系统空闲任务。
- 任务句柄:如果不关心任务句柄,可以传NULL。但建议还是留着,后面删除或修改优先级时要用。
我的创建习惯:
TaskHandle_t xHandleLED = NULL;
xTaskCreate(
vLEDTask, // 任务函数
"LED", // 名字短一点,方便调试
128, // 栈深度128字 = 512字节
NULL, // 不需要参数
2, // 优先级2
&xHandleLED // 保存句柄
);
2.5 任务删除与动态管理
任务不是创建了就永远存在的。有时候我们需要动态删除任务,比如某个功能模块不再使用了,或者任务执行完毕需要自我销毁。
任务删除有两种方式:
| 方式 | 调用方法 | 注意事项 |
|---|---|---|
| 自我删除 | 任务内部调用 vTaskDelete(NULL) |
删除后不会立即释放栈,由空闲任务回收 |
| 外部删除 | 其他任务调用 vTaskDelete(xHandle) |
确保被删除任务不在临界区 |
重要提醒:我曾经在项目里遇到一个诡异问题——删除了一个任务,但内存没释放。查了半天才发现,任务删除后,TCB和栈内存是由空闲任务(Idle Task)来回收的。如果空闲任务没机会运行,内存就永远漏在那了。所以,确保空闲任务能获得CPU时间!
动态管理还有一个常见场景:任务重启。比如某个任务因为异常退出了,你需要重新创建它。我的做法是:
void vWatchdogTask(void *pvParameters) {
TaskHandle_t xTaskToMonitor;
UBaseType_t uxLastWakeTime = xTaskGetTickCount();
for(;;) {
// 检查被监控任务是否还活着
if(eTaskGetState(xTaskToMonitor) == eDeleted) {
// 任务已删除,重新创建
xTaskCreate(vMonitoredTask, "Monitored", 256, NULL, 3, &xTaskToMonitor);
}
vTaskDelayUntil(&uxLastWakeTime, pdMS_TO_TICKS(1000));
}
}
总结一下:任务管理是RTOS的基石。TCB是任务的身份证,栈是任务的私人空间,状态机决定了任务什么时候该干什么。创建任务时参数要仔细,删除任务时注意内存回收。这些基本功打扎实了,后面学调度、同步、通信才会顺手。