任务状态机,我们提过"挂起"和"终止"两个状态,但没讲怎么用。实际工程里,你迟早需要这些操作:
通信断开了,把通信任务暂时停掉,省 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 里要小心
如果从中断里调osThreadResume,CMSIS-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 (4=Terminated)\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 state=4 (4=Terminated)串口设置:USART1,115200-8-N-1。观察点:
1. Demo 出生后每 500ms 打印一次 run;
2.
[CTRL] suspend后 2 秒内 Demo没有打印(被冻结);3.
[CTRL] resume后 Demo 从 run #5 继续计数(没丢次数,cnt 是任务局部变量,挂起期间保留);4.
[CTRL] request stop后 Demo 看到g_demo_stop,先清理再osThreadExit;5. 最后查状态 = 4(Terminated),确认已删除。
这个例子展示了"优雅停止"的正确姿势:外部置标志,任务自己清理后退出,而不是粗暴 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 自动判 ISR) | xTaskResumeFromISR(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. 工程建议:少删除多挂起,任务启动时全建好,靠挂起/恢复管理生命周期。