乐于分享
好东西不私藏

从 FreeRTOS 源码,洞察任务切换到底是怎么发生的?

从 FreeRTOS 源码,洞察任务切换到底是怎么发生的?

用过 FreeRTOS 的朋友都知道,多个任务能”同时”运行。但你有没有想过,CPU 明明只有一个,它是怎么做到让多个任务轮流执行的?今天我们就来扒开源码,看看任务切换这件事到底是怎么发生的。

先打个比方:任务切换就像换班

想象一下工厂里的流水线。

张三正在岗位上干活,突然广播响了:”张三下班,李四上岗!”

张三不能直接拍拍屁股走人。他得先把手头的活记下来——做到哪一步了、工具放在哪、材料还剩多少。这些信息写在交接本上,然后才能离开。

李四来了,也不能直接开干。他得先翻开自己上次的交接本,看看之前做到哪了,把工具和材料恢复到上次的状态,然后才能继续。

FreeRTOS 的任务切换,干的就是这个事:

  • • 保存现场:把当前任务的”工作状态”存起来
  • • 恢复现场:把下一个任务的”工作状态”拿出来

所谓的”工作状态”,在 CPU 里就是一堆寄存器的值。这个过程,专业点叫”上下文切换”(Context Switch)。

任务的本质:一个函数 + 一块栈 + 一个控制块

在聊切换之前,得先搞清楚”任务”到底是什么。

你调用 xTaskCreate() 创建任务时,FreeRTOS 在背后做了三件事:

  1. 1. 分配一个任务控制块(TCB):记录任务的名字、优先级、状态等信息
  2. 2. 分配一块栈空间:任务运行时用来保存局部变量、函数调用关系、以及上下文
  3. 3. 初始化栈:在栈里预先填好”假的”寄存器值,让任务第一次被调度时能正常启动

TCB 里有个关键成员:pxTopOfStack——它指向任务栈的栈顶。任务切换时,就是通过这个指针来保存和恢复上下文的。

简单说:任务 = 函数入口 + 独立的栈 + TCB 管理信息。切换任务,本质上就是切换栈指针,让 CPU 去执行另一个栈上保存的代码流程。

什么时候会发生任务切换?

任务切换不会凭空发生,总得有个触发条件。常见的场景有这么几种:

1. 时间片轮转(Tick 中断)

FreeRTOS 有个心跳定时器,默认每 1ms 跳一次(可配置)。每次 Tick 中断,系统会检查:有没有同优先级的任务在排队?有的话就轮换一下。

2. 高优先级任务就绪

低优先级任务正在跑,突然一个高优先级任务被唤醒了(比如收到了信号量)。这时候必须立即切换,让高优先级的先跑。

3. 当前任务主动让出 CPU

任务调用 vTaskDelay()xQueueReceive() 等阻塞 API 时,自己把自己挂起了,CPU 当然得切给别人。

4. 当前任务被挂起或删除

调用 vTaskSuspend() 或 vTaskDelete() 后,当前任务没法继续了,必须切换。

5. 任务优先级被修改

运行中途通过 vTaskPrioritySet() 改了优先级,可能触发重新调度。

不管是哪种情况,最终都会调用到一个关键函数:portYIELD()。它的作用就是告诉系统:”我要触发一次任务切换”。

为什么用 PendSV 来做切换?

这里要解释一个设计选择。

在 ARM Cortex-M 上,FreeRTOS 用 PendSV 中断 来完成任务切换。为啥不直接在 Tick 中断里切换呢?

原因是:中断可能嵌套

假设这样一个场景:Tick 中断触发,正准备切换任务,突然来了个更高优先级的外设中断。如果在 Tick 里直接切换上下文,这时候栈的状态会乱套——你还没保存完,新中断又压了一堆东西进来。

PendSV 的妙处在于:它的优先级被设成最低

这意味着:只有当所有其他中断都处理完了,PendSV 才会执行。在 PendSV 里做上下文切换,能保证不会被其他中断打断,栈的状态是干净的。

工作流程是这样的:

一句话总结:Tick 负责”决定要不要切”,PendSV 负责”真正去切”

核心流程:上下文切换三步走

现在进入正题。PendSV 中断里到底做了什么?

整个上下文切换可以拆成三步:

这里有个细节要注意:ARM Cortex-M 在进入中断时,硬件会自动保存一部分寄存器(R0-R3、R12、LR、PC、xPSR),这部分不用软件操心。软件需要手动保存的是 R4-R11 这几个寄存器。

退出中断时同理:硬件会自动恢复那几个寄存器,软件只需要恢复 R4-R11。

源码解析:PendSV 中断处理函数

下面是 Cortex-M3/M4 上 PendSV 的核心汇编代码(精简版):

