任务状态与切换:运行、就绪、阻塞、挂起
各位做嵌入式开发的朋友,咱们今天来聊聊 FreeRTOS 里任务的那些状态。说白了,任务就像咱们公司里的员工——有时候在干活(运行),有时候等着分配任务(就绪),有时候在等某个资源(阻塞),还有时候被强制放假(挂起)。
我记得刚接触 RTOS 那会儿,总觉得任务状态这玩意儿太抽象。后来在项目里调一个串口通信的 bug,折腾了两天才发现是任务状态搞错了。从那以后,我对这四种状态的理解就深刻多了。
任务的四种基本状态
FreeRTOS 的任务状态,其实就四种。我习惯用一个表格来对比,这样最清楚:
| 状态 | 英文 | 含义 | 典型场景 |
|---|---|---|---|
| 运行 | Running | 任务正在占用 CPU 执行代码 | 任务函数正在执行中 |
| 就绪 | Ready | 任务已准备好运行,但 CPU 被别的任务占着 | 高优先级任务在跑,低优先级等着 |
| 阻塞 | Blocked | 任务在等待某个事件或超时 | 等待信号量、队列、延时 |
| 挂起 | Suspended | 任务被强制暂停,不参与调度 | 调试时暂停某个任务 |
关键点:同一时刻,一个 CPU 核心上只能有一个任务处于运行状态。其他任务要么等着,要么睡着,要么被关禁闭。
状态切换的时机
任务不会无缘无故变状态。我总结了一下,切换时机主要有这么几个:
- 任务创建时:新任务进入就绪态
- 调度器启动时:最高优先级的就绪任务进入运行态
- 任务主动让出 CPU:调用
taskYIELD()或阻塞 API - 时间片耗尽:同优先级任务轮流使用 CPU
- 等待的事件发生:阻塞态变回就绪态
- 调用挂起/恢复 API:进入或退出挂起态
嗯,这里要注意一点——挂起态比较特殊。只有通过 vTaskSuspend() 和 vTaskResume() 才能进出,其他事件触发不了它。
调度器的工作原理
调度器是 FreeRTOS 的大脑。它的工作说白了就一句话:从就绪列表里挑一个最该跑的任务,把 CPU 给它。
FreeRTOS 用的是抢占式调度 + 时间片轮转。我解释一下这两个东西:
- 抢占式调度:高优先级任务一旦就绪,立马抢走 CPU。低优先级任务?不好意思,你先等着。
- 时间片轮转:同优先级任务,每人分一个时间片(默认 1 个 tick),轮流跑。
我的经验:在 STM32 上做项目时,我习惯把实时性要求高的任务(比如电机控制)设成高优先级,把显示、日志这类任务放低优先级。这样关键任务不会被耽误。
任务切换的完整流程
任务切换不是随便切切的。每次切换,调度器都要干这几件事:
- 保存当前任务的上下文(寄存器、堆栈指针等)
- 从就绪列表中找到最高优先级的任务
- 恢复新任务的上下文
- 跳转到新任务继续执行
这个过程在 FreeRTOS 里叫 PendSV 异常。我刚开始看源码时,觉得 PendSV 这部分最绕。后来画了个流程图,一下子就清楚了。
避坑指南:我曾经在一个项目里把某个任务的优先级设得特别高,结果它一直占着 CPU,低优先级的任务饿死了。后来加了 vTaskDelay() 才解决。记住:优先级越高,责任越大。
状态转换图
下面这张 SVG 图,把四种状态和切换路径画得明明白白。建议你保存下来,写代码时对照着看。
实际项目中的状态管理
讲个我自己的例子。去年做的一个 STM32F407 项目,有 5 个任务:
- 传感器采集任务(优先级 3):每 10ms 采集一次数据
- 数据处理任务(优先级 2):处理采集到的数据
- 通信任务(优先级 2):通过串口发送数据
- 显示任务(优先级 1):刷新 OLED 屏幕
- 看门狗喂狗任务(优先级 0):定时喂狗
你想想看,如果我把传感器采集任务设成死循环不阻塞,那其他任务全得饿死。所以我让它在采集完数据后调用 vTaskDelay(10) 进入阻塞态,这样 CPU 就能分给其他任务了。
核心原则:不要让任何任务长时间占用 CPU。该阻塞时就阻塞,该让就让。这是 RTOS 编程的基本素养。
调度器的工作细节
FreeRTOS 的调度器,核心数据结构是就绪列表。每个优先级对应一个链表,同优先级的任务串在一起。
调度器每次做决策时,就做两件事:
- 从高优先级往下找,找到第一个非空的就绪列表
- 取出列表头部的任务,切换过去
这个算法叫位图调度,效率极高。FreeRTOS 用一个 32 位的变量来标记哪些优先级有就绪任务,然后用一条指令就能找到最高优先级的那个。我在 STM32 上测过,一次调度决策只需要几十个 CPU 周期。
小技巧:如果你想知道当前任务处于什么状态,可以用 uxTaskGetTaskState() 这个 API。调试时特别好用。
总结一下
任务状态和切换,是理解 FreeRTOS 的基石。记住这几点:
- 四种状态:运行、就绪、阻塞、挂起
- 切换时机:时间片、阻塞、事件、挂起/恢复
- 调度器:抢占式 + 时间片轮转
- 核心原则:别占着 CPU 不放
嗯,这些概念搞清楚了,后面学任务通信、信号量、队列这些,就会轻松很多。咱们下节课见。