任务管理基础:TCB、状态机与API

各位同学,今天我们来聊聊RTOS里最核心的东西——任务管理。说白了,任务就是RTOS里最小的调度单元。你写的每个线程、每个处理逻辑,最终都要包装成一个任务交给内核去跑。

我刚开始接触RTOS时,总觉得任务管理很简单,不就是创建几个线程嘛。直到有一次在工业控制器项目里,因为任务优先级设错了,导致整个系统死锁……嗯,从那以后我才真正重视起任务管理的每个细节。

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

每个任务在RTOS里都有一个对应的数据结构,叫任务控制块(Task Control Block,TCB)。你可以把它理解成任务的“身份证”或“档案袋”。内核不关心你的任务函数叫什么名字,它只认TCB。

一个典型的TCB长什么样?我直接给你看代码:

typedef struct tcb {
    uint32_t    *stack_ptr;      // 栈指针(当前SP值)
    uint32_t    *stack_base;     // 栈底地址
    uint32_t     stack_size;     // 栈大小(字节)
    uint8_t      priority;       // 任务优先级
    uint8_t      state;          // 任务状态
    char         name[16];       // 任务名称(调试用)
    void        *msg_queue;      // 消息队列指针
    uint32_t     delay_ticks;    // 延时剩余ticks
    struct tcb   *next;          // 链表指针
} TCB_t;

这里面每个字段都有讲究。我重点说几个:

  • stack_ptr / stack_base:每个任务有自己的栈,切换任务时就是换这个指针。我见过有人把栈大小设成1KB,结果任务里递归调用炸了栈——血的教训。
  • priority:优先级数值越小,优先级越高(这是常见做法,也有反的,看具体实现)。
  • state:任务当前处于什么状态,后面会细讲。
  • delay_ticks:任务调用延时函数时,内核靠这个字段倒计时。

核心要点:TCB是内核管理任务的唯一依据。创建任务 = 分配并初始化一个TCB;删除任务 = 回收TCB及其栈空间。

任务状态机——就绪/运行/阻塞/挂起

一个任务在它的生命周期里,会经历几种状态。我画了一张图,你一看就明白:

RTOS任务状态机 就绪(Ready) 等待CPU使用权 运行(Running) 正在执行代码 阻塞(Blocked) 等待事件/资源 挂起(Suspended) 人为暂停 调度器选择 时间片用完/被抢占 等待资源 事件到达/超时 调用挂起API 调用恢复API 说明: 就绪态和运行态是“可调度”状态;阻塞态和挂起态是“不可调度”状态。

这四种状态之间的转换,我总结成一张表:

当前状态 触发条件 下一状态
就绪 调度器选中该任务 运行
运行 时间片用完 / 更高优先级任务就绪 就绪
运行 等待信号量、队列、延时等 阻塞
阻塞 等待的事件到达 / 超时 就绪
运行 调用任务挂起API 挂起
挂起 调用任务恢复API 就绪

我的经验:调试时最常遇到的问题是任务“卡死”在阻塞态。你想想看,一个任务在等信号量,但没人释放它——这就是典型的死锁。我建议你在每个阻塞API调用后加超时参数,别用无限等待。

任务创建与删除API

每个RTOS都会提供创建和删除任务的接口。虽然各家API名字不同,但核心逻辑大同小异。我以最常见的接口为例:

// 创建任务
int task_create(
    TCB_t       *tcb,       // 用户提供的TCB缓冲区
    const char  *name,      // 任务名称
    void       (*entry)(void*), // 任务入口函数
    void        *param,     // 入口参数
    void        *stack,     // 栈缓冲区
    uint32_t     stack_size,// 栈大小
    uint8_t      priority   // 优先级
);

// 删除任务
int task_delete(TCB_t *tcb);

创建任务时,内核内部做了这几件事:

  1. 初始化TCB的各个字段(栈指针、优先级、状态等)。
  2. 初始化任务栈,把入口函数地址、返回地址、CPU寄存器初始值压栈。
  3. 将TCB插入就绪队列。
  4. 如果新任务优先级高于当前运行任务,触发调度。

删除任务时,内核要做的:

  1. 从所有队列(就绪、阻塞、挂起)中移除该TCB。
  2. 回收栈空间(如果是动态分配的)。
  3. 触发一次调度(因为当前任务可能被删了)。

注意:千万不要在中断服务函数里调用任务删除API!我曾经在一个项目中这么干过,结果中断返回后调度器直接崩溃。原因很简单——中断上下文和任务上下文不是一套寄存器组,强行删除会导致栈混乱。

任务优先级——谁先跑谁后跑

优先级是RTOS调度决策的核心依据。每个任务创建时都要指定一个优先级数值。常见的规则是:数值越小,优先级越高(也有反过来的,比如FreeRTOS)。

优先级设计有几个关键点:

  • 优先级数量:一般RTOS支持8~256级。太多了没必要,调度开销反而大。
  • 优先级反转:低优先级任务持有资源,高优先级任务等资源——这就是优先级反转。解决办法是优先级继承或优先级天花板协议。
  • 同优先级任务:如果两个任务优先级相同,一般采用时间片轮转调度。

我举个例子说明优先级的作用:

// 任务A:优先级5,处理按键输入
void task_A(void *param) {
    while(1) {
        wait_key();      // 阻塞等待按键
        process_key();   // 处理按键
    }
}

// 任务B:优先级3,处理显示刷新
void task_B(void *param) {
    while(1) {
        refresh_display();  // 刷新屏幕
        delay_ms(16);       // 约60fps
    }
}

// 任务C:优先级1,处理紧急报警
void task_C(void *param) {
    while(1) {
        wait_alarm();    // 等待报警信号
        handle_alarm();  // 立即处理
    }
}

你看,报警任务优先级最高(1),显示刷新次之(3),按键处理最低(5)。一旦报警触发,任务C会立即抢占其他任务。这就是优先级调度的核心思想——紧急的事情先办。

避坑指南:我曾经在一个多传感器数据采集项目里,把所有任务优先级都设成一样。结果某个传感器驱动里有个死循环,整个系统直接卡死。后来我把关键任务(如看门狗喂狗)优先级提到最高,才解决了问题。记住:优先级不是摆设,它是系统稳定性的最后一道防线。

小结

任务管理是RTOS的基石。TCB是任务的身份证,状态机描述了任务的“生老病死”,API是操作任务的工具,优先级决定了谁先获得CPU。这些概念你理解透了,后面学调度算法、同步机制就会轻松很多。