FreeRTOS挂起、恢复、删除:任务控制三板斧
任务状态机我们提过挂起和终止两个状态但没讲怎么用。实际工程里你迟早需要这些操作通信断开了把通信任务暂时停掉省 CPU等连接恢复再启动。某个一次性任务如自检干完就删除自己释放资源。检测到故障把故障任务删掉重启。这三个操作——挂起Suspend、恢复Resume、删除Terminate——就是任务控制的三板斧。它们看起来简单一行 API但用错会引入难以排查的资源泄漏、状态错乱、甚至死锁。这篇把它们的使用场景、API 细节、以及用了之后会发生什么讲清楚重点是删除任务的资源释放隐患。一、挂起 Suspend让任务罚站osThreadSuspend(id); /* 挂起指定任务 */行为调用后目标任务无论处于什么状态运行/就绪/阻塞都被强制移到挂起列表不参与任何调度。它就像被冻结——CPU 再也不给它用它的代码停在原地。可以挂起自己osThreadSuspend(osThreadGetId()); /* 自己挂起自己 */自己挂起自己后调度器立即切到其它任务。常用于任务发现自己不该继续运行主动停下等外部 Resume。典型场景通信断开后挂起通信任务/** * file comm_lifecycle.c * brief 通信任务按连接状态挂起/恢复 * date 2026-07-15 * version V1.0 - 初版创建 */ #include cmsis_os2.h #include stdio.h static osThreadId_t g_comm_handle; static volatile uint8_t g_comm_connected 0U; /** * brief 通信任务连接建立后才干活 * note 通过被外部挂起/恢复来控制启停 */ static void CommTask(void *argument) { (void)argument; for (;;) { if (g_comm_connected) { CommProcess(); /* 收发数据 */ } osDelay(50); } } /** * brief 连接断开时调用挂起通信任务 */ void Comm_OnDisconnect(void) { printf([COMM] disconnected, suspending CommTask\r\n); g_comm_connected 0U; osThreadSuspend(g_comm_handle); } /** * brief 连接恢复时调用恢复通信任务 */ void Comm_OnReconnect(void) { printf([COMM] reconnected, resuming CommTask\r\n); g_comm_connected 1U; osThreadResume(g_comm_handle); }挂起的三个注意点1.挂起一个正在阻塞的任务它的等待状态不会丢失。比如任务在osDelay(1000)挂起后睡 5 秒再 Resume它继续等剩下那 1000ms实际行为取决于实现但 FreeRTOS 保证 Resume 后从挂起前的状态继续。第 12 篇验证过。2.挂起不是暂停执行到一半的代码。任务被挂起的瞬间可能在某条指令中间但上下文寄存器被保存Resume 后精确恢复。对任务自身来说感觉没中断。3.挂起的任务仍占用栈和 TCB。挂起只是不让它运行RAM 不释放。要释放得用 Terminate。二、恢复 Resume唯一的唤醒途径osThreadResume(id); /* 恢复挂起的任务 */行为把任务从挂起列表移回原状态通常是就绪或阻塞。这是唤醒挂起任务的唯一途径——即使任务原本在等的事件如 osDelay 到期发生挂起状态下也不会唤醒它。Resume 在 ISR 里要小心如果从中断里调osThreadResumeCMSIS-RTOS v2 会自动走 ISR 路径这是 v2 的便利。但要注意Resume 一个比当前任务高优先级的挂起任务会立即触发切换。/* 在中断里恢复一个高优先级任务 */ void EXTI0_IRQHandler(void) { osThreadResume(g_urgent_handle); /* 恢复后立即切换到它 */ }三、删除 Terminate最危险的操作osThreadExit(); /* 任务删除自己 */ osThreadTerminate(id); /* 删除指定任务 */删除任务看似简单一行代码但它是最容易引入隐蔽 bug 的操作。因为删除任务时有几样东西要处理隐患 1资源没释放任务删除时FreeRTOS 只回收它自己的栈和 TCB动态创建的前提下且由空闲任务回收。但任务运行期间获取的其它资源不会自动释放/* 危险任务拿了互斥量就被删除互斥量永远不释放 */ static void BadTask(void *argument) { (void)argument; osMutexAcquire(g_mutex, osWaitForever); /* 拿锁 */ DoWork(); /* 忘了 osMutexRelease! */ osThreadExit(); /* 删除自己 → 锁永久持有其它任务死锁 */ }规则删除任务前必须确保它已经释放了所有持有的资源互斥量、队列、动态分配的内存、打开的文件/外设。隐患 2动态删除的资源回收延迟osThreadTerminate只是标记任务为待删除真正的栈和 TCB 回收由空闲任务完成。如果空闲任务得不到运行所有高优先级任务都不让出资源不会被回收heap 会泄漏。/* 危险模式循环创建-删除任务 */ for (;;) { osThreadId_t h osThreadNew(TempTask, NULL, attr); osDelay(100); osThreadTerminate(h); /* 标记删除 */ /* 若空闲任务没机会运行TCB/栈不回收heap 越用越少 */ }规则避免频繁创建-删除任务。需要临时任务时优先用挂起/恢复复用同一个任务而不是删了重建。隐患 3删除别的任务时的时序问题osThreadTerminate(别的任务)时那个任务可能正在执行任意代码持锁、访问硬件、修改全局变量。强制删除会把它半路掐断留下不一致的状态。规则尽量不要从外部删除正在运行的任务。更安全的做法是让任务自己检测该退出了的标志然后自己清理后osThreadExit。/* 安全模式任务自删除先清理 */ static volatile uint8_t g_should_stop 0U; static void WorkerTask(void *argument) { (void)argument; for (;;) { if (g_should_stop) { CleanupResources(); /* 先清理 */ printf([WORK] self-exit after cleanup\r\n); osThreadExit(); /* 再退出 */ } DoWork(); osDelay(10); } } /* 外部想停止它置标志让任务自己退出 */ void Worker_StopGracefully(void) { g_should_stop 1U; /* 不直接 Terminate等任务自己看到标志后清理退出 */ }四、完整生命周期示例把三板斧串起来演示一个任务的完整生命周期创建 → 运行 → 挂起 → 恢复 → 自删除。/** * brief 演示任务能响应挂起/恢复/停止标志 */ static osThreadId_t g_demo_handle; static volatile uint8_t g_demo_stop 0U; static void DemoTask(void *argument) { uint32_t cnt 0; (void)argument; printf([DEMO] born\r\n); for (;;) { if (g_demo_stop) { printf([DEMO] cleaning up...\r\n); /* 这里释放任何持有的资源 */ printf([DEMO] self-delete, ran %lu times\r\n, (unsigned long)cnt); osThreadExit(); } cnt; printf([DEMO] run #%lu\r\n, (unsigned long)cnt); osDelay(500); } } /** * brief 控制者演示对 DemoTask 的挂起/恢复/停止 */ static void ControllerTask(void *argument) { (void)argument; osDelay(2000); /* 让 Demo 跑 2 秒 */ printf([CTRL] suspend\r\n); osThreadSuspend(g_demo_handle); osDelay(2000); /* 挂起 2 秒Demo 不打印 */ printf([CTRL] resume\r\n); osThreadResume(g_demo_handle); osDelay(2000); /* 恢复后跑 2 秒 */ printf([CTRL] request stop\r\n); g_demo_stop 1U; /* 让 Demo 自己清理退出 */ osDelay(2000); printf([CTRL] Demo state%d (4Terminated)\r\n, (int)osThreadGetState(g_demo_handle)); for (;;) { osDelay(5000); } }串口结果[DEMO] born [DEMO] run #1 [DEMO] run #2 [DEMO] run #3 [DEMO] run #4 [CTRL] suspend [CTRL] resume [DEMO] run #5 [DEMO] run #6 [DEMO] run #7 [CTRL] request stop [DEMO] cleaning up... [DEMO] self-delete, ran 7 times [CTRL] Demo state4 (4Terminated)串口设置USART1115200-8-N-1。观察点1. Demo 出生后每 500ms 打印一次 run2.[CTRL] suspend后 2 秒内 Demo没有打印被冻结3.[CTRL] resume后 Demo 从 run #5 继续计数没丢次数cnt 是任务局部变量挂起期间保留4.[CTRL] request stop后 Demo 看到g_demo_stop先清理再osThreadExit5. 最后查状态 4Terminated确认已删除。这个例子展示了优雅停止的正确姿势外部置标志任务自己清理后退出而不是粗暴 Terminate。五、删除任务后句柄还能用吗osThreadTerminate(g_demo_handle); /* 此时 g_demo_handle 还指向那个已被删除的 TCB */ osThreadGetState(g_demo_handle); /* 返回什么 */任务被删除后句柄变成无效TCB 可能已被空闲任务回收并复用。继续用这个句柄•osThreadGetState可能返回osThreadTerminated或osThreadError• 用它做参数的其它 API 行为未定义——如果那个 TCB 内存已被重新分配给新任务你可能误操作了新任务规则删除任务后立即把句柄置空别再用osThreadTerminate(g_demo_handle); g_demo_handle NULL; /* 置空防止误用 */并在使用前判空if (g_demo_handle ! NULL) { osThreadResume(g_demo_handle); }六、一个实战建议少删除多挂起综合所有隐患给一个工程建议能用挂起/恢复解决的就不要删除。删除任务的隐患太多资源泄漏、回收延迟、时序问题而挂起/恢复是可逆操作资源始终占用但不释放安全得多。典型设计任务在系统初始化时全部创建好运行期间通过挂起/恢复控制启停几乎不做删除。只在以下情况才删除• 真正的一次性任务如开机自检干完不再需要• 严重的故障恢复需要销毁重建某个子系统。这就是为什么很多成熟产品的 RTOS 工程任务数量从启动到关机始终恒定——不创建、不删除靠状态机和挂起/恢复管理生命周期。七、本篇 API 速查功能CMSIS-RTOS v2原生 FreeRTOS说明挂起任务osThreadSuspend(id)vTaskSuspend(handle)可挂起自己或别人恢复任务osThreadResume(id)vTaskResume(handle)唤醒挂起任务的唯一途径ISR 内恢复osThreadResume(id)v2 自动判 ISRxTaskResumeFromISR(handle)v2 统一接口删除自己osThreadExit()vTaskDelete(NULL)任务内调用删除指定任务osThreadTerminate(id)vTaskDelete(handle)资源由空闲任务回收查状态osThreadGetState(id)eTaskGetState(handle)删除后返回 Terminated/Error任务控制的安全清单操作安全做法停止任务优先置标志让任务自退出而非外部 Terminate删除前确保已释放所有锁、内存、外设删除后句柄立即置 NULL不再使用频繁启停用挂起/恢复不要创建/删除循环动态删除后保证空闲任务有机会运行否则资源不回收总结1. 挂起Suspend让任务冻结不参与调度恢复Resume是唯一唤醒途径即使在等的事件发生也不行。2. 挂起的任务资源不释放栈/TCB 占着要释放得删除。3. 删除任务三大隐患资源未释放锁/内存泄漏、动态删除回收延迟依赖空闲任务、外部删除时序问题半路掐断留不一致状态。4.优雅停止 外部置标志 任务自清理 osThreadExit比外部 Terminate 安全。5. 删除后句柄立即置空别再用TCB 可能被复用误操作新任务。6. 工程建议少删除多挂起任务启动时全建好靠挂起/恢复管理生命周期。