找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1721

积分

0

好友

227

主题
发表于 12 小时前 | 查看: 2| 回复: 0

调度器是整个 RTOS 的核心,它决定了“谁在跑、什么时候换人”。今天我们深入了解一下 FreeRTOS 的调度器是如何工作的。

1. FreeRTOS调度器

调度器就像是系统的“裁判”,它持续地监控着所有任务的状态(就绪 / 运行 / 阻塞),然后从就绪链表中选出优先级最高的任务来占用 CPU。一旦任务状态发生变化,它就会触发切换。

FreeRTOS 默认采用两种策略并行工作:

  • 抢占式调度:在不同优先级的任务之间,高优先级任务可以随时抢占低优先级任务的 CPU 使用权,不管低优先级的任务是否已经执行完毕。
  • 时间片轮转:在同一优先级的多个任务之间,每个任务轮流执行一个固定的时间片(默认是 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 中断一次)。每次 SysTick 中断主要完成两件事:将系统时钟节拍 tick 计数加一、检查是否有阻塞任务超时需要唤醒,然后决定是否需要进行上下文切换。

FreeRTOS SysTick中断处理函数xPortSysTickHandler代码截图

关键的 xTaskIncrementTick() 函数除了处理阻塞任务的唤醒,其内部还包含了时间片轮转的判断逻辑。它会检查当前优先级下是否还有其他就绪任务,如果有,就返回 pdTRUE,从而触发一次切换。

FreeRTOS时间片轮转判断逻辑代码截图

这段代码说明了一个重要事实:时间片轮转并不是一个完全独立的机制,它内嵌在 xTaskIncrementTick() 函数里,每次 SysTick 中断都会顺带进行判断。

3. 抢占式调度

3.1 运行流程

假设系统中有三个任务,优先级分别是:TaskA(高,优先级3)、TaskB(中,优先级2)、TaskC(低,优先级1)。它们的抢占调度流程可以用下面的流程图清晰地展示:

FreeRTOS抢占式调度流程示例图

3.2 调度触发时机

调度并不是随机发生的,它只在特定的“调度点”被触发。常见的调度点如下:

调度触发点 说明
SysTick 中断 最常见,默认每 1ms 检查一次
任务主动阻塞 vTaskDelay(), xQueueReceive()
任务创建 / 删除 / 改优先级 任务状态发生变化时
ISR 中唤醒高优先级任务 中断服务程序中,唤醒任务后触发

特别需要注意的是,在中断服务程序 (ISR) 里唤醒了一个高优先级任务后,必须使用 portYIELD_FROM_ISR() 通知调度器。否则,即使高优先级任务已经就绪,也得等到下一个 SysTick 中断到来时才会发生切换。

FreeRTOS ISR中调用portYIELD_FROM_ISR示例代码

3.3 核心源码

vTaskSwitchContext() 是上下文切换的核心函数,它内部最关键的是 taskSELECT_HIGHEST_PRIORITY_TASK() 宏。这个宏展开后的等效逻辑如下,它从最高优先级向下查找第一个非空的就绪链表:

FreeRTOS选择最高优先级任务逻辑代码

当调用 vTaskStartScheduler() 启动调度器时,它会依次完成四件事:创建空闲任务 → 关中断 → 设置 xSchedulerRunning = pdTRUE → 调用 xPortStartScheduler()(这个函数内部会启动 SysTick 定时器,并切换到第一个任务去执行)。

这里提到了空闲任务,它是 FreeRTOS 系统中一个不可或缺的部分。它的作用主要有两个:第一,当所有用户任务都处于阻塞状态时,CPU 仍然有一个任务可以运行,避免 CPU 空转;第二,它负责回收那些被删除任务的 TCB(任务控制块)和栈内存资源。

4. 时间片轮转

4.1 运行流程

对于同一优先级的多个任务,时间片轮转机制会让它们按照就绪链表中的顺序轮流执行。每个任务执行一个时间片,用完后就切换到链表中的下一个任务。下图直观地展示了两个同优先级任务的轮转过程:

FreeRTOS时间片轮转甘特图

4.2 配置与注意事项

时间片的长度由系统节拍频率决定,它在 FreeRTOSConfig.h 中配置:

#define configTICK_RATE_HZ  1000UL   /* 时间片 = 1ms;改成 500 则为 2ms */

这里有一个非常重要的实践要点:当使用任务延时函数时,必须使用 pdMS_TO_TICKS() 宏进行转换,而不能直接写死 tick 数值。

vTaskDelay(pdMS_TO_TICKS(100)); // 正确:延时 100ms
vTaskDelay(100);                // 错误:如果 tick 频率改变,延时时间会变化

