news 2026/10/21 21:44:12

FreeRTOS 任务调度机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FreeRTOS 任务调度机制

一. FreeRTOS 中的任务状态

一个任务通常会处于以下四种状态之一。

1.1 运行态 Running

当前正在使用 CPU 执行的任务处于运行态。

在单核 MCU 中,同一时刻通常只有一个任务处于运行态。

1.2 就绪态 Ready

任务已经具备运行条件,但当前没有获得 CPU。

例如:

  • 没有等待事件;

  • 没有执行延时;

  • 没有被挂起;

  • 没有被其他同步对象阻塞。

就绪态任务会被放入对应优先级的就绪链表中。

1.3 阻塞态 Blocked

任务正在等待某个时间或事件,例如:

vTaskDelay(pdMS_TO_TICKS(100));

或者:

xQueueReceive(queue, &data, portMAX_DELAY);

处于阻塞态的任务不会参与正常调度。只有延时时间到达,或者等待的事件发生后,任务才会重新进入就绪态。

1.4 挂起态 Suspended

任务被显式挂起后,不会参与调度:

vTaskSuspend(taskHandle);

恢复任务需要调用:

vTaskResume(taskHandle);

阻塞态和挂起态的区别在于:

  • 阻塞态通常有明确的唤醒条件;

  • 挂起态不会因为时间到达自动恢复;

  • 挂起态任务必须由其他代码显式恢复。


二. FreeRTOS 如何管理任务

FreeRTOS 内核并不是把所有任务放在一个普通数组中,而是使用多个链表管理不同状态的任务。

2.1 就绪链表 Ready List

FreeRTOS 通常使用:

pxReadyTasksLists[]

保存就绪态任务。

它是一个按照优先级索引的链表数组:

pxReadyTasksLists[0] -> 优先级 0 的任务 pxReadyTasksLists[1] -> 优先级 1 的任务 pxReadyTasksLists[2] -> 优先级 2 的任务 ...

同一个优先级可以有多个任务。每个链表节点通常对应一个任务控制块 TCB。

调度器首先找到最高优先级的非空就绪链表,然后从这个链表中选择任务运行。

2.2 延时链表 Delayed List

延时任务通常由延时链表管理,相关指针常见为:

pxDelayedTaskList

任务调用以下 API 后,通常会进入延时链表:

vTaskDelay(); vTaskDelayUntil();

任务进入延时链表后,不再参与就绪任务竞争,直到延时时间到达。

FreeRTOS 实际实现中通常还会维护另一个用于处理系统节拍溢出的延时链表。应用层不需要直接操作这些链表,但理解这一点有助于阅读tasks.c源码。

2.3xPendingReadyList

xPendingReadyList

它主要用于调度器被挂起期间,某个中断服务函数将任务唤醒的场景。

因为调度器暂时不能立即处理任务状态切换,所以被唤醒的任务会暂时放入这个链表。调度器恢复后,再把这些任务转移到对应的就绪链表。

因此,截图中提到的三个重要概念可以概括为:

链表或结构作用
pxReadyTasksLists[]保存可以立即运行的任务
pxDelayedTaskList保存正在等待延时的任务
xPendingReadyList保存调度器暂时无法立即转入就绪态的任务

三. 调度器如何选择任务

FreeRTOS 选择任务时,并不是简单地在所有任务中逐个查找,而是通过“按优先级组织的就绪链表”完成任务选择。

3.1 就绪任务的组织方式

FreeRTOS 使用一个链表数组保存就绪任务:

pxReadyTasksLists[0] pxReadyTasksLists[1] pxReadyTasksLists[2] ...

数组下标代表任务优先级。

例如:

pxReadyTasksLists[3] -> TaskA -> TaskB pxReadyTasksLists[2] -> TaskC pxReadyTasksLists[1] -> 空 pxReadyTasksLists[0] -> IdleTask

这表示:

  • TaskA 和 TaskB 的优先级为 3;

  • TaskC 的优先级为 2;

  • IdleTask 的优先级为 0。

只有处于就绪态的任务才会放入pxReadyTasksLists[]。阻塞态任务会被移入延时链表或相应事件对象的等待链表,因此不会参与当前的任务选择。


3.2 任务选择的基本过程

调度器选择任务时,可以抽象为以下过程:

