3、任务调度机制:抢占式调度、时间片轮转调度、空闲任务、任务切换过程
好,咱们今天聊聊FreeRTOS的核心——任务调度。说白了,调度器就是操作系统的“大脑”,它决定哪个任务在什么时候运行。我做了这么多年嵌入式,见过太多因为调度没搞明白而翻车的项目。嗯,咱们一个一个来看。
3.1 抢占式调度:优先级说了算
抢占式调度,这是FreeRTOS默认的调度方式。它的规则很简单:谁优先级高,谁就先跑。
举个例子。你有个高优先级任务处理按键,有个低优先级任务刷LED。按键任务一旦就绪,立马把LED任务踢出去。这就是“抢占”。
核心规则:
- 高优先级任务就绪时,立即抢占低优先级任务
- 同优先级任务之间,不会互相抢占
- 中断可以抢占任何任务
我在项目中遇到过一个问题:有个同事把两个任务都设成相同优先级,结果发现系统响应特别慢。为什么?因为同优先级不抢占,得等时间片轮转。这个坑我后面会讲。
我的习惯:一般把实时性要求高的任务(比如通信、按键)设高优先级,把后台任务(比如日志、统计)设低优先级。优先级别超过5个,否则容易把自己绕晕。
3.2 时间片轮转调度:大家轮流来
同优先级的任务怎么跑?靠时间片轮转。每个任务跑一个固定的时间片(默认1个tick),时间到了就换下一个。
你想想看,如果两个任务优先级一样,又没有时间片轮转,那第一个任务就会一直占着CPU,第二个任务永远没机会跑。这显然不合理。
// 创建两个同优先级任务
xTaskCreate(vTask1, "Task1", 1000, NULL, 2, NULL);
xTaskCreate(vTask2, "Task2", 1000, NULL, 2, NULL);
// 配置时间片
// FreeRTOSConfig.h 中
#define configUSE_TIME_SLICING 1 // 开启时间片轮转
时间片轮转的触发点有两个:
- 任务主动调用taskYIELD()让出CPU
- 任务的时间片用完,触发SysTick中断
我曾经踩过的坑:时间片太短(比如1ms),任务切换太频繁,系统开销大。时间片太长(比如100ms),低优先级任务响应慢。我一般用5-10ms,具体看你的tick频率。
3.3 空闲任务:系统最后的守护者
当所有任务都阻塞或挂起时,谁在跑?空闲任务。它是系统自动创建的,优先级最低(0)。
空闲任务主要干两件事:
- 释放内存:删除任务时,任务控制块和栈不会立即释放,而是等空闲任务来回收
- 进入低功耗:如果开启了configUSE_IDLE_HOOK,可以在空闲任务里让CPU休眠
// 空闲任务钩子函数示例
void vApplicationIdleHook(void)
{
// 进入低功耗模式
__WFI(); // Wait For Interrupt
}
嗯,这里要注意:空闲任务里不能调用阻塞函数(比如vTaskDelay),否则系统会崩溃。我刚开始学FreeRTOS时就犯过这个错。
3.4 任务切换过程:到底发生了什么?
任务切换,说白了就是“保存现场、恢复现场”。具体分三步:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 保存当前任务上下文 | 把R0-R15、PSP、LR等寄存器压栈 |
| 2 | 选择下一个任务 | 调度器从就绪列表中找最高优先级任务 |
| 3 | 恢复新任务上下文 | 从新任务的栈中弹出寄存器值 |
任务切换的触发时机:
- SysTick中断:每个tick到来时,检查是否需要切换
- 任务主动让出:调用taskYIELD()或阻塞函数
- 中断退出时:中断处理完后,如果有更高优先级任务就绪,直接切换
关键点:任务切换是在中断上下文中完成的。所以切换过程要尽量快,我一般控制在几十微秒以内。如果切换太慢,会影响系统的实时性。
我记得有一次调试一个多任务系统,发现任务切换特别慢。查了半天,原来是某个任务栈开得太大,切换时保存/恢复上下文花了太多时间。后来把栈大小从2048降到512,问题就解决了。
3.5 调度策略选择建议
最后,我给大家一些实际建议:
我的经验:
- 实时任务用抢占式,后台任务用时间片轮转
- 优先级别超过7级,否则调度开销大
- 同优先级任务别超过3个,否则时间片轮转效率低
- 空闲任务钩子里别做复杂操作,保持简洁
嗯,调度这块就讲这么多。记住一句话:调度器是公平的,但优先级是不公平的。合理分配优先级,你的系统才能跑得稳。