时间片大小的取舍是一个平衡问题:

  • 时间片太小:任务切换会非常频繁,上下文保存和恢复带来的 CPU 开销会显著上升。
  • 时间片太大:同一优先级任务之间的响应延迟会增大,任务好像“卡住”了一样。
    你需要根据实际的应用场景来找到一个合适的平衡点。

5. 上下文切换

无论是由 SysTick 中断、任务主动阻塞还是其他事件触发了调度需求,最终都会走到“上下文切换”这一步。这个过程可以概括为三个主要步骤:

FreeRTOS上下文切换流程图

你可能会有疑问,为什么切换不直接在 SysTick 中断里完成,而是要触发一个 PendSV 异常?这是因为 PendSV 的优先级被设置为最低。这样做的好处是,可以让所有更高优先级的硬件中断(比如你的 UART、SPI 中断)都先处理完毕,然后再进行任务切换。这保证了硬件中断的实时性不受任务切换的影响

PendSV 异常处理程序通常由汇编编写,以追求最高的执行效率。下面是其在 ARM Cortex-M 内核上一个简化版的逻辑:

PendSV_Handler:
    /* Step1: 保存当前任务上下文 */
    MRS R0, PSP
    STMDA R0!, {R4-R11}
    LDR R1, =pxCurrentTCB
    LDR R2, [R1]
    STR R0, [R2]

    /* Step2: 切换 pxCurrentTCB 到最高优先级任务 */
    BL vTaskSwitchContext

    /* Step3: 恢复新任务上下文 */
    LDR R1, =pxCurrentTCB
    LDR R2, [R1]
    LDR R0, [R2]
    LDMIA R0!, {R4-R11}
    MSR PSP, R0
    BX LR

FreeRTOS PendSV_Handler汇编代码截图

6. 常见Q&A

Q:抢占式调度和时间片轮转的根本区别是什么?
A:抢占式调度作用于不同优先级的任务之间,核心是“优先级高者随时抢占”;时间片轮转作用于同一优先级的任务之间,核心是“按时间片顺序轮流执行”。两者在系统中是同时工作的,并且优先级判断永远优先于时间片轮转

Q:为什么上下文切换要放在 PendSV 异常里,而不是直接在 SysTick 中断里完成?
A:主要是为了保证中断的实时性。SysTick 的优先级通常设置得比较高,如果直接在其中进行复杂的上下文保存和恢复,可能会延迟对其他更紧急硬件中断的响应。PendSV 优先级最低,可以确保所有“正事”(硬件中断)都处理完后,再来做“家务事”(任务切换)。

Q:空闲任务到底有什么用?可以把它删掉吗?
A:绝对不能删除。空闲任务有两个关键职责:第一,当所有用户任务都阻塞时,为 CPU 提供一个可执行的“背景”任务,防止系统进入未定义状态;第二,它负责在后台回收被 vTaskDelete() 删除的任务所占用的 TCB 和栈内存,这是 FreeRTOS 动态内存管理的重要一环。

7. 常见问题与避坑指南

坑1:修改了 configTICK_RATE_HZ,但代码中的延时还是写死的 tick 数
这是新手常犯的错误。例如,你把系统频率从 1000Hz 改成了 500Hz,以为 vTaskDelay(1000) 还是延时1秒,实际上它变成了2秒。唯一的解决方案就是统一使用 pdMS_TO_TICKS() 宏来转换毫秒时间为 tick 数。

坑2:高优先级任务写成了“死循环”,永不释放 CPU
如果一个高优先级任务在其循环体中没有任何阻塞调用(如 vTaskDelay(), xQueueReceive()),那么它将一直占据 CPU,导致所有低优先级的任务永远得不到执行,系统看似“假死”。设计准则:高优先级任务中必须包含能让出 CPU 的阻塞操作。

坑3:认为同优先级任务之间操作共享资源不需要保护
这是一个危险的误解。时间片轮转只是在宏观上“轮流”执行,但在微观的任一时刻,仍然只有一个任务在访问共享资源(如全局变量、外设)。如果两个同优先级任务可能同时访问该资源,必须使用信号量、互斥锁等机制进行保护,否则会导致数据竞争和系统的不确定性。

理解 FreeRTOS 的调度机制,是进行稳定、高效嵌入式系统开发的基础。希望本文的解析能帮助你更清晰地把握抢占与轮转的精髓。如果你在项目中遇到过其他有趣的调度问题,欢迎在云栈社区的技术论坛与其他开发者交流探讨。




上一篇:如何用Claude Code与本地文件夹构建AI生产力系统
下一篇:Oracle 12c Flex ASM部署与运维实战:实现高可用的存储管理
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-1 21:59 , Processed in 0.485120 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表