最高优先级 = 找到最高的非空就绪链表; 当前任务 = 从该链表中选择下一个任务; pxCurrentTCB = 当前任务;

也就是说,调度器首先比较优先级,而不是先比较任务创建顺序。

如果最高优先级为 3,调度器会优先处理:

pxReadyTasksLists[3]

只有当这个链表为空时,才会继续查找优先级 2、优先级 1 等更低优先级链表。

在未启用优化选择的情况下,内核可能从最高优先级开始向下检查链表。某些移植层会使用优先级位图快速定位最高优先级,从而避免逐级遍历。

因此,具体查找代码会随 FreeRTOS 版本和移植层变化,但调度原则不变:

只要有更高优先级的就绪任务,调度器就不会选择低优先级任务运行。。


3.3 相同优先级任务如何选择

如果最高优先级链表中只有一个任务,调度器直接选择它。

如果链表中有多个同优先级任务,例如:

pxReadyTasksLists[0] -> Task1 -> Task2 -> Task3

FreeRTOS 会通过链表中的当前位置选择下一个任务。这个过程可以理解为:

Task1 -> Task2 -> Task3 -> Task1 -> ...

这种轮转通常通过链表游标完成。内核每次从同优先级就绪链表中选择下一个节点,从而实现同优先级任务之间的轮流执行。

但是否在每个系统节拍都发生轮转,还取决于配置:

#define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1

当满足以下条件时,同优先级任务通常可以进行时间片轮转:

启用了抢占式调度;

时间片轮转只发生在相同优先级任务之间,不能让低优先级任务抢占高优先级任务。


3.4 任务阻塞后,调度器如何重新选择

假设当前运行的是 Task1:

void Task1(void *argument) { for (;;) { printf("Task1\r\n"); vTaskDelay(pdMS_TO_TICKS(100)); } }

当 Task1 调用:

vTaskDelay(pdMS_TO_TICKS(100));

系统会执行类似下面的状态变化:

Task1:运行态 ↓ Task1:从就绪链表移除 ↓ Task1:进入延时链表 ↓ 调度器重新查找最高优先级就绪任务 ↓ 选择 Task2 或其他就绪任务

100 ms 到期后:

Task1:延时链表 ↓ Task1:重新加入对应优先级的就绪链表 ↓ 如果 Task1 优先级足够高 ↓ Task1 可能立即抢占当前任务

这说明,任务是否运行不仅取决于任务优先级,还取决于任务当前是否处于就绪态。


3.5 中断唤醒任务后的选择

任务也可能被中断服务函数唤醒。

例如,Task4 正在等待队列数据:

xQueueReceive(queue, &data, portMAX_DELAY);

此时 Task4 处于阻塞态。

当中断接收到数据后,可以调用:

void UART_IRQHandler(void) { BaseType_t higherPriorityTaskWoken = pdFALSE; uint8_t data; data = UART_ReadByte(); xQueueSendFromISR( queue, &data, &higherPriorityTaskWoken ); portYIELD_FROM_ISR(higherPriorityTaskWoken); }

任务状态变化如下:

Task4:阻塞态 ↓ 中断发送队列数据 ↓ Task4:重新进入就绪态 ↓ Task4:加入对应优先级的就绪链表 ↓ 如果 Task4 优先级更高 ↓ 中断退出时请求任务切换

higherPriorityTaskWoken用于表示这次中断操作是否唤醒了更高优先级任务。

如果结果为pdTRUE,则:

portYIELD_FROM_ISR(higherPriorityTaskWoken);

会请求在退出中断时执行任务切换。


3.6 三种典型情况

情况一:存在更高优先级就绪任务

Task1 正在运行,优先级为 1 Task4 进入就绪态,优先级为 4

调度器会选择 Task4,Task4 抢占 Task1。

情况二:只有同优先级任务

Task1 正在运行,优先级为 1 Task2 进入就绪态,优先级为 1

如果启用了时间片轮转,Task1 和 Task2 可以轮流运行。

如果没有启用时间片轮转,则通常需要等待任务阻塞或主动调用:

taskYIELD();

才会切换到另一个同优先级任务。

情况三:没有其他用户任务就绪

如果所有用户任务都处于阻塞态或挂起态,调度器会运行空闲任务:

用户任务全部不可运行 ↓ 运行 IdleTask

