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里不是一直跑着的。它有四种状态,来回切换。我画了一张图,你看一眼就明白了。

任务状态机流转图 就绪 等待CPU 运行 占用CPU 阻塞 等待事件/资源 挂起 人为暂停 调度器选中 时间片用完/抢占 等待资源 资源就绪 调用挂起API 调用恢复API 关键规则:同一时刻只有一个任务处于“运行”状态 其他任务要么就绪排队,要么阻塞等资源,要么被挂起

避坑指南:我曾经在项目里犯过一个低级错误——在中断服务函数里调用了阻塞型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是任务的身份证,栈是任务的私人空间,状态机决定了任务什么时候该干什么。创建任务时参数要仔细,删除任务时注意内存回收。这些基本功打扎实了,后面学调度、同步、通信才会顺手。