任务与任务状态:任务的概念、任务状态机、任务控制块

大家好,我是你们的嵌入式系统讲师。今天我们来聊聊RTOS里最核心的概念——任务。说实话,很多初学者一上来就被各种状态搞晕了。什么就绪、运行、阻塞、挂起……听着就头大。别急,我们一个一个拆开讲。

任务到底是什么?

任务,说白了就是一段独立的程序。它有自己的代码、自己的栈、自己的优先级。你可以把它想象成一个独立的小人,在CPU里跑来跑去。

我习惯把任务叫做“线程”或者“进程”,但在RTOS里,我们通常叫它“任务”。每个任务都像是一个独立的单片机程序,只不过它们共享同一个CPU。

举个例子。你写了一个温度采集程序,又写了一个LED闪烁程序。在裸机里,你得自己安排谁先跑谁后跑。但在RTOS里,这两个就是两个独立的任务。系统会自动帮它们分配CPU时间。

核心要点:任务 = 独立的执行流 + 独立的栈空间 + 独立的优先级

任务状态机:五个状态的流转

任务不是一直运行的。它会在不同状态之间切换。我画了一张图,你看一眼就明白了。

任务状态机流转图 就绪 Ready 运行 Running 阻塞 Blocked 挂起 Suspended 创建/删除 Create/Delete 调度器选择 时间片用完/抢占 等待资源/延时 资源就绪/延时到 vTaskSuspend() vTaskResume() xTaskCreate() vTaskDelete()

这张图我画了好一会儿。你看,任务的状态流转其实不复杂。核心就四个状态:就绪、运行、阻塞、挂起。再加上创建和删除两个边界操作。

四个状态详解

1. 就绪状态(Ready)

任务已经准备好了,随时可以运行。它就在那里等着,等调度器叫它。我打个比方:就像排队买奶茶,你已经站在队伍里了,就等轮到你。

就绪状态的任务,它的所有资源都准备好了。栈空间分配好了,优先级设置好了,就等CPU来执行它。

2. 运行状态(Running)

任务正在占用CPU。注意,在单核CPU里,同一时刻只能有一个任务在运行。这就是所谓的“单核单任务”。

我在项目中遇到过一个问题:一个任务占用了太多CPU时间,导致其他任务饿死了。后来我加了个延时,问题就解决了。你想想看,如果一个人一直霸占着奶茶店的操作台,其他人不就等死了吗?

3. 阻塞状态(Blocked)

任务在等某个事件。比如等一个信号量、等一个消息队列、或者等一个延时。这时候任务不占用CPU,系统会把它放到阻塞队列里。

阻塞状态其实是个好东西。它让CPU能去干别的事。我经常跟学员说:别让你的任务空转,该阻塞时就阻塞

小技巧:如果你发现CPU占用率很高,但系统响应很慢,很可能是某个任务在空转。加个vTaskDelay()就能解决。

4. 挂起状态(Suspended)

任务被强制暂停了。它不参与调度,也不占用任何资源。就像你让一个人先别动,站在旁边等着。

挂起状态我用的不多,但偶尔会用。比如调试的时候,我想暂停某个任务看看它的状态。或者系统进入低功耗模式时,我会挂起所有非关键任务。

任务控制块(TCB)

每个任务都有一个TCB。TCB是什么?说白了就是任务的身份证。系统通过TCB来管理所有任务。

我习惯把TCB想象成一个档案袋。里面装着任务的所有信息:

字段 说明 我的经验
任务栈指针 指向任务自己的栈空间 栈大小一定要算好,我吃过栈溢出的亏
任务优先级 数值越小优先级越高 别把所有任务设成同一优先级,会出问题
任务状态 当前处于哪个状态 调试时经常看这个字段
任务名称 给任务起个名字 名字要有意义,别叫task1、task2
事件等待列表 在等什么事件 阻塞任务会挂在这里
任务句柄 任务的唯一标识 操作任务时要用到它

在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里的字段!系统API才是正确的操作方式。我曾经见过有人直接改优先级,结果系统崩溃了。

状态切换的实战场景

我举个例子。假设你有三个任务:

  • Task_A:采集温度,优先级2
  • Task_B:控制LED,优先级1
  • Task_C:串口通信,优先级3

系统启动后,三个任务都进入就绪状态。调度器一看,Task_B优先级最高,先让它运行。Task_B运行了一会儿,调用了vTaskDelay(100),进入阻塞状态。

这时候调度器再看,Task_A优先级次高,让它运行。Task_A运行中,突然来了个串口中断,Task_C被唤醒。但Task_C优先级最低,还是得等Task_A运行完。

嗯,这里要注意:优先级高的任务不一定一直运行。如果高优先级任务阻塞了,低优先级任务就有机会了。

避坑指南

我曾经犯过一个错误:把三个任务都设成同一优先级。结果系统运行起来后,任务之间互相抢CPU,谁都没法好好干活。后来我才明白,同一优先级的任务靠时间片轮转调度,但时间片太短会导致频繁切换,效率很低。

还有一次,我写了一个任务,里面有个while(1)循环,没有任何阻塞。结果这个任务把CPU占满了,其他任务全饿死了。后来我加了个vTaskDelay(1),问题就解决了。

核心原则:每个任务都要有阻塞点。要么等资源,要么等延时。别让任务空转。

好了,任务和任务状态就讲到这里。记住这张状态机图,记住TCB的结构,你就能理解RTOS是怎么管理任务的了。下次我们聊聊任务优先级和调度策略,到时候你会更清楚为什么优先级这么重要。