整个调度模块集中在单个文件:tasks.c (V11.1.0+)+ 头文件 task.h。
一、核心数据结构
1. TCB(任务控制块)
定义:tasks.c#L374-L452
TCB 是任务调度的心脏,字段分以下几类:
| 类别 | 关键字段 | 作用 |
|---|---|---|
| 栈管理 | pxTopOfStack(必须首字段)、pxStack、pxEndOfStack |
栈顶/栈底指针。首字段要求便于汇编上下文切换代码直接通过 TCB 指针定位 |
| 列表挂接 | xStateListItem、xEventListItem |
每个任务有两个链表节点:状态节点(就绪/阻塞/挂起表共用)、事件节点(挂在队列/信号量/事件组等等待表) |
| 优先级 | uxPriority(实时优先级,0 最低)、uxBasePriority(互斥锁优先级继承用) |
|
| SMP 专用 | xTaskRunState(-1=未运行 / -2=待让出 / 0~N-1=所在核)、uxTaskAttributes、uxCoreAffinityMask |
tasks.c#L318-L330 |
| MPU 安全 | xMPUSettings(必须第二字段)、uxCriticalNesting |
|
| 互斥锁 | uxMutexesHeld |
跟踪互斥锁持有计数 |
| 任务通知 | ulNotifiedValue[]、ucNotifyState[](configTASK_NOTIFICATION_ARRAY_ENTRIES 个) |
轻量级 IPC |
| 调试/统计 | pcTaskName[]、uxTCBNumber、ulRunTimeCounter |
|
| 分配方式 | ucStaticallyAllocated:动态/静态栈+TCB 的 3 种组合 tasks.c#L123-L125 |
2. 全局调度列表
定义:tasks.c#L471-L530
pxReadyTasksLists[ configMAX_PRIORITIES ] ← 就绪表:每个优先级一条链表
pxDelayedTaskList / pxOverflowDelayedTaskList ← 双延迟表(解决 tick 溢出)
xPendingReadyList ← 调度器挂起时临时寄存就绪的任务
xSuspendedTaskList ← vTaskSuspend 挂起的任务
xTasksWaitingTermination ← 已删除等待回收的任务
关键变量:
pxCurrentTCB(单核)/pxCurrentTCBs[](SMP 数组)tasks.c#L458-L469uxTopReadyPriority--- 当前最高就绪优先级缓存xTickCount--- 系统节拍计数xNextTaskUnblockTime--- 下一个阻塞任务到期时间(用于 tickless idle)uxSchedulerSuspended--- 调度器挂起深度(vTaskSuspendAll/vTaskResumeAll)
3. 当前任务指针(单核 vs SMP)
// 单核
TCB_t * volatile pxCurrentTCB;
// 多核
TCB_t * volatile pxCurrentTCBs[configNUMBER_OF_CORES];
#define pxCurrentTCB xTaskGetCurrentTaskHandle() // 重定义为函数
二、任务生命周期与 API
1. 创建任务
流程:xTaskCreate() → prvCreateTask()(分配栈+TCB)→ prvInitialiseNewTask()(初始化 TCB 字段、构造初始栈帧)→ prvAddNewTaskToReadyList()
关键函数 :prvInitialiseNewTask
核心步骤:
- 栈填充 :用
tskSTACK_FILL_BYTE(0xA5)填充,便于后续栈高水位检测(tasks.c#L1843-L1847) - 栈顶对齐 :按
portBYTE_ALIGNMENT_MASK对齐(8 或 16 字节),支持向下/向上两种栈生长方向 - 事件链表项初始化 :
xEventListItem的 itemValue 设为configMAX_PRIORITIES - uxPriority(反向优先级 ,使高优先级排在事件链表头)tasks.c#L1940 - 构造初始栈帧 :
pxPortInitialiseStack()(可移植层实现)------ 伪造一个"任务刚被中断"的栈,返回地址设为任务函数入口,使调度器第一次切换到该任务时能正常"弹出"并开始执行(tasks.c#L1961-L1964)
2. 启动调度器
vTaskStartScheduler():
1. prvCreateIdleTasks() ------ 每核 1 个空闲任务(优先级 0)
2. xTimerCreateTimerTask() ------ 若启用定时器,创建 Timer Service Task
3. portDISABLE_INTERRUPTS() ------ 关中断,防止启动前 tick 触发
4. xSchedulerRunning = TRUE
5. xPortStartScheduler() ------ 可移植层:配置 SysTick/SVC/PendSV,启动第一个任务
注意:xPortStartScheduler() 通常不返回 ;返回 pdTRUE 表示堆不够创建 Idle/Timer 任务;返回 pdFALSE 表示应用调用了 xTaskEndScheduler()(多数 port 不实现)。
3. 任务状态转换
状态枚举:task.h#L105-L113
xTaskCreate 时间到/事件
┌─────────────► Ready ◄───────────┐
│ │ │
│ 选中最高优先级 │ 阻塞/延时 │ vTaskSuspend
│ ▼ │
└────────────── Running ────────► Blocked ───────► Suspended
主动让出/时间片到期 portMAX_DELAY vTaskResume
└────────────────┘
vTaskDelete() (也可从 Suspended 恢复)
└──────────────► Deleted(内存由 Idle 任务回收)
查询状态:eTaskGetState() 通过判断 xStateListItem 当前挂在哪个链表上来区分(就绪/延迟挂起/删除链表)。
4. 阻塞(延时/等待)
vTaskDelay() 与 vTaskDelayUntil() 最终都调用:
prvAddCurrentTaskToDelayedList()
逻辑要点:
- 从就绪表移除当前任务(tasks.c#L8598-L8603)
- 若
xTicksToWait == portMAX_DELAY && xCanBlockIndefinitely→ 加入xSuspendedTaskList(永久阻塞) - 否则计算唤醒时间
xTimeToWake = xTickCount + xTicksToWait,按溢出情况挂入两个延迟表之一:- 未溢出 →
pxDelayedTaskList(按唤醒时间升序插入) - 已溢出 →
pxOverflowDelayedTaskList
- 未溢出 →
- 若该任务排到延迟表头,更新
xNextTaskUnblockTime(tickless 优化用)
Tick 溢出的巧妙设计 :两个延迟表在 xTickCount 归零时互换 (taskSWITCH_DELAYED_LISTS,tasks.c#L264-L276),使溢出延迟表变为当前延迟表。这样溢出问题被优雅地 O(1) 解决,无需遍历重排。
三、调度核心算法
1. 最高优先级任务选择
单核通用版(非 port 优化) :tasks.c#L193-L209
// 从 uxTopReadyPriority 向下找第一个非空就绪链表
while( listLIST_IS_EMPTY( &pxReadyTasksLists[uxTopPriority] ) ) { --uxTopPriority; }
// 通过 pxIndex 游标的方式取下一个(同优先级轮转,时间片)
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &pxReadyTasksLists[uxTopPriority] );
uxTopReadyPriority = uxTopPriority;
Port 优化版(如 ARM Cortex-M 的 CLZ 指令) :tasks.c#L235-L243 --- 通过 portGET_HIGHEST_PRIORITY 宏(一条 CLZ + 位运算)O(1) 找到最高优先级,避免 while 循环。
SMP 版 :调用 prvSelectHighestPriorityTask(xCoreID),需考虑核心亲和性与任务运行状态。
2. Tick 中断处理(调度的"心跳")
xTaskIncrementTick() --- 每个 SysTick 触发。核心流程:
调度器未挂起?
├─ Yes → xTickCount++
│ 溢出? → taskSWITCH_DELAYED_LISTS() 互换延迟表
│ xConstTickCount >= xNextTaskUnblockTime?
│ └─ Yes → 遍历延迟表,到期任务移到就绪表:
│ (1) 从延迟表移除 xStateListItem
│ (2) 若还挂在事件等待表,也移除 xEventListItem
│ (3) prvAddTaskToReadyList()
│ (4) 抢占开启时:若新就绪任务优先级 > 当前 → xSwitchRequired = TRUE
│ 时间片(configUSE_TIME_SLICING)?
│ └─ 若当前优先级就绪表有 >1 个任务 → xSwitchRequired = TRUE
│ 处理 xYieldPendings[core]
│ 调用 vApplicationTickHook()
└─ No → xPendedTicks++(延迟补偿 tick,待 xTaskResumeAll 时补算)
关键点 tasks.c#L4861-L4891:同优先级任务的时间片轮转仅在"抢占 + 时间片 都开"且"同优先级就绪表 >1"时才在 tick 末尾请求切换。
3. 任务让渡(Yield)
在 API 内部请求调度:taskYIELD_WITHIN_API() / portYIELD_WITHIN_API(),对应 Cortex-M 的 PendSV。
抢占式自动让出条件:
- Tick 中解阻塞出更高优先级任务(tasks.c#L4838-L4841)
xQueueSend/Receive、xSemaphoreGive等从阻塞唤醒高优先级任务- 同优先级时间片到期
4. 调度器挂起/恢复
vTaskSuspendAll():uxSchedulerSuspended++,此后 tick 不再调度,仅把解阻塞任务放入xPendingReadyListxTaskResumeAll():递减计数至 0 时,把xPendingReadyList全部移入就绪表,并补算xPendedTicks次累计 tick
四、SMP 多核调度(V11 特色)
configNUMBER_OF_CORES > 1 时触发 SMP 路径,关键差异:
| 机制 | 单核 | SMP |
|---|---|---|
| 运行状态判断 | pxTCB == pxCurrentTCB |
xTaskRunState ∈ [0, N-1] 表示运行核 ID |
| 让出触发 | portYIELD_WITHIN_API() |
prvYieldCore(xCoreID):本核置 xYieldPendings[core]=TRUE,其他核通过 portYIELD_CORE() 发核间中断 tasks.c#L349-L365 |
| 解阻塞抢占 | 仅比当前高才切 | prvYieldForTask():选择优先级最低的运行核,若 pxTCB 优先级更高则让其让出 |
| 亲和性 | 无 | uxCoreAffinityMask(位掩码,每位对应一个核心),xTaskCreateAffinitySet() 创建时绑定 |
| 任务可被调度上核条件 | taskTASK_IS_RUNNING()==FALSE |
xTaskRunState==taskTASK_NOT_RUNNING(-1) 且 亲和位包含目标核 |
| 关临界 | 全局 portENTER_CRITICAL() |
每核独立 uxCriticalNesting(TCB 中)tasks.c#L335-L340 |
五、空闲任务与资源回收
prvIdleTask 永远在优先级 0,永不阻塞。做两件事:
prvCheckTasksWaitingTermination():清理xTasksWaitingTermination中待删除任务的 TCB/栈(调用者任务不能自己释放自己的内存)- 若
configUSE_TICKLESS_IDLE:进入低功耗睡眠,直到下一个阻塞任务到期时间
六、调度模块的配置开关一览
| 宏 | 效果 |
|---|---|
configUSE_PREEMPTION |
0=协作式;1=抢占式(默认) |
configUSE_TIME_SLICING |
同优先级 tick 末轮转(1=开) |
configMAX_PRIORITIES |
优先级数,影响就绪表数组大小 |
configNUMBER_OF_CORES |
1=单核(传统 FreeRTOS);>1=SMP |
configUSE_PORT_OPTIMISED_TASK_SELECTION |
启用 port 专属的 CLZ/位运算 O(1) 选任务 |
configUSE_TICKLESS_IDLE |
Tickless 低功耗 |
configUSE_CORE_AFFINITY |
SMP 下任务绑定核心 |
portSTACK_GROWTH |
<0 向下生长(多数架构);>0 向上生长 |
INCLUDE_vTaskDelay/INCLUDE_vTaskSuspend/INCLUDE_xTaskAbortDelay 等 |
裁剪可选 API |
七、调度器数据流总图
SysTick ISR
│
▼
xTaskIncrementTick()
│ │
│ ▼
│ xTickCount++ / 溢出表交换
│ │
│ ▼
│ 遍历延迟表 → 超时任务移至就绪表
│ (pxDelayedTaskList → pxReadyTasksLists[])
│ │
│ ▼
│ 需要切换? → xSwitchRequired = TRUE
│ │
▼ ▼
PendSV/SVC(上下文切换)
│
▼
taskSELECT_HIGHEST_PRIORITY_TASK() ← 选出新 pxCurrentTCB
├─ 同优先级:通过 listGET_OWNER_OF_NEXT_ENTRY 的 pxIndex 游标轮转
└─ SMP:prvSelectHighestPriorityTask(xCoreID)
│
▼
调用 pxPortInitialiseStack() 造出的栈帧恢复 → 新任务运行
八、常见踩坑点(经验参考)
结合过往经验的两个高频"调度不工作"原因:
- 中断向量名不匹配 :Cortex-M 启动文件常将 SVC_Handler/PendSV_Handler/SysTick_Handler 定义为弱符号,而 FreeRTOS port.c 使用
vPortSVCHandler/xPortPendSVHandler/xPortSysTickHandler。正确做法 :在FreeRTOSConfig.h中用宏映射,而非改内核或手写汇编。 - 切勿在明确要求 RTOS 的场景下改为裸机
while(1):调度不工作应修复 port/配置/栈/堆配置,而非移除任务模型来"绕过"。