任务状态与切换:运行、就绪、阻塞、挂起

各位做嵌入式开发的朋友,咱们今天来聊聊 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 上做项目时,我习惯把实时性要求高的任务(比如电机控制)设成高优先级,把显示、日志这类任务放低优先级。这样关键任务不会被耽误。

任务切换的完整流程

任务切换不是随便切切的。每次切换,调度器都要干这几件事:

  1. 保存当前任务的上下文(寄存器、堆栈指针等)
  2. 从就绪列表中找到最高优先级的任务
  3. 恢复新任务的上下文
  4. 跳转到新任务继续执行

这个过程在 FreeRTOS 里叫 PendSV 异常。我刚开始看源码时,觉得 PendSV 这部分最绕。后来画了个流程图,一下子就清楚了。

避坑指南:我曾经在一个项目里把某个任务的优先级设得特别高,结果它一直占着 CPU,低优先级的任务饿死了。后来加了 vTaskDelay() 才解决。记住:优先级越高,责任越大。

状态转换图

下面这张 SVG 图,把四种状态和切换路径画得明明白白。建议你保存下来,写代码时对照着看。

FreeRTOS 任务状态转换图 FreeRTOS 任务状态转换图 运行态 就绪态 阻塞态 挂起态 调度器选中 时间片耗尽/被抢占 等待事件/延时 事件发生 vTaskSuspend() vTaskResume() vTaskSuspend() vTaskSuspend() 说明: 1. 实线箭头表示常见切换路径,虚线表示挂起相关路径 2. 只有就绪态的任务才能被调度器选中进入运行态 3. 挂起态只能通过 vTaskSuspend() 进入,vTaskResume() 恢复 4. 阻塞态任务在等待的事件发生后,先回到就绪态,再等待调度

实际项目中的状态管理

讲个我自己的例子。去年做的一个 STM32F407 项目,有 5 个任务:

  • 传感器采集任务(优先级 3):每 10ms 采集一次数据
  • 数据处理任务(优先级 2):处理采集到的数据
  • 通信任务(优先级 2):通过串口发送数据
  • 显示任务(优先级 1):刷新 OLED 屏幕
  • 看门狗喂狗任务(优先级 0):定时喂狗

你想想看,如果我把传感器采集任务设成死循环不阻塞,那其他任务全得饿死。所以我让它在采集完数据后调用 vTaskDelay(10) 进入阻塞态,这样 CPU 就能分给其他任务了。

核心原则:不要让任何任务长时间占用 CPU。该阻塞时就阻塞,该让就让。这是 RTOS 编程的基本素养。

调度器的工作细节

FreeRTOS 的调度器,核心数据结构是就绪列表。每个优先级对应一个链表,同优先级的任务串在一起。

调度器每次做决策时,就做两件事:

  1. 从高优先级往下找,找到第一个非空的就绪列表
  2. 取出列表头部的任务,切换过去

这个算法叫位图调度,效率极高。FreeRTOS 用一个 32 位的变量来标记哪些优先级有就绪任务,然后用一条指令就能找到最高优先级的那个。我在 STM32 上测过,一次调度决策只需要几十个 CPU 周期。

小技巧:如果你想知道当前任务处于什么状态,可以用 uxTaskGetTaskState() 这个 API。调试时特别好用。

总结一下

任务状态和切换,是理解 FreeRTOS 的基石。记住这几点:

  • 四种状态:运行、就绪、阻塞、挂起
  • 切换时机:时间片、阻塞、事件、挂起/恢复
  • 调度器:抢占式 + 时间片轮转
  • 核心原则:别占着 CPU 不放

嗯,这些概念搞清楚了,后面学任务通信、信号量、队列这些,就会轻松很多。咱们下节课见。