3、任务基础知识:任务状态机、TCB源码分析与优先级

好,咱们今天聊聊FreeRTOS里最核心的概念——任务。说白了,整个操作系统就是围绕任务转的。你想想看,如果没有任务,那RTOS跟裸机有什么区别?

我个人习惯,在学任何一个RTOS之前,先把它的任务模型搞清楚。因为后面所有的调度、同步、通信,都是建立在任务之上的。今天我们就从三个角度来解剖任务:它的状态怎么变、它的身份证长什么样、以及它怎么分三六九等。

3.1 任务状态机:任务的一生

一个任务在系统里,不是一直占着CPU跑的。它会在几个状态之间跳来跳去。FreeRTOS里任务有四种状态:运行态就绪态阻塞态挂起态

我刚开始学的时候,总觉得状态机这东西很抽象。后来在项目里调试一个任务不跑的问题,才真正体会到——你只要搞清楚任务当前在哪个状态,问题就解决了一半。

3.1.1 运行态(Running)

这个最好理解。任务正在使用CPU,代码正在被执行。在单核CPU上,同一时刻只能有一个任务处于运行态。嗯,这是常识。

3.1.2 就绪态(Ready)

任务已经准备好了,随时可以运行。但CPU正被别人占着,所以只能排队等着。就绪态的任务都在就绪列表里,调度器会从里面挑一个优先级最高的来运行。

3.1.3 阻塞态(Blocked)

任务在等某件事发生。比如等一个延时到期,或者等一个信号量、消息队列。这时候任务不参与CPU竞争,省资源。

我曾经在一个项目中,有个任务一直不执行,查了半天发现它调用了vTaskDelay,延时设成了portMAX_DELAY。说白了,它把自己阻塞死了,永远等不到超时。这种坑,踩过一次就记住了。

3.1.4 挂起态(Suspended)

任务被刻意暂停了。跟阻塞不一样,挂起态不依赖任何事件,纯粹是人为控制。调用vTaskSuspend()进入,调用vTaskResume()退出。

状态之间的转换,我画个简单的图帮你理解:

创建任务
    |
    v
就绪态 <---> 运行态
  |              |
  |              v
  |           阻塞态
  |              |
  v              v
挂起态 <--------

注意,从挂起态只能回到就绪态,不能直接到运行态。这是API的设计限制,记住就好。

3.2 任务控制块TCB:任务的身份证

每个任务在FreeRTOS里都有一个对应的结构体,叫任务控制块(Task Control Block,TCB)。它记录了任务的所有信息:栈指针、优先级、状态、任务名等等。

说白了,TCB就是任务的身份证。调度器通过TCB来管理任务,而不是直接操作任务函数。

3.2.1 TCB结构体源码分析

我们直接看源码,在tasks.c里:

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;

这里我重点说几个字段:

  • pxTopOfStack:这是调度器切换任务时用的。每次任务切换,CPU的寄存器值会保存到当前任务的栈里,然后从新任务的栈里恢复。这个指针指向的就是栈里保存寄存器值的位置。
  • xStateListItem:任务当前在哪个状态列表里?就绪列表、阻塞列表还是挂起列表?就是通过这个ListItem挂接的。
  • uxPriority:任务的优先级,数值越大优先级越高。这个后面会细说。

重点提醒:TCB是静态分配的,还是在堆上动态分配的?这取决于你的配置。如果configSUPPORT_DYNAMIC_ALLOCATION为1,就用pvPortMalloc分配。否则,你需要自己提供静态的TCB内存。

3.2.2 TCB的创建过程

当你调用xTaskCreate()时,系统会做这几件事:

  1. 分配TCB内存(动态或静态)
  2. 初始化TCB的各个字段
  3. 初始化任务栈,把初始的寄存器值压栈
  4. 把任务挂到就绪列表里

我记得有一次,我创建了很多任务,结果系统跑着跑着就崩了。查了半天,发现是栈空间分配太小,任务一运行就栈溢出,把TCB给踩了。嗯,从那以后我养成了一个习惯:每个任务至少给512字节的栈,调试阶段给1024字节。

3.3 任务优先级:谁先跑谁后跑

FreeRTOS是一个抢占式实时操作系统。什么意思?就是高优先级的任务可以打断低优先级任务的执行。

3.3.1 优先级数值

优先级用UBaseType_t表示,范围从0到configMAX_PRIORITIES - 1。数值越大,优先级越高。0是最低优先级。

你想想看,如果两个任务优先级相同怎么办?那就轮着来,时间片轮转调度。每个任务跑一个时间片,然后换下一个。

3.3.2 优先级反转问题

这里有个经典问题——优先级反转。低优先级任务拿着一个资源,高优先级任务在等这个资源,结果中优先级任务抢占了CPU,导致高优先级任务迟迟拿不到资源。

我曾经在一个项目中遇到过这个问题。一个传感器数据采集任务优先级很高,但它要等一个I2C总线锁。结果一个低优先级的显示任务拿着锁不放,中间还有个网络任务在疯狂跑。传感器数据一直采集不到,导致控制算法失效。后来用了优先级继承机制才解决。

避坑指南:不要把所有任务都设成同一个优先级。那样就退化成轮询了,实时性没法保证。也不要设太多优先级等级,一般8-16级就够用。优先级太多,调度器的开销会变大。

3.3.3 优先级配置建议

优先级范围 典型用途 说明
最高(如7) 中断处理、时间关键任务 响应时间要求极短
中高(如5-6) 传感器采集、控制算法 周期性、有实时要求
中等(如3-4) 数据处理、协议栈 可以容忍少量延迟
低(如1-2) 显示、日志、后台任务 不着急,有空再跑
最低(0) 空闲任务 系统自动创建,别手动用

3.4 小结

今天我们把任务的基础知识过了一遍。任务状态机帮你理解任务在系统里的生命周期,TCB是任务的身份证,优先级决定了任务之间的竞争关系。这三样东西,是理解FreeRTOS调度的基石。

下一章,我们会深入调度器的实现,看看FreeRTOS到底是怎么从就绪列表里挑任务来跑的。到时候你会发现,TCB里的那些ListItem是怎么串起来的。

个人建议:学完这章,你可以打开FreeRTOS源码,找到tskTaskControlBlock结构体定义,自己一行一行读一遍。再对照着xTaskCreate()的实现,看看TCB是怎么被初始化的。这样印象会深很多。