FreeRTOS学习笔记(四):任务执行与切换
好的,这是一个关于 FreeRTOS 核心机制的绝佳问题。我会详细、系统地讲解任务是如何执行的,以及优先级切换的完整过程。
第一部分:FreeRTOS 任务是如何执行的?
FreeRTOS 是一个抢占式的实时操作系统内核。其任务执行遵循一个核心原则:调度器(Scheduler)总是选择当前处于“就绪态”(Ready)的最高优先级任务来运行。
1. 任务的形态:一个永不返回的函数
每个 FreeRTOS 任务本质上是一个永不返回的 void
函数,通常是一个无限的 for(;;)
循环或 while(1)
循环。
void vMyTask( void * pvParameters ) // 任务函数原型
{// 可选的初始化代码for( ;; ) // 无限循环,任务的主体{// 任务要做的实际工作...do_work();// 关键点:必须调用一个能让任务“放弃”CPU的FreeRTOS API// 例如,等待一个事件或延迟一段时间vTaskDelay( pdMS_TO_TICKS( 1000 ) ); // 延迟1秒}// 理论上任务不应结束,但如果结束,必须调用 vTaskDelete(NULL) 来删除自己。
}
2. 任务的状态
一个任务在任何时刻都处于以下几种状态之一,理解这些状态是理解调度的关键:
- 运行态(Running):任务正在 CPU 上执行。在单核处理器上,任何时候都只有一个任务处于此状态。
- 就绪态(Ready):任务已经准备好可以运行(不被阻塞或挂起),但当前没有运行,因为有一个更高优先级的任务正在运行,或者一个同等优先级的任务正在其时间片内运行。
- 阻塞态(Blocked):任务正在等待某个事件。事件可以是:
- 时间相关:例如调用
vTaskDelay()
等待一段时间。 - 同步事件:例如等待一个信号量(Semaphore)、队列(Queue) 消息、任务通知(Task Notification) 等。
- 处于阻塞态的任务不会被调度器选择执行。当事件发生时,任务会自动离开阻塞态,进入就绪态。
- 时间相关:例如调用
- 挂起态(Suspended):任务被主动挂起,通过
vTaskSuspend()
实现。被挂起的任务对调度器“不可见”,无论发生什么事件都不会被执行,除非其他任务调用vTaskResume()
来恢复它。它不参与调度。
3. 任务的控制块(TCB)和栈
- TCB(Task Control Block):FreeRTOS 为每个任务创建一个数据结构(TCB),用来存储任务的所有元信息,如优先级、堆栈指针、状态、事件列表项等。
- 栈(Stack):每个任务都有自己独立的堆栈空间,用于存储函数调用链、局部变量和任务挂起时的上下文(CPU寄存器值)。这是实现多任务并发的基石。
第二部分:优先级任务切换的详细过程
任务切换,也称为上下文切换(Context Switch),即保存当前任务的运行环境(上下文),恢复另一个任务的运行环境,并开始执行它。
核心原则:抢占(Preemption)
FreeRTOS 是抢占式调度器。这意味着:
- 如果一个更高优先级的任务进入就绪态(例如,它等待的事件发生了),调度器会立即停止当前运行的任务(即使它还没执行完),并切换到更高优先级的任务。
- 这保证了系统对高优先级事件的响应是即时的。
触发任务切换的四大场景
-
系统时钟节拍(SysTick)中断
- 这是时间片轮转的基础。SysTick 定时器定期产生中断(例如每1ms一次)。
- 在中断服务程序(ISR)中,内核会:
- 递增系统时钟计数器
xTickCount
。 - 检查是否有因延时到期而需要从阻塞态唤醒的任务。
- 检查是否需要任务切换:如果当前任务的时间片已用完,并且存在同等优先级的就绪任务,则会触发切换(同优先级任务轮转)。如果发现了一个更高优先级的任务就绪了,也会触发切换。
- 递增系统时钟计数器
- 这是周期性的自动切换。
-
任务主动进入阻塞态
- 这是最常见、最推荐的切换方式,体现了事件驱动编程思想。
- 当一个高优先级任务执行到
xQueueReceive()
,xSemaphoreTake()
,vTaskDelay()
等函数时,因为它要等待的事件尚未发生,它会主动放弃CPU,将自己置于阻塞态。 - 一旦它进入阻塞态,它就不再是“就绪态”的任务,调度器会立刻寻找当前最高优先级的就绪任务来执行。这通常是低优先级或同等优先级的任务。
- 示例:一个高优先级任务等待一个按键消息。在它等待(阻塞)期间,CPU 会去执行低优先级的 LED 闪烁任务、显示刷新任务等。
-
中断服务程序(ISR)使更高优先级任务就绪
- 这是一个硬件外部中断(如 GPIO 引脚中断、UART 接收中断、定时器中断)触发的切换。
- 流程:
- 硬件中断发生,CPU 跳转到对应的 ISR。
- 在 ISR 中,代码通过
xSemaphoreGiveFromISR()
,xQueueSendToBackFromISR()
,xTaskResumeFromISR()
等 FromISR 系列的 API 给出一个信号量、发送一条消息或恢复一个任务。 - 这些 API 会通知调度器:一个更高优先级的任务因为此事件而就绪了。
- 在 ISR 的末尾,FreeRTOS 会进行上下文判断:如果被唤醒的任务优先级高于被中断的任务,ISR 退出时会直接触发一次上下文切换,让更高优先级的任务立即运行,而不是先返回被中断的任务。
-
任务主动让步(Yield)
- 任务可以调用
taskYIELD()
来主动请求调度器立即进行任务切换。 - 注意:
taskYIELD()
并不会使任务进入阻塞态,它只是让任务从运行态变为就绪态,参与下一轮调度。 - 如果存在同等或更高优先级的任务处于就绪态,则调度器会切换到那个任务。否则,它可能继续执行当前任务。
- 任务可以调用
切换的核心机制:PendSV 异常
为了让切换过程高效且不干扰中断的实时性,FreeRTOS 在 ARM Cortex-M 架构上使用 PendSV(可挂起的系统调用) 异常来执行实际的上下文切换工作。
- 触发:上述任何一种场景(如 SysTick ISR、FromISR API)判断需要切换后,并不立刻切换,而是简单地挂起(Pend)一个 PendSV 异常。
- 延迟执行:PendSV 被设置为最低优先级的中断。这意味着 CPU 会先完成所有高优先级的 ISR 处理(保证中断响应及时)。
- 执行切换:当所有高优先级中断处理完毕后,CPU 才来执行 PendSV 异常处理程序(
xPortPendSVHandler
)。在这里,才会进行繁重的上下文保存和恢复工作:- 保存上下文:将当前任务的 CPU 寄存器(R4-R11等)压入该任务自己的堆栈。
- 切换TCB:将当前任务的控制块(TCB)指针指向下一个要运行的任务。
- 恢复上下文:从下一个任务的堆栈中弹出 CPU 寄存器值。
- 退出并运行:当 PendSV 异常处理程序退出时,CPU 会自动使用刚刚恢复的寄存器,程序计数器(PC)也随之跳转,于是自然而然就开始执行新的任务了。
总结与最佳实践
场景 | 触发方式 | 说明 |
---|---|---|
时间片到期 | SysTick 中断 | 同优先级任务轮转的基础 |
任务等待事件 | vTaskDelay() , xSemaphoreTake() , 等 | 最推荐的方式,事件驱动,高效节能 |
中断唤醒任务 | xSemaphoreGiveFromISR() , 等 | 保证高优先级任务对硬件事件的即时响应 |
主动让步 | taskYIELD() | 较少使用,用于计算密集型任务中主动让步 |
核心设计哲学:一个设计良好的 FreeRTOS 应用,其高优先级任务的大部分时间都应处于阻塞态,等待事件发生。 事件到来后(来自中断或其他任务),高优先级任务被唤醒,快速处理事件,处理完毕后立刻又返回阻塞态。这样,低优先级任务就能获得充足的 CPU 时间片来运行。
错误示范(绝对要避免):
void vBadHighPriorityTask( void * pvParameters )
{for( ;; ){// 这是一个“忙等待”(Busy-Waiting)循环// 它永不阻塞,将永远霸占CPU,导致系统被“锁死”// 低优先级任务永远无法运行!process_data();}
}