PendSV_Handler:    ; 关中断,防止切换过程被打断    CPSID   I    ; 获取当前任务的栈指针(PSP)    MRS     R0, PSP    ; 把 R4-R11 压入当前任务的栈    STMDB   R0!, {R4-R11}    ; 把更新后的栈顶保存到当前 TCB    LDR     R1, =pxCurrentTCB    LDR     R2, [R1]    STR     R0, [R2]              ; pxCurrentTCB->pxTopOfStack = R0    ; ========== 分割线:上面保存旧任务,下面恢复新任务 ==========    ; 调用 C 函数,选出下一个任务    BL      vTaskSwitchContext    ; 获取新任务的 TCB    LDR     R1, =pxCurrentTCB    LDR     R2, [R1]    LDR     R0, [R2]              ; R0 = 新任务的 pxTopOfStack    ; 从新任务的栈弹出 R4-R11    LDMIA   R0!, {R4-R11}    ; 更新 PSP 为新任务的栈指针    MSR     PSP, R0    ; 开中断    CPSIE   I    ; 返回,硬件自动恢复 R0-R3、PC 等    BX      LR

逐段解释:

  1. 1. CPSID I:关中断。虽然 PendSV 优先级最低,但保险起见还是关掉。
  2. 2. MRS R0, PSP:读取进程栈指针。FreeRTOS 的任务都用 PSP(进程栈),而不是 MSP(主栈)。
  3. 3. STMDB R0!, {R4-R11}:把 R4-R11 压栈。STMDB 是”先减后存”,R0 自动更新。
  4. 4. 保存栈顶到 TCB:这样下次恢复时能找到。
  5. 5. BL vTaskSwitchContext:调用 C 函数,选出下一个任务,更新 pxCurrentTCB
  6. 6. 从新 TCB 读取栈顶:准备恢复新任务的上下文。
  7. 7. LDMIA R0!, {R4-R11}:从栈弹出 R4-R11。LDMIA 是”先取后加”。
  8. 8. MSR PSP, R0:更新 PSP,指向新任务的栈。
  9. 9. BX LR:返回。硬件会根据 LR 的特殊值(EXC_RETURN)自动恢复剩余寄存器。

源码解析:vTaskSwitchContext

PendSV 汇编里调用的 vTaskSwitchContext() 是个 C 函数,它的核心工作就是:找到下一个该运行的任务

void vTaskSwitchContext( void ){    // 如果调度器被挂起,不允许切换    if( uxSchedulerSuspended != 0 )    {        xYieldPending = pdTRUE;        return;    }    // 选择最高优先级就绪任务    taskSELECT_HIGHEST_PRIORITY_TASK();}

taskSELECT_HIGHEST_PRIORITY_TASK 是个宏,展开后大概是这样:

// 找到最高优先级的就绪列表(不为空的)while( listLIST_IS_EMPTY( &pxReadyTasksLists[ uxTopPriority ] ) ){    uxTopPriority--;}// 从这个列表里取下一个任务listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &pxReadyTasksLists[ uxTopPriority ] );

逻辑很直接:从最高优先级往下找,找到第一个非空的就绪列表,然后从里面取一个任务。如果同优先级有多个任务,就轮流取(时间片轮转)。

执行完这个函数后,全局变量 pxCurrentTCB 就指向了新任务的 TCB。回到汇编代码,就能从新 TCB 里拿到新任务的栈指针,完成切换。

图解:栈里到底存了什么?

为了让你更直观地理解,下面画一下任务栈在切换前后的样子:

要点:

  • • 进入 PendSV 时,硬件已经把 R0-R3、R12、LR、PC、xPSR 压到任务 A 的栈上了
  • • 软件再把 R4-R11 压进去,保存完成
  • • 切换到任务 B 时,软件先恢复 R4-R11
  • • 退出 PendSV 时,硬件自动恢复剩下的寄存器,PC 指向任务 B 之前被中断的位置
  • • 任务 B 就像什么都没发生一样,继续执行

串一遍完整流程

最后,我们把整个任务切换的过程串起来走一遍:

整个过程对任务来说是透明的——任务 A 不知道自己被暂停了,任务 B 也不知道自己是被”复活”的。它们只是在各自的时间线上继续执行,仿佛独占 CPU 一样。

总结

回到开头的问题:任务切换是怎么发生的?

用一句话概括:触发 PendSV 中断,保存当前任务的寄存器到栈,选出下一个任务,从它的栈恢复寄存器,然后继续执行

核心就是三个字:换栈跑


如果你觉得这篇文章有帮助,欢迎点赞、转发。有问题也欢迎在评论区讨论,我们下期见!

【往期推荐】
C语言设计模式实战
嵌入式软件模块解耦进阶:构建高内聚、低耦合的系统架构
给你的设备做一套”砖不死”的 OTA 升级方案
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 从 FreeRTOS 源码,洞察任务切换到底是怎么发生的?

评论 抢沙发

5 + 9 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