FreeRTOS学习(三)- 任务调度模块

整个调度模块集中在单个文件:tasks.c (V11.1.0+)+ 头文件 task.h


一、核心数据结构

1. TCB(任务控制块)

定义:tasks.c#L374-L452

TCB 是任务调度的心脏,字段分以下几类:

类别 关键字段 作用
栈管理 pxTopOfStack(必须首字段)、pxStackpxEndOfStack 栈顶/栈底指针。首字段要求便于汇编上下文切换代码直接通过 TCB 指针定位
列表挂接 xStateListItemxEventListItem 每个任务有两个链表节点:状态节点(就绪/阻塞/挂起表共用)、事件节点(挂在队列/信号量/事件组等等待表)
优先级 uxPriority(实时优先级,0 最低)、uxBasePriority(互斥锁优先级继承用)
SMP 专用 xTaskRunState(-1=未运行 / -2=待让出 / 0~N-1=所在核)、uxTaskAttributesuxCoreAffinityMask tasks.c#L318-L330
MPU 安全 xMPUSettings(必须第二字段)、uxCriticalNesting
互斥锁 uxMutexesHeld 跟踪互斥锁持有计数
任务通知 ulNotifiedValue[]ucNotifyState[]configTASK_NOTIFICATION_ARRAY_ENTRIES 个) 轻量级 IPC
调试/统计 pcTaskName[]uxTCBNumberulRunTimeCounter
分配方式 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-L469
  • uxTopReadyPriority --- 当前最高就绪优先级缓存
  • 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

核心步骤:

  1. 栈填充 :用 tskSTACK_FILL_BYTE(0xA5) 填充,便于后续栈高水位检测(tasks.c#L1843-L1847)
  2. 栈顶对齐 :按 portBYTE_ALIGNMENT_MASK 对齐(8 或 16 字节),支持向下/向上两种栈生长方向
  3. 事件链表项初始化xEventListItem 的 itemValue 设为 configMAX_PRIORITIES - uxPriority反向优先级 ,使高优先级排在事件链表头)tasks.c#L1940
  4. 构造初始栈帧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()

逻辑要点:

  1. 从就绪表移除当前任务(tasks.c#L8598-L8603)
  2. xTicksToWait == portMAX_DELAY && xCanBlockIndefinitely → 加入 xSuspendedTaskList(永久阻塞)
  3. 否则计算唤醒时间 xTimeToWake = xTickCount + xTicksToWait,按溢出情况挂入两个延迟表之一:
    • 未溢出 → pxDelayedTaskList(按唤醒时间升序插入)
    • 已溢出 → pxOverflowDelayedTaskList
  4. 若该任务排到延迟表头,更新 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/ReceivexSemaphoreGive 等从阻塞唤醒高优先级任务
  • 同优先级时间片到期

4. 调度器挂起/恢复

  • vTaskSuspendAll()uxSchedulerSuspended++,此后 tick 不再调度,仅把解阻塞任务放入 xPendingReadyList
  • xTaskResumeAll():递减计数至 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,永不阻塞。做两件事:

  1. prvCheckTasksWaitingTermination():清理 xTasksWaitingTermination 中待删除任务的 TCB/栈(调用者任务不能自己释放自己的内存)
  2. 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() 造出的栈帧恢复 → 新任务运行

八、常见踩坑点(经验参考)

结合过往经验的两个高频"调度不工作"原因:

  1. 中断向量名不匹配 :Cortex-M 启动文件常将 SVC_Handler/PendSV_Handler/SysTick_Handler 定义为弱符号,而 FreeRTOS port.c 使用 vPortSVCHandler/xPortPendSVHandler/xPortSysTickHandler正确做法 :在 FreeRTOSConfig.h 中用宏映射,而非改内核或手写汇编。
  2. 切勿在明确要求 RTOS 的场景下改为裸机 while(1):调度不工作应修复 port/配置/栈/堆配置,而非移除任务模型来"绕过"。
相关推荐
吃着火锅x唱着歌1 小时前
Effective C++ 学习笔记 条款40 明智而审慎地使用多重继承
c++·笔记·学习
xian_wwq2 小时前
【学习笔记】Prompt Engineering 没死,只是它不再够用了-2/16
笔记·学习·prompt
9527出列2 小时前
Tursm中WebSocket 连接生命周期:建立、保活、断开与重连
开源
冬奇Lab2 小时前
开源项目第182期:Graphify — 把整个代码库变成可查询知识图谱,让 AI 编程助手真正「懂」你的项目
人工智能·开源·资讯
天吾cc2 小时前
RTT-ADC
单片机·嵌入式硬件·学习
kaixin_啊啊3 小时前
test_机器学习算法学习
学习·算法·机器学习
m4Rk_3 小时前
【论文阅读】Agent 记忆机制(34):MemoryBank——用遗忘曲线管理可强化的长期对话记忆
论文阅读·人工智能·学习·开源·github
初禾w-w3 小时前
阿里云开源 UModel 并发起 USS 倡议:构建企业级通用语义标准,重塑 AI 交互底座
人工智能·阿里云·开源·企业ai·对象图语义·语义割裂
奈斯先生Vector3 小时前
AI 辅助线上排障工作台:从故障证据、Codex 协作到可回放修复
人工智能·架构·开源·aigc·midjourney