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)。

空闲任务主要干两件事:

  1. 释放内存:删除任务时,任务控制块和栈不会立即释放,而是等空闲任务来回收
  2. 进入低功耗:如果开启了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个,否则时间片轮转效率低
  • 空闲任务钩子里别做复杂操作,保持简洁

嗯,调度这块就讲这么多。记住一句话:调度器是公平的,但优先级是不公平的。合理分配优先级,你的系统才能跑得稳。