资讯详情 FreeRTOS——什么是任务切换
📅 2026/10/27 2:02:01
任务切换的核心1.什么是任务切换任务切换概念单片机正常情况下只有一个主程序在跑。在引入FreeRTOS之后系统会有多个任务每个任务都像一段独立的小程序有自己的栈空间自己的执行现场。但CPU同一时刻只能执行同一个任务。所谓任务切换就是CPU暂停当前任务保存它的运行现场再恢复另一个的任务的现场让那个任务继续运行这个过程也叫上下文切换Context Switch。从任务自身角度来说它不知道切换发生过只感觉自己一直在运行切换是内核和硬件悄悄完成的。举个例子你在写TaskA的代码写到一半被打断内核把你的“写到哪了、变量是多少、栈顶在哪”记下来换TaskB运行一段时间再切回 TaskA 时从上次停下的地方继续写这个“记下来”和“恢复回来”就是任务切换。切换时需要保存和恢复内容任务切换本质是保存旧任务现场恢复新任务的现场。什么是现场现场就是任务被切走时CPU和任务栈里与执行相关的关键信息。主要包括内容作用程序计数器PC记录任务执行到哪条指令栈指针SP当前任务栈的位置通用寄存器保存任务运行时的中间数据状态寄存器xPSR保存CPU状态信息在FreeRTOS这些现场主要保存在两处任务栈里TCB的pxTop0fStack中TCB第一个成员就是栈顶指针1volatileStackType_t*pxTopOfStack;/* Points to the location of the las切换时旧任务的栈顶地址会写回这里方便下次恢复。Cortex-M3 上哪些寄存器要保存本工程使用的是 ARM Cortex-M3相关代码在port.c。进入中断时硬件会自动把一部分寄存器压栈寄存器是否硬件自动保存R0是R1是R2是R3是R12是LR是PC是xPSR是R4 ~ R11否软件保存所以在xPortPendSVHandler()里软件只需要保存 R4 ~ R111 mrs r0, psp 2 isb 3 4 ldr r3, pxCurrentTCB /* Get the location of the current TCB. */ 5 ldr r2, [r3] 6 7 stmdb r0!, {r4-r11} /* Save the remaining registers. */ 8 str r0, [r2] /* Save the new top of stack into the first恢复时再把它们弹回来1 ldr r1, [r3] 2 ldr r0, [r1] /* The first item in pxCurrentTCB is the ta 3 ldmia r0!, {r4-r11} /* Pop the registers and the critical nestin 4 msr psp, r0 5 isb任务栈里保存的内容任务创建时内核会先把栈布置成“像刚从异常返回一样”。对应函数是pxPortInitialiseStack()1StackType_t*pxPortInitialiseStack(StackType_t*pxTopOfStack,TaskFunction_t pxCode,void*pvP2{34pxTopOfStack--;5*pxTopOfStackportINITIAL_XPSR;6pxTopOfStack--;7*pxTopOfStack((StackType_t)pxCode)portSTART_ADDRESS_MASK;8pxTopOfStack--;9*pxTopOfStack(StackType_t)prvTaskExitError;/* 对应压入LR */10pxTopOfStack-5;11*pxTopOfStack(StackType_t)pvParameters;/* 对应硬件保存5个寄存器 */12pxTopOfStack-8;/* 留8个位置给软件手动压栈 */1314returnpxTopOfStack;15}栈帧布局如下所以一次完整切换实际上保存的是硬件保存R0R1R2R3R12LRPCxPSR 软件保存R4~R11 TCB记录pxTopOfStack恢复现场数据流向当调度器决定要运行 TaskB 时它会找到 TaskB 的 TCB取出里面的pxTopOfStack也就是 TaskB 的栈顶指针指向 TaskB 在 RAM 里的那块区域。接下来FreeRTOS 的PendSV异常服务程序汇编代码会做以下事情软件出栈R4 ~ R11执行ldmia r0!, {r4-r11}。这句汇编的意思是把r0指向的 RAM 地址里的数据连续装载Load到 CPU 的R4到R11寄存器里并同步更新r0的地址。修改 SP 指针把更新后的地址写进硬件寄存器PSP进程栈指针。硬件出栈R0~R3, R12, LR, PC, xPSR当汇编指令执行到最后触发异常返回比如bx r14。此时Cortex-M3 的硬件会自动接管它会顺着PSP指向的 RAM 地址自动把剩下的 8 个寄存器R0~R3, R12, LR, PC, xPSR的值弹回 CPU 中。任务指针TCB当前运行的任务指针除了寄存器和栈内核还要知道“当前是谁在运行”。这个指针就是1PRIVILEGED_DATA TCB_t*volatilepxCurrentTCBNULL;切换前1 pxCurrentTCB - TaskA切换后1 pxCurrentTCB - TaskB所以任务切换时真正变化的核心内容可以总结为项目切换前切换后当前任务TaskATaskB栈顶指针TaskA 的栈顶TaskB 的栈顶CPU 寄存器TaskA 的现场TaskB 的现场任务管理块TCBFreeRTOS 每创建一个任务内核都会为它分配一块管理结构叫做TCBTask Control Block任务控制块。可以把 TCB 理解成任务的“档案”内核靠它知道任务是谁、优先级多少、栈在哪、当前处于什么状态。源码中类型名是TCB_t定义在tasks.c。TCB 里最重要的几个成员1typedefstructtskTaskControlBlock2{3volatileStackType_t*pxTopOfStack;/* 任务栈顶指针 */4ListItem_t xStateListItem;/* 状态链表节点 */5ListItem_t xEventListItem;/* 事件链表节点 */6UBaseType_t uxPriority;/* 任务优先级 */7StackType_t*pxStack;/* 任务栈起始地址 */8charpcTaskName[16];/* 任务名 */9...10}TCB_t;pxTopOfStack——任务栈顶是FreeRTOS定义在RAM内存里的一个软件变量指针这是TCB里最重要的成员而且必须是结构体第一个成员。作用记录任务栈当前用到哪里任务切换时保存/恢复栈顶就靠它本质上是保存SP的值任务切换时汇编代码会直接读取TCB第一个成员ldr r0, [r1] /* 从TCB取出栈顶 */xStateListItem——状态节点嵌在TCB里链表节点表示任务当前状态、任务处于不同状态时会被挂到不同链表状态挂到哪个链表就绪就绪链表阻塞延时链表挂起挂起链表xEventListItem— 事件节点也是嵌在 TCB 里的链表节点表示任务是否在等待事件。比如等队列数据等信号量一个任务可以同时用xStateListItem挂到延时链表等超时用xEventListItem挂到队列等待链表等数据uxPriority— 优先级任务优先级数字越大优先级越高。调度器会优先运行优先级高的任务。pxStack— 栈起始地址记录任务栈从哪里开始。和pxTopOfStack的区别成员含义pxStack栈底pxTopOfStack当前栈顶pcTaskName— 任务名任务名字主要用于调试。几个关键关系任务句柄就是 TCB 指针1TaskHandle_t xHandle;2xTaskCreate(Task1,Task1,128,NULL,1,xHandle);这里的xHandle本质上就是1TaskHandle_t-TCB*当前运行任务内核里有一个全局指针1TCB_t*volatilepxCurrentTCBNULL;它指向当前正在运行的任务 TCB。任务切换完成后pxCurrentTCB就会指向新的任务。TCB 和任务栈每个任务都有两部分1 TCB - 管理信息 2 任务栈 - 运行时数据关系如下1TCB2├─ pxStack-栈底3└─ pxTopOfStack-当前栈顶TCB 整体结构1TCB2├─ pxTopOfStack 栈顶任务切换时用3├─ xStateListItem 状态节点挂就绪/延时/挂起链表4├─ xEventListItem 事件节点挂队列/信号量链表5├─ uxPriority 优先级6├─ pxStack 栈起始地址7└─ pcTaskName 任务名总结TCB 就是 FreeRTOS 管理任务的核心结构每个任务一个 TCB。最需要记住的 3 点pxTopOfStack任务切换的关键xStateListItem/xEventListItem任务挂到内核链表上的“挂钩”pxCurrentTCB指向当前正在运行的任务一句话总结TCB 是任务档案任务栈是运行空间链表节点是排队位置。2.当前任务与就绪链表pxCurrentTCB作用内核里有一个全局指针PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB NULL;它指向当前正在CPU上运行的任务TCBpxCurrentTCB————某个任务TCB作用很简单告诉内核“现在是谁在跑”场景pxCurrentTCB 指向TaskA 正在运行TaskA 的 TCB切换到 TaskB 后TaskB 的 TCB调度器还没启动NULL任务切换完成后pxCurrentTCB会更新为新任务1taskSELECT_HIGHEST_PRIORITY_TASK();// 内部会更新 pxCurrentTCBPendSV 切换任务时也是通过它找到新任务的栈1 ldr r1, [r3] // 读取 pxCurrentTCB 2 ldr r0, [r1] // 读取 pxTopOfStackpxReadyTasksLists[]就绪链表所有准备好、可以随时运行的任务都放在就绪链表里。1PRIVILEGED_DATAstaticList_t pxReadyTasksLists[configMAX_PRIORITIES]{0};这是一个数组每个优先级对应一条链表1 pxReadyTasksLists[0] - 优先级 0 的就绪任务 2 pxReadyTasksLists[1] - 优先级 1 的就绪任务 3 pxReadyTasksLists[2] - 优先级 2 的就绪任务 4 ... 5 pxReadyTasksLists[55] - 优先级 55 的就绪任务要点一个优先级一条链表同优先级多个任务挂在同一条链表里调度器选任务时只从就绪链表里选任务如何进入就绪链表从xTaskCreate()开始看最清楚。创建任务的入口1xTaskCreate(Task1,Task1,128,NULL,1,NULL);这个函数内部主要做 3 件事1prvInitialiseNewTask(pxTaskCode,pcName,(uint32_t)usStackDepth,pvParameters,uxPriority,pxCreatedTaskpxNewTCBNULL);/* 初始化TCB */2prvAddNewTaskToReadyList(pxNewTCB);/* 挂入就绪链表 */流程如下1 xTaskCreate() 2 | 3 |-- ① 分配 TCB 和任务栈 4 |-- ② prvInitialiseNewTask() 初始化 TCB 5 └-- ③ prvAddNewTaskToReadyList() 挂入就绪链表第一步分配内存pxNewTCB pvPortMallocsizeofTCB_t;//分配TCB pxNewTCB - pxStack pvPortMalloc栈大小;//分配任务栈每个任务都需要两块内存TCB ———— 管理信息 任务栈 ———— 运行数据第二步初始化 TCBprvInitialiseNewTask()负责把 TCB 填好主要工作1pxNewTCB-uxPriorityuxPriority;// 设置优先级2strcpy(pxNewTCB-pcTaskName,pcName);// 设置任务名34vListInitialiseItem((pxNewTCB-xStateListItem));// 初始化状态节点5vListInitialiseItem((pxNewTCB-xEventListItem));// 初始化事件节点67listSET_LIST_ITEM_OWNER((pxNewTCB-xStateListItem),pxNewTCB);8listSET_LIST_ITEM_OWNER((pxNewTCB-xEventListItem),pxNewTCB);910pxNewTCB-pxTopOfStackpxPortInitialiseStack(...);// 初始化任务栈第三步挂入就绪链表prvAddNewTaskToReadyList()负责把新任务挂到就绪链表1prvAddTaskToReadyList(pxNewTCB);这个函数里还会处理pxCurrentTCB1if(pxCurrentTCBNULL)2{3pxCurrentTCBpxNewTCB;第一个创建的任务会直接成为pxCurrentTCB同时初始化所有内核链表就绪链表、延时链表等真正挂链prvAddTaskToReadyList()1#defineprvAddTaskToReadyList(pxTCB)\2taskRECORD_READY_PRIORITY((pxTCB)-uxPriority);\3vListInsertEnd((pxReadyTasksLists[(pxTCB)-uxPriority]),((pxTCB)-xStateListItem));两步① 记录最高就绪优先级1taskRECORD_READY_PRIORITY(pxTCB-uxPriority);例如优先级 2 的任务就绪就记住“当前最高就绪优先级至少是 2”。② 挂到对应优先级链表1vListInsertEnd((pxReadyTasksLists[uxPriority]),(pxTCB-xStateListItem));把任务的xStateListItem插到对应优先级的就绪链表里。完整创建流程以创建一个优先级为 1 的任务为例1 xTaskCreate(Task1, Task1, 128, NULL, 1, NULL) 2 | 3 |-- 分配 TCB 栈/* 本质只是分配内存 */ 4 | 5 |-- prvInitialiseNewTask() 6 | |-- 设置优先级 1 7 | |-- 设置任务名 Task1 8 | |-- 初始化 xStateListItem 9 | |-- 初始化任务栈 10 | 11 |-- prvAddNewTaskToReadyList() 12 |-- pxCurrentTCB 新任务 (如果是第一个任务) 13 |-- taskRECORD_READY_PRIORITY(1) /* 记录最高就绪优先级 */ 14 |-- vListInsertEnd( pxReadyTasksLists[1], xStateListItem ) 15 | 16 v 17 pxReadyTasksLists[1]: Task1 -- 哨兵任务创建完成后就已经在就绪链表里等着运行了。其他进入就绪链表的场景不只是创建任务以下情况也会进入就绪链表本质都是调用prvAddTaskToReadyList()以下是图片中提取的内容场景说明任务创建xTaskCreate()延时结束vTaskDelay() 时间到等到队列数据队列收到数据任务被唤醒等到信号量信号量释放任务被唤醒taskSELECT_HIGHEST_PRIORITY_TASK()如何选出下一个任务任务切换时vTaskSwitchContext()会调用这个宏决定下一个运行谁1#definetaskSELECT_HIGHEST_PRIORITY_TASK()2{3UBaseType_t uxTopPriorityuxTopReadyPriority;/* */4/* 找到最高优先级非空链表 */5while(listLIST_IS_EMPTY((pxReadyTasksLists[uxTopPriority])))6{7--uxTopPriority;8}910listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB,(pxReadyTasksLists[uxTopPriority]));11uxTopReadyPriorityuxTopPriority;12}第 1 步找最高优先级非空链表1while(listLIST_IS_EMPTY((pxReadyTasksLists[uxTopPriority])))2{3--uxTopPriority;4}从记录的最高优先级往下找直到找到有任务的就绪链表。实际在任务状态更新时有时uxTopReadyPriority已自动切到下一个最高优先级任务就绪链表硬件优化但是当不开启硬件优化时判空延迟更新uxTopReadyPriority需要判空来兜底容错。1uxTopReadyPriority523pxReadyTasksLists[5]-空,继续4pxReadyTasksLists[4]-空,继续5pxReadyTasksLists[3]-有 TaskA,停止第 2 步同优先级任务轮转选取1listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB,(pxReadyTasksLists[uxTopPriority]));在同优先级链表中取下一个任务更新pxCurrentTCB。同优先级多个任务会轮流执行。总体流程1 xTaskCreate 创建 TaskA 2 | 3 |-- 初始化 TCB 4 |-- prvAddTaskToReadyList() 5 | └-- TaskA 挂入 pxReadyTasksLists[2] 6 | 7 |-- vTaskStartScheduler() 启动调度器 8 | 9 v 10 taskSELECT_HIGHEST_PRIORITY_TASK() 11 | 12 |-- 找到 pxReadyTasksLists[2] 非空 13 |-- 选出 TaskA 14 |-- pxCurrentTCB TaskA 15 | 16 v 17 TaskA 开始运行3 .任务切换的 C 语言层vTaskSwitchContext()的作用PendSV 中断里会调用这个函数1 bl vTaskSwitchContext源码如下1voidvTaskSwitchContext(void)2{3if(uxSchedulerSuspended!(UBaseType_t)pdFALSE)/* 调度器是否被挂起 */4{5xYieldPendingpdTRUE;6}7else8{9xYieldPendingpdFALSE;10taskCHECK_FOR_STACK_OVERFLOW();/* 检查栈是否溢出 */11taskSELECT_HIGHEST_PRIORITY_TASK();/* 选出下一个任务更新pxCurrentTCB */12}13}它做什么vTaskSwitchContext()不负责保存/恢复寄存器只做三件事步骤代码作用1检查调度器是否挂起挂起时不切换2taskCHECK_FOR_STACK_OVERFLOW()检查栈是否溢出3taskSELECT_HIGHEST_PRIORITY_TASK()选出下一个任务更新pxCurrentTCB它不做什么不操作 PSP 栈指针不保存 R4-R11不恢复寄存器这些都由 PendSV 的汇编代码完成。在切换流程中的位置1 PendSV 中断 2 | 3 |-- 保存旧任务 R4-R11 - 汇编层 4 | 5 |-- vTaskSwitchContext() - C 语言层 6 | └-- taskSELECT_HIGHEST_PRIORITY_TASK() 7 | └-- pxCurrentTCB 新任务 8 | 9 └-- 恢复新任务 R4-R11 - 汇编层一句话C 层负责“选题”汇编层负责“换场”。调度器挂起时为什么不切换什么是调度器挂起调用vTaskSuspendAll会挂起调度器1 void vTaskSuspendAll( void ) 2 { 3 uxSchedulerSuspended; 4 }uxSchedulerSuspended是挂起调度器每调用一次vTaskSuspendAll()计数 1每调用一次vTaskResumeAll()计数 -1计数为0时调度器恢复正常。挂起时为什么不切换调度器挂起期间内核可能在批量修改任务链表。如果这时任务发生任务切换链表可能处于不完整状态导致系统出错。所以vTaskSwitchContext()里第一件事开始检查1 if( uxSchedulerSuspended ! pdFALSE ) 2 { 3 xYieldPending pdTRUE; // 不切换只记个标记 4 } 5 else 6 { 7 taskSELECT_HIGHEST_PRIORITY_TASK(); // 正常切换 8 }挂起期间切换请求被延迟不会立刻执行。xYieldPending作用1 static volatile BaseType_t xYieldPending pdFALSE;一个延迟切换标记pdFALSE没有待处理的切换请求pdTRUE有切换请求但当前没执行先记录什么时候置位情况1调度器挂起期间收到切换请求1 if( uxSchedulerSuspended ! pdFALSE ) 2 { 3 xYieldPending pdTRUE; // 现在不能切先记着 4 }情况2调度器恢复后发现有更高优先级任务1 if( pxTCB-uxPriority pxCurrentTCB-uxPriority ) 2 { 3 xYieldPending pdTRUE; 4 }情况3挂起期间积累的tick需要切换1 if( xTaskIncrementTick() ! pdFALSE ) 2 { 3 xYieldPending pdTRUE; 4 }什么时候执行切换调度器恢复后vTaskResumeAll()会检查这个标记1 if( xYieldPending ! pdFALSE ) 2 { 3 #if( configUSE_PREEMPTION ! 0 ) 4 { 5 xAlreadyYielded pdTRUE; 6 } 7 #endif 8 taskYIELD_IF_USING_PREEMPTION(); 9 }如果xYieldPending pdTRUE就触发真正的任务切换。SysTick里也会检查1 if( xYieldPending ! pdFALSE ) 2 { 3 xSwitchRequired pdTRUE; // 触发 PendSV 4 }完整流程示例1 调度器正常运行 2 | 3 |-- vTaskSuspendAll() uxSchedulerSuspended 1 4 | 5 |-- 期间某操作需要切换 6 | └-- vTaskSwitchContext() 7 | └-- xYieldPending pdTRUE (不切换只标记) 8 | 9 |-- xTaskResumeAll() uxSchedulerSuspended 0 10 └-- 检查 xYieldPending pdTRUE 11 └-- taskYIELD_IF_USING_PREEMPTION() 12 └-- 触发 PendSV, 真正切换 13 | 14 v 15 新任务开始运行三个变量关系1 uxSchedulerSuspended - 调度器是否挂起 2 xYieldPending - 是否有待处理的切换请求 3 pxCurrentTCB - 当前运行的是哪个任务变量作用uxSchedulerSuspended控制能不能切换xYieldPending记录有没有切换请求被延迟pxCurrentTCB记录当前运行谁配合关系1 切换请求到来 2 | 3 |-- 调度器未挂起 - 立刻切换更新 pxCurrentTCB 4 | 5 └-- 调度器已挂起 - xYieldPending pdTRUE等恢复后再切一句话总结vTaskSwitchContext()负责选题调度器挂起时切换会被推迟并用xYieldPending记住这个请求等调度器恢复后再执行。