FreeRTOS的任务调度,到底是怎么「抢」CPU的

📅 2026/6/26 6:13:06
FreeRTOS的任务调度,到底是怎么「抢」CPU的
void vTask1(void *pvParameters) { for(;;) { // 做点计算密集的事 for(int i 0; i 100000; i); // 主动让出CPU taskYIELD(); } } void vTask2(void *pvParameters) { for(;;) { // 等待队列数据 ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 处理数据... } }这两段代码放在同一个系统里谁先跑、谁后跑、谁跑多久——这些问题的答案就是FreeRTOS调度器每天在干的事。很多初学者把「任务调度」理解成「轮流执行」这个说法不能说错但太粗糙了。我们来把它拆开看看。优先级不是摆设FreeRTOS支持抢占式调度核心规则就一条任何时候最高优先级的就绪任务先跑。注意「就绪」两个字——任务可能被挂起vTaskDelay、阻塞等待信号量/队列/通知、或暂停vTaskSuspend。不在就绪态的任务调度器根本不管它。上面代码里如果vTask1优先级5vTask2优先级3那么vTask1会一直占着CPU跑循环vTask2永远没机会执行——除非vTask1主动调用taskYIELD()或进入阻塞态。这正是第一段代码里taskYIELD()的含义主动让调度器看看有没有同优先级的任务需要切换。一个有意思的设计是FreeRTOS在taskYIELD()内部触发了PendSV中断调度器在PendSV里完成上下文切换。换句话说不是「时间到了被踢出CPU」而是「我自己说跑够了」。当然这是手动调用的情况。时间片轮转是给同优先级准备的如果两个任务优先级相同呢FreeRTOS用时间片轮转Round-Robin来处理。每个tick中断通常1ms或10ms由configTICK_RATE_HZ决定到来时调度器检查当前运行的任务是否用完了时间片。用完了切换到下一个同优先级的就绪任务。这里有个关键点很多人没注意到FreeRTOS的时间片轮转只发生在tick中断里。如果连续几个tick之间没有任务切换比如唯一一个就绪的任务一直在跑那调度器除了计计数什么都不做。还有一个细节值得琢磨configUSE_TIME_SLICING这个宏。把它设为0同优先级任务就不再自动轮转了。这种情况下同一个优先级的任务之间只能用taskYIELD()手动移交CPU。这个配置在一些对确定性要求极高的场景下很有用——你能精确控制切换时机而不是让tick中断替你决定。调度器的「心跳」tick中断FreeRTOS的一切调度活动都围绕着SysTick或其他定时器的中断服务函数xPortSysTickHandler展开。每次tick中断递增xTickCount系统滴答计数器检查是否有延时结束的任务vTaskDelay到期将它们移回就绪态如果有更高优先级的任务就绪触发PendSV做任务切换注意tick中断的频率直接决定了系统响应时间的下限。假定tick周期10ms一个高优先级任务在tick刚过的时刻就绪了它最长要等10ms才能被调度到——因为你得等下一个tick中断来触发切换。这就是为什么实时性要求高的系统要么把tick频率设得很高代价是CPU浪费在中断上下文中要么用tickless模式。tickless idle是怎么回事低功耗场景下高频tick是个大问题——每次tick中断都把CPU从睡眠中唤醒。FreeRTOS提供了configUSE_TICKLESS_IDLE机制当系统进入idle任务时它计算下一个定时事件比如最近一个vTaskDelay到期还有多久然后把定时器设成一次性触发模式直接睡到那个时间点再醒。这个设计很巧妙——既保证了定时准确性又最大化地减少了唤醒次数。不过代价是xTickCount的维护变得复杂了因为睡眠期间tick是停的醒来后需要补偿。回到最开始的问题那两段代码会怎么运行取决于优先级和阻塞状态。vTask2在ulTaskNotifyTake处阻塞了它根本不参与调度竞争。vTask1一直跑循环如果没有同优先级任务它就是唯一的执行者。直到某个外部中断或另一个任务给vTask2发了通知vTask2变成就绪态——如果它的优先级高于vTask1调度器会立即把CPU抢过来。这才是抢占式调度的本质不是「大家轮流」而是「谁优先级高、谁准备好了谁上」。