任务管理基础:任务的概念、任务状态、任务优先级与TCB

大家好,我是你们的老朋友。今天我们来聊聊RTOS里最核心的东西——任务管理。说白了,任务就是RTOS的灵魂。没有任务,系统就是个空壳子。

我记得刚入行那会儿,总觉得任务就是“一个函数跑起来”。后来被现实狠狠教育了一顿——任务远没那么简单。你想想看,一个系统里同时跑十几个任务,谁先跑、谁后跑、谁卡住了怎么办?这些都得靠任务管理来解决。

任务到底是什么?

任务,在RTOS里就是一个独立的执行单元。它有自己的代码、自己的栈空间、自己的运行环境。每个任务看起来就像是一个“独立的小程序”,在CPU上轮流执行。

我习惯把任务比作“公司里的员工”。每个员工有自己的工位(栈空间)、自己的工作内容(任务函数)、自己的优先级(职位高低)。老板(调度器)负责安排谁什么时候干活。

一个典型的任务函数长这样:

void my_task(void *parameter)
{
    while(1)
    {
        // 任务要做的事情
        do_something();
        
        // 让出CPU,给其他任务机会
        vTaskDelay(10);
    }
}

嗯,这里要注意:任务函数里通常是个死循环。为什么?因为任务一旦返回,就被系统认为“结束了”,不会再被调度。除非你明确要创建一个一次性任务。

任务的五种状态

任务不是一直“在跑”的。它有五种状态,就像人有五种状态一样——工作、待命、睡觉、请假、消失。

状态 说明 我遇到过的场景
运行态 任务正在占用CPU执行 只有一个任务能处于此状态
就绪态 任务准备好了,就等CPU 优先级最高的就绪任务会被调度
阻塞态 任务在等某个事件 比如等延时、等信号量、等队列
挂起态 任务被暂停,不参与调度 调试时常用,或者临时禁用某个功能
删除态 任务已被销毁 资源被回收,不能再被调度

核心要点:任务在任何时刻只能处于一种状态。状态之间可以互相转换。比如从运行态变成阻塞态(等延时),从阻塞态变成就绪态(延时结束)。

我曾经在一个项目里踩过坑:一个任务在等串口数据,结果串口一直没来数据,任务就永远阻塞了。其他任务也受影响,整个系统卡死。后来我加了个超时机制,问题才解决。

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

优先级是RTOS调度的重要依据。数字越小优先级越高?还是数字越大优先级越高?这取决于具体的RTOS。FreeRTOS里,数字越大优先级越高。uC/OS里则相反。

我个人习惯用FreeRTOS,所以默认“数值大=优先级高”。

优先级的设计有个原则:不要把所有任务都设成同一个优先级。为什么?因为同优先级任务之间是时间片轮转,如果某个任务跑太久,其他任务就得不到CPU。

我的建议:关键任务(比如紧急响应)给高优先级,普通任务给中等优先级,后台任务(比如日志记录)给低优先级。这样系统响应快,又不会让低优先级任务饿死。

我曾经接手过一个项目,所有任务都是优先级5。结果一个任务里有个大循环,其他任务全被堵死了。改优先级后,系统立马顺畅了。

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

每个任务都有一个TCB。TCB里存着任务的所有信息:栈指针、状态、优先级、任务名、栈大小等等。你可以把TCB理解为任务的“身份证”和“档案袋”。

在FreeRTOS里,TCB的结构大致如下:

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;

你不需要记住所有字段,但要知道:TCB是调度器管理任务的核心数据结构。调度器就是通过遍历TCB列表,找到最高优先级的就绪任务,然后切换过去。

注意:TCB占用的内存是静态分配的(如果用静态创建)或者从堆里动态分配的(如果用动态创建)。嵌入式系统内存有限,别创建太多任务。我一般建议不超过20个任务,除非你的MCU内存很大。

任务创建与删除

创建任务用 xTaskCreate()xTaskCreateStatic()。前者动态分配TCB和栈,后者需要你提前准备好内存。

// 动态创建
TaskHandle_t myTaskHandle;
xTaskCreate(my_task, "My Task", 1024, NULL, 5, &myTaskHandle);

// 静态创建
StaticTask_t taskBuffer;
StackType_t taskStack[1024];
TaskHandle_t myTaskHandle2;
myTaskHandle2 = xTaskCreateStatic(my_task, "My Task2", 1024, 
                                   NULL, 5, taskStack, &taskBuffer);

删除任务用 vTaskDelete()。注意:删除后任务不能再被调度,TCB和栈空间会被释放(动态创建的情况)。

避坑指南:我曾经在中断服务函数里调用了vTaskDelete(),结果系统直接崩溃。后来才知道,中断里不能直接调用任务删除函数,得用消息队列通知一个任务去删。

任务状态转换实战

我们来看一个实际场景。假设有三个任务:

  • TaskA:优先级5,负责读取传感器
  • TaskB:优先级3,负责处理数据
  • TaskC:优先级1,负责发送日志

系统启动后,TaskA先运行。它读了一次传感器,然后调用 vTaskDelay(100) 进入阻塞态。调度器一看,TaskA阻塞了,那就让TaskB跑。TaskB处理完数据,也阻塞了(等下一个数据)。调度器再让TaskC跑。TaskC发完日志,也阻塞了(等下次发送时间)。

这时候三个任务都阻塞了?那CPU在干嘛?

答案是:CPU会运行空闲任务(Idle Task)。空闲任务优先级最低,没事干的时候才跑它。

100毫秒后,TaskA的延时结束,从阻塞态变成就绪态。调度器发现TaskA优先级最高,立马切换过去。TaskA又开始读传感器……如此循环。

这就是任务状态转换的典型过程。你想想看,如果没有RTOS,你得自己写一个超级循环来管理这些。有了RTOS,每个任务各司其职,代码清晰多了。

总结

任务管理是RTOS的基石。理解任务的状态、优先级和TCB,你就能写出稳定高效的多任务程序。记住:

  • 任务有五种状态,状态之间可以转换
  • 优先级决定谁先获得CPU
  • TCB是任务的管理数据结构
  • 创建和删除任务要小心,别在中断里乱搞

下一章我们会讲任务调度算法,看看调度器到底是怎么“选人”的。到时候我会分享一个我调试过的调度bug,保证让你印象深刻。

好了,今天就到这里。有问题欢迎在评论区留言,或者加我微信交流。咱们下期见!


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