四.任务的主动切换与被动切换

FreeRTOS 同一时刻通常只有一个任务占用单核 CPU。当前任务不再运行时,调度器会从就绪态任务中选择最高优先级的任务。按照触发原因,可以把任务切换理解为主动切换和被动切换。

4.1 主动切换:当前任务自己让出 CPU

1. 调用taskYIELD()

taskYIELD();

当前任务主动请求重新调度,但仍处于就绪态。如果有同优先级的其他就绪任务,调度器可以选择其中一个;如果没有合适的其他任务,当前任务可能继续运行。

因此,taskYIELD()不是“让低优先级任务运行”的指令。只要当前任务仍是最高优先级的就绪任务,低优先级任务就不能因此抢占它。

2. 调用延时函数

vTaskDelay(pdMS_TO_TICKS(100));

当前任务进入阻塞态,暂时退出就绪链表。调度器随即选择其他就绪任务。等待时间到期后,该任务重新进入就绪态,再按优先级参与调度。

与taskYIELD()的关键区别是:taskYIELD()后任务仍可立即运行;vTaskDelay()后任务要等待延时结束。

3. 等待队列、信号量等事件

uint8_t data; if (xQueueReceive(uartQueue, &data, portMAX_DELAY) == pdPASS) { ProcessData(data); }

如果队列为空,任务会阻塞等待数据,把 CPU 交给其他就绪任务。队列收到数据后,它才重新进入就绪态。

信号量、任务通知等等待 API 也有类似行为。需要注意:只有等待条件尚未满足、且允许等待时,任务才会阻塞。例如队列中已有数据,xQueueReceive()可以直接返回,不一定发生任务切换;超时参数为0时也不会等待。

4.2 被动切换:当前任务因外部变化失去 CPU

被动切换指当前任务没有主动让出 CPU,但系统中出现了更适合运行的就绪任务,或者系统节拍触发了同优先级任务轮转。

4.2.1. 中断唤醒更高优先级任务

假设 Task1 正在运行,而优先级更高的 Task4 正在等待串口数据:

Task1 正在运行 ↓ 串口接收中断发生 ↓ 中断向队列发送数据,唤醒 Task4 ↓ Task4 进入就绪态 ↓ 中断退出时切换到 Task4

中断服务函数应使用FromISR版本的 API:

void UART_IRQHandler(void) { BaseType_t higherPriorityTaskWoken = pdFALSE; uint8_t data = UART_ReadByte(); xQueueSendFromISR( uartQueue, &data, &higherPriorityTaskWoken ); portYIELD_FROM_ISR(higherPriorityTaskWoken); }

这里的portYIELD_FROM_ISR()用于在需要时请求中断退出后的任务切换。宏的名称和使用方式应以工程所用的 FreeRTOS 移植层为准。

4.2.2 延时任务到期

假设 Task4 调用vTaskDelay()后进入阻塞态,Task1 开始运行。当系统节拍发现 Task4 的延时已到,就会将 Task4 移回就绪态。在抢占式调度下,如果 Task4 的优先级高于 Task1,Task4 就可以抢占 Task1。

4.2.3. 同优先级任务的时间片轮转

如果启用了抢占式调度和时间片轮转:

#define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1

多个同优先级、始终处于就绪态的任务可以在系统节拍到来时轮流运行。此时当前任务没有主动调用taskYIELD(),切换由系统节拍推动。

时间片轮转只针对同优先级任务。它不会使低优先级任务抢占高优先级任务。

4.2.4. 两者有什么区别

对比项主动切换被动切换
触发原因当前任务主动让出 CPU 或进入阻塞态中断、系统节拍等使调度条件发生变化
常见场景taskYIELD()、vTaskDelay()、等待空队列中断唤醒高优先级任务、延时到期、时间片轮转
当前任务的状态可能仍就绪,也可能进入阻塞态通常仍就绪,只是暂时失去 CPU

总结:主动与被动描述的是“切换由什么触发”,不是两套不同的调度规则。无论哪种情况,调度器最终都优先选择最高优先级的就绪态任务;只有同优先级任务之间,才需要进一步考虑时间片、主动让出 CPU 等因素。


五.抢占式调度与非抢占式调度

5.1 抢占式调度

配置:

#define configUSE_PREEMPTION 1

