任务相关函数
任务相关函数
FreeRTOS 中,任务相关 API 的底层核心其实都围绕几个动作展开:
创建任务 → 初始化 TCB 和任务栈 → 加入就绪链表 → 运行 → 挂起/恢复 → 删除
如果从内核的角度看,任务管理本质上就是:
TCB + 任务栈 + 各种状态链表 + 调度器
因此,分析任务相关函数时,不要只记 API 的功能,而应该重点关注:
- 任务的 TCB 是怎么建立起来的
- 任务栈是怎么初始化的
- 任务是怎么进入 Ready List 的
- 任务状态改变时,究竟是从哪个链表移动到了哪个链表
- 为什么某些操作会触发任务切换
- 为什么 ISR 中不能直接调用普通任务 API
一、任务创建:xTaskCreate()
1.1 xTaskCreate() 做了什么?
xTaskCreate() 的核心工作可以概括成:
申请 TCB 和任务栈 → 初始化 TCB → 初始化任务栈 → 把任务加入 Ready List
也就是说:
xTaskCreate()
│
├── 申请 TCB
│
├── 申请任务栈
│
├── prvInitialiseNewTask()
│ ├── 初始化 TCB
│ ├── 初始化任务栈
│ └── 构造任务第一次运行时的 CPU 上下文
│
└── prvAddNewTaskToReadyList()
└── 将任务加入 Ready List所以可以把整个创建过程记成:
内存 → TCB → 栈 → Ready List
1.2 动态创建首先申请什么?
当使用动态内存创建任务时:
xTaskCreate(...)内部会通过:
pvPortMalloc()申请内存。
需要申请两块主要内存:
┌─────────────────────┐
│ TCB │
│ 任务控制块 │
└─────────────────────┘
┌─────────────────────┐
│ │
│ Task Stack │
│ 任务栈 │
│ │
└─────────────────────┘其中:
- TCB:保存任务的各种管理信息
- 任务栈:保存任务运行过程中需要保存的 CPU 上下文以及局部变量等
pvPortMalloc() 不只是简单地“申请几个字节”,还会根据 FreeRTOS 的配置进行相应的内存对齐处理。
1.3 为什么要根据栈增长方向决定分配顺序?
源码中有:
#if( portSTACK_GROWTH > 0 )和:
#else这是因为不同 CPU 架构的栈增长方向可能不同。
例如 ARM Cortex-M 中,通常:
高地址
↓
栈
↓
低地址也就是:
栈向低地址增长
因此 FreeRTOS 会根据:
portSTACK_GROWTH决定栈和 TCB 的具体处理方式。
这里不用死记“到底哪个先申请”,真正需要记住的是:
FreeRTOS 需要同时为 TCB 和 Task Stack 分配内存,并且如果第二块内存申请失败,需要释放第一块已经申请的内存。
例如:
申请 TCB
│
├── 成功
│
└── 申请 Stack
│
├── 成功 → 继续创建
│
└── 失败 → 释放 TCB这样可以避免内存泄漏。
1.4 TCB 在初始化之前是什么?
这是理解任务创建非常重要的一点。
执行:
prvInitialiseNewTask()之前:
TCB
┌─────────────────────┐
│ 一块刚申请出来的内存 │
│ │
│ 还没有完整的任务信息 │
└─────────────────────┘此时只是:
有了一个 TCB 内存空间,以及任务栈空间。
执行:
prvInitialiseNewTask()之后:
TCB
┌──────────────────────────┐
│ 任务名称 │
│ 任务入口函数 │
│ 任务参数 │
│ 任务优先级 │
│ pxTopOfStack │
│ State List Item │
│ Event List Item │
│ 互斥锁相关信息 │
│ ... │
└──────────────────────────┘这时候才真正成为一个完整的:
任务控制块
所以:
创建任务,本质上就是把一个空 TCB 填充成一个完整任务对象。
二、初始化任务:prvInitialiseNewTask()
prvInitialiseNewTask() 是任务创建过程中非常核心的一个函数。
它主要负责:
prvInitialiseNewTask()
│
├── MPU / 特权模式处理
│
├── 初始化任务栈
│
├── 计算栈顶地址
│
├── 设置任务名称
│
├── 设置任务优先级
│
├── 初始化 State List Item
│
├── 初始化 Event List Item
│
└── 调用 pxPortInitialiseStack()
│
└── 构造任务第一次运行时的 CPU 上下文可以把它理解成:
给 TCB 填资料 + 给任务栈准备第一次运行所需要的 CPU 上下文。
2.1 初始化任务栈
如果启用了:
- 栈溢出检测
- Trace
- Stack High Water Mark
FreeRTOS 会使用:
memset(
pxNewTCB->pxStack,
tskSTACK_FILL_BYTE,
...
);把任务栈填充成特定的字节。
这样以后就可以通过检查这些“没有被使用过的区域”来判断:
这个任务到底使用了多少栈空间。
这也是 uxTaskGetStackHighWaterMark() 等功能的基础。
2.2 计算栈顶地址
例如 Cortex-M 这种栈向低地址增长的情况:
pxTopOfStack =
pxNewTCB->pxStack + ( ulStackDepth - 1 );假设:
Stack 起始地址:0x20001000
Stack 大小:256 个 StackType_t那么栈的另一端位于更高地址。
最终 FreeRTOS 会计算出:
pxTopOfStack
↓
┌──────────────────────┐ 高地址
│ │
│ Task Stack │
│ │
│ │
└──────────────────────┘
↑
栈向下增长同时还会进行:
configASSERT(...)检查栈地址是否满足 CPU 要求的对齐条件。
2.3 初始化任务名称
任务名称会被复制到:
pxNewTCB->pcTaskName例如:
xTaskCreate(
Task1,
"Task1",
...
);最终 TCB 中会保存:
pcTaskName
↓
"Task1"这也是为什么调试 FreeRTOS 时,可以在 IDE 中看到类似:
Task1
Task2
IDLE
Timer这样的任务名称。
2.4 初始化任务优先级
源码中会限制:
uxPriority < configMAX_PRIORITIES因为 FreeRTOS 的 Ready List 通常按照优先级建立:
pxReadyTasksLists[0]
pxReadyTasksLists[1]
pxReadyTasksLists[2]
...
pxReadyTasksLists[configMAX_PRIORITIES - 1]所以:
任务优先级本质上也是 Ready List 的索引。
例如:
Priority 0 → pxReadyTasksLists[0]
Priority 1 → pxReadyTasksLists[1]
Priority 2 → pxReadyTasksLists[2]
Priority 3 → pxReadyTasksLists[3]这也是为什么任务优先级不能超过:
configMAX_PRIORITIES2.5 初始化 TCB 中的链表节点
一个任务不是只需要一个状态。
FreeRTOS 中,一个任务可能处于:
Ready
Blocked
Suspended同时还可能因为等待:
Queue
Semaphore
Event Group
...而出现在某个事件等待链表中。
因此 TCB 中有两个非常重要的链表节点:
xStateListItem
xEventListItem初始化:
vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
vListInitialiseItem( &( pxNewTCB->xEventListItem ) );可以理解成:
TCB
│
├── xStateListItem
│ └── 用于任务状态链表
│
└── xEventListItem
└── 用于事件等待链表这是理解 FreeRTOS 链表机制的一个关键点。
三、初始化任务栈:pxPortInitialiseStack()
这一部分是任务创建中最值得理解的地方。
因为:
FreeRTOS 创建任务时,并没有真的让任务函数立即执行,而是在任务栈中“伪造”出了一个任务第一次运行时应该拥有的 CPU 上下文。
也就是说:
现在任务还没有运行
↓
先在任务栈里“伪造”寄存器现场
↓
以后第一次切换到这个任务
↓
从栈中恢复这些寄存器
↓
任务函数正式开始运行3.1 为什么要伪造寄存器现场?
假设任务函数:
void Task(void *pvParameters)
{
while(1)
{
...
}
}它实际上需要 CPU 执行:
PC → Task
R0 → pvParameters
其他寄存器 → 初始值
SP → 任务栈但是任务刚创建出来时,它根本没有执行过。
因此 FreeRTOS 必须人为构造一个“假现场”。
以后调度器执行上下文恢复时:
任务栈
↓
恢复寄存器
↓
恢复 PC
↓
进入 Task()3.2 pxPortInitialiseStack() 干了什么?
以 ARM 端口为例:
*pxTopOfStack = NULL;
pxTopOfStack--;
*pxTopOfStack = NULL;
pxTopOfStack--;
*pxTopOfStack = NULL;
pxTopOfStack--;这些属于栈中的占位数据。
随后保存:
*pxTopOfStack = portINITIAL_SPSR;也就是任务启动时的程序状态寄存器。
然后设置:
*pxTopOfStack = ( StackType_t ) pxCode;这里非常重要。
pxCode 就是任务函数入口地址。
也就是说:
PC
↓
pxCode
↓
Task()3.3 R0 保存什么?
继续往下:
*pxTopOfStack = ( StackType_t ) pvParameters;这对应:
R0 = pvParameters因为 ARM 调用约定中,函数的第一个参数通常通过 R0 传递。
所以:
void Task(void *pvParameters)任务第一次运行的时候:
R0
↓
pvParameters
↓
Task(pvParameters)这就是为什么 xTaskCreate() 可以给任务传递参数。
3.4 LR 为什么设置成 prvTaskExitError?
代码:
*pxTopOfStack =
( StackType_t ) prvTaskExitError;对应:
LR = prvTaskExitError正常情况下任务应该:
while(1)
{
}一直运行。
如果任务函数意外执行到:
return;那么就相当于:
任务函数返回
↓
使用 LR
↓
进入 prvTaskExitError()而不是像普通 C 函数一样随便返回到一个未知位置。
3.5 为什么寄存器初始值写成 0x01010101?
例如:
R1 = 0x01010101
R2 = 0x02020202
R3 = 0x03030303
...这些值本身没有什么业务意义。
主要是为了:
调试时能够很容易识别寄存器是否被正确恢复。
如果调试器里看到:
R4 = 0x04040404
R5 = 0x05050505就很容易知道:
当前看到的确实是 FreeRTOS 初始化出来的任务上下文。
四、任务第一次运行的本质
到这里,可以把任务第一次运行串起来。
创建任务时:
xTaskCreate()
│
├── 创建 TCB
│
├── 创建 Task Stack
│
├── 初始化 TCB
│
├── pxPortInitialiseStack()
│ │
│ └── 在栈里伪造 CPU 上下文
│
└── 加入 Ready List然后调度器选择这个任务:
Ready Task
↓
任务切换
↓
从 Task Stack 恢复寄存器
↓
恢复 PC
↓
PC = Task()
↓
任务开始运行因此:
任务创建阶段并不是直接调用任务函数,而是在任务栈中提前构造好“第一次运行时的现场”。
这是理解 FreeRTOS 上下文切换的关键。
五、加入就绪列表:prvAddNewTaskToReadyList()
任务初始化完成以后,还不能被调度器直接运行。
还需要:
prvAddNewTaskToReadyList( pxNewTCB );它的核心作用就是:
把新任务正式登记到 FreeRTOS 的任务管理系统,并将它加入对应优先级的 Ready List。
整体流程:
prvAddNewTaskToReadyList()
│
├── 进入临界区
│
├── 任务数量 +1
│
├── 判断是否第一个任务
│
├── 必要时初始化任务链表
│
├── 更新任务编号
│
├── traceTASK_CREATE()
│
├── 加入 Ready List
│
├── portSETUP_TCB()
│
└── 退出临界区
│
↓
调度器已经运行?
│
└── 是
↓
新任务优先级更高?
│
└── 是
↓
触发任务切换5.1 为什么要进入临界区?
因为这里会修改 FreeRTOS 最重要的数据结构:
Ready List
Task List
pxCurrentTCB
任务数量
优先级状态如果修改过程中突然被中断打断,就可能造成链表或者任务状态的不一致。
因此:
taskENTER_CRITICAL();保证这一段操作具有原子性。
5.2 第一个任务有什么特殊?
如果:
pxCurrentTCB == NULL说明:
系统还没有当前任务。
那么:
pxCurrentTCB = pxNewTCB;同时如果这是系统创建的第一个任务:
prvInitialiseTaskLists();初始化 FreeRTOS 内核所需要的各种任务链表。
因此:
创建第一个任务
↓
pxCurrentTCB = 新任务
↓
初始化各种任务链表5.3 调度器没启动时创建任务
如果:
xSchedulerRunning == pdFALSE此时虽然可以创建多个任务,但是:
还不会真正发生任务切换。
FreeRTOS 会根据优先级更新:
pxCurrentTCB例如:
Task A:Priority 2
Task B:Priority 5调度器还没启动:
pxCurrentTCB
↓
Task B但是:
这不意味着 Task B 已经开始执行。
真正开始调度需要:
vTaskStartScheduler();5.4 调度器已经启动时
如果:
xSchedulerRunning == pdTRUE新任务加入 Ready List 后,如果:
pxNewTCB->uxPriority > pxCurrentTCB->uxPriority那么:
taskYIELD_IF_USING_PREEMPTION();触发一次任务切换。
也就是说:
当前任务:Priority 2
新任务:Priority 5
↓
新任务优先级更高
↓
触发调度
↓
Priority 5 任务获得 CPU六、任务删除:vTaskDelete()
删除任务的核心问题是:
把任务从调度器管理体系中移除,并最终释放 TCB 和任务栈。
但是这里有一个非常重要的特殊情况:
任务删除自己时,不能立即释放自己的 TCB 和任务栈。
因为:
当前正在运行的任务
↓
正在使用自己的栈
↓
不能把自己的栈释放掉所以 FreeRTOS 引入:
xTasksWaitingTermination专门保存:
已经删除,但还不能立即释放资源的任务。
七、vTaskDelete() 的执行流程
整体流程:
vTaskDelete()
│
├── 进入临界区
│
├── 获取目标任务 TCB
│
├── 从 Ready / Blocked 状态链表移除
│
├── 从 Event List 移除
│
├── 更新任务编号
│
├── 判断是不是当前任务
│
├──── 是 ──────────────────────┐
│ │
│ 加入 xTasksWaitingTermination
│ │
│ uxDeletedTasksWaitingCleanUp++
│ │
├──── 否 ──────────────────────┤
│ │
│ uxCurrentNumberOfTasks--
│ │
│ prvDeleteTCB()
│ │
└──────────────────────────────┘
│
├── traceTASK_DELETE()
│
└── 退出临界区
│
↓
如果删除的是自己
│
↓
portYIELD_WITHIN_API()
│
↓
切换到其他任务
│
↓
Idle Task 最终回收资源7.1 删除其他任务
例如:
vTaskDelete(TaskA);而当前运行的是:
TaskB那么:
TaskB
│
└── 删除 TaskA
│
├── 从任务链表移除
├── 释放 TaskA 栈
└── 释放 TaskA TCB因为 TaskB 没有使用 TaskA 的栈,所以可以立即释放。
7.2 删除自己
如果:
vTaskDelete(NULL);表示:
删除当前任务自己。
这时候:
TaskA 正在运行
↓
TaskA 删除自己
↓
TaskA 仍然在使用自己的栈
↓
不能马上释放因此:
vListInsertEnd(
&xTasksWaitingTermination,
&( pxTCB->xStateListItem )
);把它放到:
xTasksWaitingTermination等待 Idle Task 后续处理。
7.3 为什么最终由 Idle Task 回收?
因为 Idle Task 一直存在。
它在空闲时可以检查:
uxDeletedTasksWaitingCleanUp如果有待回收任务:
Idle Task
↓
检查待清理任务
↓
prvDeleteTCB()
↓
释放 Task Stack
↓
释放 TCB所以:
删除自己 ≠ 立即释放内存。
更准确地说:
删除自己是“逻辑删除立即完成,物理内存延迟回收”。
这个概念以后分析 FreeRTOS 内存问题时非常重要。
八、挂起任务:vTaskSuspend()
vTaskSuspend() 和 vTaskDelete() 有一个非常明显的区别:
Delete
↓
任务生命周期结束
↓
最终释放 TCB + Stack
Suspend
↓
任务只是暂时不参与调度
↓
TCB + Stack 仍然存在
↓
以后可以 Resume所以:
Suspend 是改变任务状态,Delete 是结束任务生命周期。
九、vTaskSuspend() 的核心过程
任务挂起,本质上就是:
把任务从当前所在的状态链表中移除,然后放入 Suspended List。
流程:
vTaskSuspend()
│
├── 进入临界区
│
├── 获取目标任务 TCB
│
├── 从 State List 移除
│
├── 从 Event List 移除
│
├── 加入 xSuspendedTaskList
│
└── 退出临界区
│
↓
是不是当前任务?
│
┌────┴────┐
│ │
否 是
│ │
│ 调度器运行?
│ │
│ ┌──┴──┐
│ │ │
│ 是 否
│ │ │
│ 触发切换 判断其他任务
│
└──────────────9.1 为什么要从 Event List 中移除?
一个任务可能不是单纯处于 Ready 状态。
例如:
TaskA
↓
等待 Semaphore
↓
Event List此时如果:
vTaskSuspend(TaskA);就必须把它从:
Event List中也移除。
否则:
TaskA
├── Suspended List
└── Semaphore Event List一个任务同时挂在两个不应该同时存在的状态结构中,就会造成内核状态混乱。
所以挂起任务时:
State List 和 Event List 都需要处理。
9.2 挂起自己为什么会触发任务切换?
例如:
void TaskA(void *arg)
{
vTaskSuspend(NULL);
...
}执行到:
vTaskSuspend(NULL);以后:
TaskA
↓
进入 Suspended List
↓
已经不能继续运行
↓
必须寻找其他 Ready Task
↓
触发任务切换所以:
挂起自己,本质上等于主动放弃 CPU,并且进入不可运行状态。
十、恢复任务:vTaskResume()
恢复任务和挂起任务正好相反。
挂起:
Ready List
↓
Suspended List恢复:
Suspended List
↓
Ready List所以最重要的一句话:
vTaskResume() 的本质就是把任务从 Suspended List 移回 Ready List。
十一、vTaskResume() 执行流程
vTaskResume()
│
├── 检查参数
│
├── 进入临界区
│
├── 判断任务是否真的处于 Suspended 状态
│
├── 从 Suspended List 移除
│
├── 加入 Ready List
│
├── 比较任务优先级
│
│ 恢复任务优先级
│ │
│ ↓
│ ≥ 当前任务优先级?
│ │ │
│ 是 否
│ │ │
│ ↓ ↓
│ 触发切换 不切换
│
└── 退出临界区11.1 为什么恢复任务后可能立即发生切换?
例如:
当前任务 TaskA:Priority 2
被挂起的 TaskB:Priority 5执行:
vTaskResume(TaskB);之后:
TaskB
↓
Suspended List
↓
Ready List现在:
TaskA:Priority 2
TaskB:Priority 5因为 TaskB 优先级更高,所以抢占:
TaskA
↓
TaskB因此:
Resume 不只是“修改任务状态”,还可能直接导致调度。
十二、vTaskResume() 和 xTaskResumeFromISR()
这两个函数最容易混淆。
它们的共同目标都是:
Suspended List
↓
Ready List但最大的区别是:
vTaskResume()
↓
任务上下文调用
xTaskResumeFromISR()
↓
中断上下文调用所以:
两者功能类似,但执行环境完全不同。
不能在 ISR 中随便调用普通任务 API。
十三、xTaskResumeFromISR()
ISR 中恢复任务使用:
xTaskResumeFromISR()它最大的特点就是:
不能像普通任务 API 一样直接进行普通的任务切换,而是通过返回值告诉 ISR:退出中断后是否需要进行任务切换。
13.1 为什么不能在 ISR 里面直接切换任务?
因为当前 CPU 正处于:
ISR
↓
中断上下文此时不能简单理解成:
ISR → 直接跳到 TaskB正确的机制通常是:
ISR
↓
xTaskResumeFromISR()
↓
判断是否需要切换
↓
返回 pdTRUE
↓
ISR 退出
↓
触发 PendSV / 对应端口的上下文切换机制
↓
TaskB 运行所以 xTaskResumeFromISR() 的返回值非常重要。
十四、xTaskResumeFromISR() 源码流程
xTaskResumeFromISR()
│
├── 参数检查
│
├── 中断优先级合法性检查
│
├── portSET_INTERRUPT_MASK_FROM_ISR()
│ ↓
│ 保护内核数据结构
│
├── 判断任务是否处于 Suspended List
│
├── 判断调度器是否被挂起
│
├──── 调度器未挂起 ──────────────────┐
│ │
│ 恢复任务加入 Ready List │
│ │
│ 恢复任务优先级 ≥ 当前任务? │
│ │ │
│ 是 │
│ ↓ │
│ xYieldRequired = pdTRUE │
│ │
├──── 调度器已挂起 ──────────────────┤
│ │
│ 加入 xPendingReadyList │
│ │
└────────────────────────────────────┘
│
├── portCLEAR_INTERRUPT_MASK_FROM_ISR()
│
└── return xYieldRequired十五、为什么有 xPendingReadyList?
这里是 xTaskResumeFromISR() 中非常容易忽略的一点。
如果:
uxSchedulerSuspended != pdFALSE说明:
调度器当前被挂起。
此时 ISR 虽然想恢复任务,但不能直接按照普通流程修改 Ready List。
因此 FreeRTOS 会把相关任务暂时放到:
xPendingReadyList等待调度器恢复后再处理。
可以理解成:
ISR 想恢复 TaskA
↓
但是调度器现在不能处理
↓
先放到 xPendingReadyList
↓
调度器恢复
↓
再把 TaskA 正式处理成 Ready 状态因此:
xPendingReadyList 是 ISR 与调度器之间的一个“中转区”。
十六、xYieldRequired 为什么重要?
源码:
BaseType_t xYieldRequired = pdFALSE;如果:
pxTCB->uxPriority >= pxCurrentTCB->uxPriority则:
xYieldRequired = pdTRUE;最后:
return xYieldRequired;典型使用方式:
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTaskResumeFromISR(TaskHandle);然后根据返回值决定:
ISR
↓
恢复任务
↓
恢复任务优先级更高?
↓
是
↓
返回 pdTRUE
↓
ISR 退出时触发上下文切换所以:
xYieldRequired不是“现在立刻切换任务”,而是“通知外层:ISR 结束后需要切换任务”。
十七、四个任务状态 API 放在一起看
把前面的函数全部放到一起,就非常清楚了。
┌──────────────┐
│ Ready List │
└──────┬───────┘
│
vTaskSuspend()
│
↓
┌──────────────────┐
│ Suspended List │
└────────┬─────────┘
│
vTaskResume()
│
↓
┌──────────────┐
│ Ready List │
└──────────────┘ISR 版本:
ISR
│
└── xTaskResumeFromISR()
│
├── 可以直接处理
│ ↓
│ Ready List
│
└── 调度器被挂起
↓
xPendingReadyList
↓
调度器恢复后处理十八、Delete 和 Suspend 的本质区别
这两个函数非常容易混淆。
| 操作 | Suspend | Delete |
|---|---|---|
| 是否停止运行 | 是 | 是 |
| 是否还能 Resume | 可以 | 不可以 |
| TCB 是否保留 | 是 | 最终释放 |
| Stack 是否保留 | 是 | 最终释放 |
| 是否结束任务生命周期 | 否 | 是 |
| 自己操作时是否立即释放 | 不涉及释放 | 不能立即释放 |
| 最终状态 | Suspended | Destroyed |
一句话:
Suspend 是“暂时停工”,Delete 是“彻底注销”。
十九、任务创建、挂起、恢复、删除串起来
现在可以把几个 API 放到同一张图里:
xTaskCreate()
│
↓
┌──────────────────┐
│ 创建 TCB + Stack │
└────────┬─────────┘
│
↓
prvInitialiseNewTask()
│
↓
初始化 TCB + 构造栈现场
│
↓
prvAddNewTaskToReadyList()
│
↓
┌──────────────┐
│ Ready List │
└──────┬───────┘
│
vTaskSuspend()
│
↓
┌────────────────┐
│ Suspended List │
└───────┬────────┘
│
┌─────────┴─────────┐
│ │
vTaskResume() xTaskResumeFromISR()
│ │
└─────────┬─────────┘
↓
┌──────────────┐
│ Ready List │
└──────────────┘
│
│
vTaskDelete()
↓
┌───────────────────┐
│ 任务生命周期结束 │
└─────────┬─────────┘
│
删除自己?│
┌──────┴──────┐
│ │
是 否
│ │
↓ ↓
WaitingTermination 立即释放
│
↓
Idle Task
│
↓
prvDeleteTCB()二十、这一部分真正应该记住什么?
如果以后复习 FreeRTOS 任务管理,不需要重新把几百行源码全部看一遍。
抓住下面几个核心点就够了。
20.1 创建任务
xTaskCreate()
↓
申请 TCB + Stack
↓
prvInitialiseNewTask()
↓
初始化 TCB
↓
pxPortInitialiseStack()
↓
伪造第一次运行时的 CPU 上下文
↓
prvAddNewTaskToReadyList()
↓
Ready List核心:
创建任务 = 创建 TCB + 创建栈 + 构造初始上下文 + 加入 Ready List。
20.2 挂起任务
Ready / Blocked
↓
vTaskSuspend()
↓
Suspended List核心:
Suspend = 从原状态链表移除 → 放入 Suspended List。
20.3 恢复任务
Suspended List
↓
vTaskResume()
↓
Ready List核心:
Resume = 从 Suspended List → Ready List。
20.4 ISR 恢复任务
xTaskResumeFromISR()
↓
恢复任务
↓
是否需要抢占?
↓
return pdTRUE / pdFALSE
↓
ISR 退出时决定是否切换核心:
ISR API 不负责“直接跳过去运行”,而是返回是否需要在 ISR 退出后进行任务切换。
20.5 删除任务
vTaskDelete()
↓
从调度链表移除
↓
是否删除自己?
┌──┴──┐
是 否
↓ ↓
等待Idle 立即释放
↓
prvDeleteTCB()核心:
删除自己不能马上释放自己的栈,因此由 Idle Task 延迟回收。
二十一、复盘时只看这一张图
最后把这一章压缩成一张“脑内地图”:
【任务创建】
│
xTaskCreate()
│
┌───────────┴───────────┐
↓ ↓
TCB内存 Task Stack
│ │
└───────────┬───────────┘
↓
prvInitialiseNewTask()
│
┌──────────┴──────────┐
↓ ↓
初始化TCB pxPortInitialiseStack()
│
↓
构造第一次运行的CPU现场
│
↓
prvAddNewTaskToReadyList()
│
↓
Ready List
│
┌──────────────────┼─────────────────┐
│ │ │
↓ ↓ ↓
vTaskSuspend() 正常运行 vTaskDelete()
│ │
↓ ↓
Suspended List 删除任务
│ │
│ ┌──────────┴─────────┐
│ ↓ ↓
│ 删除自己 删除别人
│ │ │
│ ↓ ↓
│ WaitingTermination 立即释放
│ │
│ ↓
│ Idle Task
│ │
│ ↓
│ prvDeleteTCB()
│
│
├───────────────┐
↓ ↓
vTaskResume() xTaskResumeFromISR()
│ │
└───────┬───────┘
↓
Ready List
│
↓
调度器选择
│
↓
上下文切换最终记忆口诀
创建:TCB + 栈 + 初始现场 → Ready
挂起:Ready → Suspended
恢复:Suspended → Ready
删除:从调度体系移除 → 释放资源
删除自己:不能立即释放 → Idle Task 回收
ISR 恢复:先恢复任务 → 返回是否需要切换 → ISR 退出后切换
这几个关系搞清楚以后,再去看 vTaskDelay()、队列、信号量、事件组,会发现它们本质上也都是:
任务状态发生变化 → 从一个链表移动到另一个链表 → 必要时触发调度。

