大家好,我是杂烩君。本次我们来了解 FreeRTOS 的调度器。调度器是整个 RTOS 的核心,决定"谁在跑、什么时候换人"。
1. FreeRTOS调度器
调度器持续监控所有任务状态(就绪 / 运行 / 阻塞),从就绪链表中选优先级最高的任务占用 CPU,状态变化时触发切换。
FreeRTOS 默认两种策略并行工作:
抢占式调度:不同优先级之间,高优先级随时抢占,不管低优先级跑没跑完
时间片轮转:同优先级之间,每人轮流跑一个时间片(默认 1ms)
核心原则:优先级高于时间片。
这两种行为由 FreeRTOSConfig.h 中的两个宏独立控制:
/* FreeRTOSConfig.h */
#define configUSE_PREEMPTION 1 /* 1=抢占式,0=合作式(任务必须主动让步) */
#define configUSE_TIME_SLICING 1 /* 1=同优先级时间片轮转,0=关闭轮转 */
两个宏都开启才是默认的"抢占 + 时间片"模式;关掉 configUSE_PREEMPTION 则退化为合作式调度,任务必须调用 taskYIELD() 才切换。
2. 时钟节拍(SysTick)
SysTick 是调度器的心跳,默认 1000Hz(每 1ms 中断一次)。每次中断主要做两件事:tick 计数 +1 并检查阻塞任务,然后决定是否需要上下文切换切换:
xTaskIncrementTick() 内部除了唤醒延时到期任务,还包含时间片轮转判断逻辑——如果当前优先级就绪链表里还有其他任务,则返回 pdTRUE,触发切换:
这段代码说明:时间片轮转不是一个独立机制,它内嵌在 xTaskIncrementTick() 里,每次 SysTick 中断都会顺带判断。
3. 抢占式调度
3.1 运行流程
假设系统中有3个任务,优先级:TaskA(高,优先级3)、TaskB(中,优先级2)、TaskC(低,优先级1)
3.2 调度触发时机
| 调度触发点/调度点 | 说明 |
|---|---|
| SysTick 中断 | 最常见,每 1ms 检查一次 |
| 任务主动阻塞 | vTaskDelay()、xQueueReceive() 等 |
| 任务创建 / 删除 / 改优先级 | 状态变化,重新调度 |
| ISR 中唤醒高优先级任务 | 中断退出后触发 |
在 ISR 里唤醒任务后,需用 portYIELD_FROM_ISR() 通知调度器,不然即便唤醒了高优先级任务,也要等下一个 SysTick 才切换:
3.3 核心源码
vTaskSwitchContext() 的核心是 taskSELECT_HIGHEST_PRIORITY_TASK() 宏,展开后等效逻辑如下:
vTaskStartScheduler() 启动时做四件事:创建空闲任务 → 关中断 → 置 xSchedulerRunning = pdTRUE → 调用 xPortStartScheduler()(内部启动 SysTick 并切换到第一个任务)。
空闲任务不可缺:所有用户任务阻塞时 CPU 有任务可跑,同时负责回收被删除任务的 TCB 和栈资源。
4. 时间片轮转
4.1 运行流程
同优先级任务按就绪链表顺序轮流跑,每人一个时间片,用完切下一个:
4.2 配置与注意事项
#define configTICK_RATE_HZ 1000UL /* 时间片 = 1ms;改成 500 则为 2ms */
/* 延时必须用宏换算,不能写死 tick 数 */
vTaskDelay(pdMS_TO_TICKS(100));
时间片大小的取舍:太小 → 切换频繁,上下文保存/恢复的 CPU 开销上升;太大 → 同优先级任务响应延迟增大,按实际场景平衡。
5. 上下文切换
无论哪种调度触发,最终都走上下文切换,三步完成:
PendSV 优先级设为最低,目的是让所有业务中断先处理完,再做任务切换,不影响中断实时性。
PendSV_Handler 汇编实现的核心逻辑(ARM Cortex-M3 精简版):
6. 常见QA
Q:抢占式调度和时间片轮转的区别?A:抢占式针对不同优先级,高优先级随时抢;时间片轮转针对同优先级,按时间片顺序切换。两者同时工作,优先级优先。
Q:为什么上下文切换用 PendSV 而不直接在 SysTick 里切?A:SysTick 优先级比外部中断高,若直接切换会打断正在处理的中断。PendSV 优先级最低,所有中断处理完后才执行切换,保证中断实时性。
Q:空闲任务有什么用?能删掉吗?A:不能删。空闲任务负责回收被删除任务的 TCB 和栈内存,所有用户任务全部阻塞时 CPU 也有任务可跑,避免调度器无任务可选。
7. 常见问题
坑1:改了 configTICK_RATE_HZ,延时写死 tick 数频率从 1000Hz 改成 500Hz,vTaskDelay(1000) 实际延时变 2 秒。统一换成 pdMS_TO_TICKS() 即可避免。
坑2:高优先级任务死循环不让步高优先级任务循环体没有任何阻塞调用,低优先级任务永远抢不到 CPU,系统假死。高优先级任务循环里需要加阻塞操作,如vTaskDelay,主动释放,低优先级任务有机会运行
坑3:同优先级任务操作共享资源时间片轮转是"轮流",任意时刻只有一个任务在跑,共享资源照样需要加互斥锁,别因为"优先级一样"就省掉保护。
你在项目里遇到过调度相关的问题吗?评论区聊聊。
160