一. 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 -> Task3FreeRTOS 会通过链表中的当前位置选择下一个任务。这个过程可以理解为:
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运行过程大致如下:
调度器启动后,Task3 优先级最高,首先运行;
Task3 打印一次信息;
Task3 调用
vTaskDelay(),进入阻塞态;Task1 和 Task2 成为最高优先级的就绪任务;
Task1 和 Task2 按时间片轮转运行;
50 ms 后 Task3 延时结束;
Task3 重新进入就绪态;
Task3 再次抢占 Task1 或 Task2。
实际输出顺序会受到任务执行时间、串口打印耗时、系统节拍频率和移植层实现影响,因此不能把输出结果简单理解成严格固定的顺序。