抢占式调度下,如果当前运行任务的优先级较低,而一个更高优先级任务变为就绪态,FreeRTOS 可以切换到高优先级任务。

例如:

Task1 正在运行,优先级为 1 中断唤醒 Task4,优先级为 4 Task4 进入就绪态 调度器切换到 Task4

这就是高优先级任务抢占低优先级任务。

5.2 非抢占式调度

配置:

#define configUSE_PREEMPTION 0

非抢占式调度下,新任务变为就绪态后,调度器不会自动抢占当前任务。

当前任务必须主动:

taskYIELD();

或者进入阻塞态:

vTaskDelay(); xQueueReceive(); xSemaphoreTake();

如果当前任务一直运行,并且既不阻塞也不主动让出 CPU,其他任务可能长期得不到运行机会。


六. 任务调度示例

void Task1(void *argument) { for (;;) { printf("Task1\r\n"); } } void Task2(void *argument) { for (;;) { printf("Task2\r\n"); } } void Task3(void *argument) { const TickType_t delayTime = pdMS_TO_TICKS(50); for (;;) { printf("Task3\r\n"); vTaskDelay(delayTime); } } int main(void) { prvSetupHardware(); xTaskCreate(Task1, "Task1", 256, NULL, 0, NULL); xTaskCreate(Task2, "Task2", 256, NULL, 0, NULL); xTaskCreate(Task3, "Task3", 256, NULL, 2, NULL); vTaskStartScheduler(); for (;;) { } }

假设:

#define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1

运行过程大致如下:

  1. 调度器启动后,Task3 优先级最高,首先运行;

  2. Task3 打印一次信息;

  3. Task3 调用vTaskDelay(),进入阻塞态;

  4. Task1 和 Task2 成为最高优先级的就绪任务;

  5. Task1 和 Task2 按时间片轮转运行;

  6. 50 ms 后 Task3 延时结束;

  7. Task3 重新进入就绪态;

  8. Task3 再次抢占 Task1 或 Task2。

实际输出顺序会受到任务执行时间、串口打印耗时、系统节拍频率和移植层实现影响,因此不能把输出结果简单理解成严格固定的顺序。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 21:12:56

Python 高阶语法(二):上下文管理器、with、yield 与资源安全释放——从文件到 FastAPI 数据库会话

这一篇我们会学习另一个在后端中非常重要的能力:with __enter__ __exit__ contextmanager yield async with它们会解释为什么文件、数据库连接、HTTP 客户端、模型资源都需要被正确打开和关闭。一、前言在编程中,有很多资源不能只打开、不关闭。例如&…

作者头像 李华
网站建设 2026/10/21 8:44:14

MySQL中的常用SQL语句

SQL语句的分类DDL(Data Definition Languages)语句:数据定义语言,这些语句定义了不同的数据段、数据库、表、列、索引等数据库对象的定义。常用的语句关键字主要包括 create、drop、alter、rename、truncate。其中 create 用于创建数据库对象,drop 用于删…

作者头像 李华
网站建设 2026/10/18 17:29:08

【BFS 解决拓扑排序】课程表

文章目录题目解析算法原理建图入度数组代码实现题目链接:207. 课程表 题目解析 拓扑排序(Topological sorting)要解决的问题是 如何给一个有向无环图的所有节点排序。 有向无环图 (Directed Acyclic Graph, 缩写 DAG&#xff09…

作者头像 李华
网站建设 2026/10/20 18:10:34

PHP四大加密技术全解析+实战笔记免费领

在PHP语言里, 加密的操作方式主要有那四种常用的办法。除了刚才提到的这几个方法以外, 还有一种跟网址链接相关的加密处理以及解密的方式是另外一种存在的情况。我希望这段文字内容对于你们正在进行的软件开发工作能够提供一点实际的帮助作用和使用价值参考。 另外, 我把之后整…

作者头像 李华
网站建设 2026/10/18 22:12:26

气源含水对气液增压器的故障影响有多大?选台湾钰腾从根源规避风险

很多机加工车间都有一个常年被忽略的隐形隐患:气源里的水。尤其是南方梅雨季、北方冬季温差大的时段,空压机出来的压缩空气里混着大量冷凝水,哪怕装了普通的油水分离器,也很难完全过滤干净。这些混在气源里的水顺着管路钻进气液增…

作者头像 李华