任务创建与管理:TCB、栈配置、优先级与状态迁移

各位同学,今天我们来聊聊任务创建与管理。这是多任务调度的基石,说白了,就是操作系统怎么认识你的任务、怎么给它分配资源、怎么决定谁先跑。

我个人习惯把任务比作一个「独立的小工人」。每个工人都有自己的工具箱(栈)、身份牌(TCB)、级别(优先级),以及当前状态(在干活、在等料、还是被叫停了)。

一、任务控制块(TCB)——任务的身份证

TCB 是什么?它就是操作系统用来管理任务的数据结构。每个任务创建时,内核都会分配一个 TCB。里面存了什么?我列一下关键字段:

  • 任务栈指针:指向当前栈顶,保存现场用
  • 任务状态:就绪、运行、阻塞、挂起
  • 优先级:数值越小优先级越高(常见做法)
  • 任务入口函数:任务从哪开始执行
  • 任务名称:调试时方便看
  • 延时/超时计数器:用于阻塞等待

我在项目中遇到过一个问题:某个任务莫名其妙不跑了。查了半天,发现是 TCB 被意外覆盖了。从那以后,我坚持把 TCB 放在独立的 RAM 段,并且加上保护。

核心要点:TCB 是内核与任务之间的唯一接口。丢了 TCB,任务就丢了。

二、任务栈配置——给工人准备工具箱

每个任务都需要自己的栈空间。为什么?因为函数调用、局部变量、中断嵌套都要用栈。如果栈不够大,任务就会跑飞,而且很难查。

我建议栈大小这样估算:

  • 任务函数调用深度 × 每次调用需要的字节数
  • 加上中断嵌套的最大深度
  • 再加 20% 的安全余量

举个例子,一个简单的 LED 闪烁任务,128 字节可能就够了。但一个带协议栈的通信任务,至少 1024 字节起步。

我的经验:我曾经在一个项目中把栈设得太小,结果任务运行几小时后随机崩溃。后来用栈水印检测,才发现栈溢出。嗯,现在我的每个任务都加栈溢出检测。

三、任务优先级设定——谁说了算?

优先级决定了任务被调度的顺序。实时系统里,优先级就是命根子。

常见的做法:

  • 抢占式调度:高优先级任务就绪,立刻抢 CPU
  • 时间片轮转:同优先级任务轮流跑

优先级设定有几个原则:

  1. 时间敏感的任务(如电机控制)给最高优先级
  2. 通信任务(如 CAN、UART)给中等优先级
  3. 后台任务(如日志、显示)给最低优先级

注意:优先级反转是个大坑。低优先级任务占着资源,高优先级任务等它释放,中间还有个中优先级任务插队。我曾经被这个问题折腾了两天,最后用优先级继承协议解决了。

四、任务状态迁移——任务的生老病死

任务有四种基本状态:就绪、运行、阻塞、挂起。它们之间的迁移关系,我画了一张图:

就绪 运行 阻塞 挂起 调度 时间片用完 等待资源 资源就绪 挂起调用 恢复 挂起调用 恢复并等待

这张图你看懂了吗?我解释一下关键路径:

  • 就绪 → 运行:调度器选中你,你就跑
  • 运行 → 就绪:时间片用完,或者被更高优先级任务抢了
  • 运行 → 阻塞:你等某个资源(信号量、队列、延时)
  • 阻塞 → 就绪:资源到了,你回到就绪队列排队
  • 运行 → 挂起:主动或被动暂停
  • 挂起 → 就绪:被恢复后重新排队

避坑指南:我曾经在项目里让一个高优先级任务频繁阻塞,结果低优先级任务永远得不到 CPU。后来我加了看门狗,才意识到问题。记住:阻塞不是休息,是等待资源。

五、代码示例:创建一个任务

下面是一个 FreeRTOS 风格的任务创建示例:

// 任务栈定义
#define TASK_STACK_SIZE 256
StackType_t taskStack[TASK_STACK_SIZE];

// 任务控制块
StaticTask_t taskTCB;

// 任务函数
void vTaskFunction(void *pvParameters) {
    while(1) {
        // 任务逻辑
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 创建任务
TaskHandle_t xTaskHandle = NULL;
xTaskHandle = xTaskCreateStatic(
    vTaskFunction,      // 任务函数
    "MyTask",           // 任务名称
    TASK_STACK_SIZE,    // 栈大小
    NULL,               // 参数
    2,                  // 优先级(0最高)
    taskStack,          // 栈指针
    &taskTCB           // TCB指针
);

if (xTaskHandle != NULL) {
    // 任务创建成功
    vTaskStartScheduler();
}

这段代码里,我手动分配了栈和 TCB。为什么不用动态分配?因为工业现场,你想想看,动态分配可能失败,而且碎片化严重。我习惯静态分配,心里踏实。

六、任务状态迁移实战

最后,我分享一个实际场景:

一个数据采集任务,优先级设为 3。它平时在阻塞状态,等待定时器触发。触发后进入就绪,调度器让它运行。运行中它要读取 ADC,如果 ADC 忙,它再次阻塞。读完后发信号量给处理任务,然后继续阻塞等下一次触发。

这个流程里,任务在就绪、运行、阻塞之间来回切换。挂起状态很少用,一般用于调试或低功耗模式。

小技巧:调试时,我经常用挂起/恢复来模拟任务异常,测试看门狗是否生效。嗯,这招挺管用的。

好了,任务创建与管理就讲到这里。记住 TCB 是身份证、栈是工具箱、优先级是级别、状态迁移是生命周期。把这四个点吃透,多任务调度你就入门了。


公众号:蓝海资料掘金营,微信deep3321