真正开始调试多任务系统,会发现状态机比想象的复杂:一个任务为什么"明明调了 osDelay 却显示 Ready"?为什么查另一个任务永远查不到 Running?删除一个任务后它去哪了?同一时刻到底有几个任务在"运行"?
这一篇把任务状态的完整转换图、每个状态的进入/退出条件、以及"如何用代码亲眼看到状态变化"讲透。理解了它,你调试多任务系统时就不会再对着诡异的现象抓瞎。
一、一张图看懂任务的一生
先把完整的状态转换图画出来,这是本篇的核心:
四个"活"状态加一个"终止"状态:
状态 | 含义 | 是否参与调度 |
|---|---|---|
| 就绪 Ready | 等待被调度器选中运行 | 是 |
| 运行 Running | 正在占用 CPU | 是(且唯一) |
| 阻塞 Blocked | 在等某个事件(延时/队列/信号量) | 否 |
| 挂起 Suspended | 被人为暂停,不参与任何调度 | 否 |
| 终止 Terminated | 已被删除,资源待回收 | 否 |
两个关键认知:
1.同一时刻只有一个任务在 Running(单核 CPU)。所有其它任务要么就绪、要么阻塞、要么挂起。
2.阻塞和挂起都不参与调度,但进入原因不同:阻塞是"在等事件"(会自动醒来),挂起是"被人强制暂停"(只能被人手动恢复)。
二、四种状态的进入与退出
逐个讲清楚每个状态怎么进、怎么出。
1. 就绪 Ready
怎么进入:
• 任务刚创建(
osThreadNew);• 从阻塞恢复(osDelay 到期、队列收到数据、信号量被释放);
• 从挂起恢复(
osThreadResume);• 从运行被切走(被高优先级抢占、时间片到)。
怎么退出:
• 被调度器选中,进入运行;
• 又调了阻塞 API,进入阻塞;
• 被挂起。
就绪态的任务,调度器在每个调度点(tick 中断、阻塞 API 调用等)会重新挑选。
2. 运行 Running
怎么进入:调度器从就绪列表里选中(按优先级最高原则)。
怎么退出:
• 调了阻塞 API(osDelay/osMessageQueueGet 等)→ 阻塞;
• 被挂起(
osThreadSuspend)→ 挂起;• 被高优先级任务抢占 → 回到就绪;
• 同优先级时间片到 → 回到就绪;
• 自己退出/被删除 → 终止。
重要:Running 态的任务全局唯一。任何时刻你用osThreadGetState查任意一个"自己以外"的任务,都不可能查到 Running(因为查询动作本身在调用者上下文执行,被查者不可能同时在运行)。
3. 阻塞 Blocked
怎么进入:调用任何带阻塞/超时的 RTOS API:
•
osDelay(ticks)/osDelayUntil(tick);•
osMessageQueueGet(id, ..., timeout)带非零超时;•
osSemaphoreAcquire(id, timeout);•
osMutexAcquire(id, timeout);•
osEventFlagsWait(...);•
osThreadFlagsWait(...)。
怎么退出:
• 等待的事件发生(延时到期、队列收到数据等)→ 就绪;
• 超时(设置了 timeout 且到时间)→ 就绪(API 返回超时错误码);
• 被挂起 → 挂起(少见,但允许)。
阻塞是 RTOS 的灵魂——它让任务"睡觉时不占 CPU",是 osDelay 优于 HAL_Delay 的根本原因(第 06 篇)。
阻塞 ≠ 挂起。阻塞是任务自己"主动等事件",会自动醒来;挂起是"被别人强制暂停",不会自动醒来。这是新手最容易混淆的两个状态。
4. 挂起 Suspended
怎么进入:调用osThreadSuspend(id)。可以挂起别的任务,也可以挂起自己(osThreadSuspend(osThreadGetId()))。
怎么退出:只能调用osThreadResume(id)。没有其它途径——即使它原本在阻塞等某个事件,挂起后那个事件发生也唤不醒它,必须先 Resume。
挂起用的场景:某个任务暂时不需要运行(比如通信断开后挂起通信任务,省 CPU),需要时再恢复。第 16 篇专讲。
5. 终止 Terminated
怎么进入:
• 任务函数自己调
osThreadExit();• 被外部调
osThreadTerminate(id)。
怎么退出:不能。终止是终态。
资源回收:动态创建的任务(osThreadNew),终止后其栈和 TCB 由空闲任务回收(前提是configUSE_PREEMPTION等配置正常)。所以终止后不要立即查它的状态,可能已经被回收。
三、用代码亲眼看到状态切换
口说无凭,我们写代码实时查询并打印一个任务的状态变化。
/** * @file state_observer.c * @brief 实时观察目标任务的状态切换 * @date 2026-07-15 * @version V1.0 - 初版创建 */ #include "cmsis_os2.h" #include <stdio.h> static osThreadId_t g_worker_handle; /* 把状态枚举转成可读字符串 */ static const char *StateToStr(osThreadState_t st) { switch (st) { case osThreadInactive: return "Inactive"; case osThreadReady: return "Ready"; case osThreadRunning: return "Running"; case osThreadBlocked: return "Blocked"; case osThreadTerminated: return "Terminated"; case osThreadError: return "Error"; default: return "?"; } } /** * @brief 被观察的工作任务:500ms 周期循环 * @note 大部分时间在 osDelay 阻塞,所以查它通常显示 Blocked */ static void WorkerTask(void *argument) { (void)argument; for (;;) { printf("[WORK] running, tick=%lu\r\n", (unsigned long)osKernelGetTickCount()); osDelay(500); } } /** * @brief 观察者任务:每 100ms 查一次 WorkerTask 的状态 * @note 频率高于 Worker 的周期,能捕捉到不同时刻的状态 */ static void ObserverTask(void *argument) { (void)argument; for (;;) { osThreadState_t st = osThreadGetState(g_worker_handle); printf("[OBS] Worker = %s\r\n", StateToStr(st)); osDelay(100); } } void App_StartStateObserver(void) { osThreadAttr_t attr = {0}; attr.name = "Worker"; attr.stack_size = 256; attr.priority = osPriorityNormal; g_worker_handle = osThreadNew(WorkerTask, NULL, &attr); attr.name = "Observer"; attr.stack_size = 256; attr.priority = osPriorityNormal; /* 同优先级,时间片轮转 */ osThreadNew(ObserverTask, NULL, &attr); }串口结果
[WORK] running, tick=1 [OBS] Worker = Blocked [OBS] Worker = Blocked [OBS] Worker = Blocked [OBS] Worker = Blocked [WORK] running, tick=501 [OBS] Worker = Blocked [OBS] Worker = Blocked ...串口设置:USART1,115200-8-N-1。观察点:
1. Worker 每 500ms 打印一次"running"(它真正运行的那一瞬间);
2. Observer 每 100ms 查一次,绝大多数时候看到的是
Blocked——因为 Worker 90% 时间在 osDelay 睡觉;3.几乎看不到
Ready或Running——因为查的那一刻,要么 Worker 在睡觉(Blocked),要么正在打印(但打印的是 Worker 自己,不是被查的对象)。
这个结果印证了第 05 篇提过的现象:任务大部分时间在阻塞。这也是为什么多个任务能"并发"——它们轮流睡觉,CPU 在它们之间快速切换。
四、捕捉 Ready 态:需要精确的时机
上面的实验很难看到 Ready,因为 Worker 阻塞时间太长。我们设计一个更刁钻的实验:让两个同优先级任务都频繁地"运行一小会儿、阻塞一小会儿",提高捕获概率。
/** * @brief 高频交替任务 A:运行 1ms、阻塞 9ms */ static void FastTaskA(void *argument) { (void)argument; for (;;) { volatile uint32_t i; for (i = 0; i < 5000U; i++) { } /* 忙约 1ms(模拟工作) */ osDelay(9); } } /** * @brief 观察者:查 A 的状态 */ static void FastObserver(void *argument) { (void)argument; for (;;) { osThreadState_t st = osThreadGetState(g_task_a_handle); if (st != osThreadBlocked) /* 只打印非阻塞的,过滤掉大量 Blocked */ { printf("[OBS] A = %s @ tick %lu\r\n", StateToStr(st), (unsigned long)osKernelGetTickCount()); } osDelay(1); } }串口结果(节选)
[OBS] A = Ready @ tick 12 [OBS] A = Ready @ tick 23 [OBS] A = Ready @ tick 34串口设置:USART1,115200-8-N-1。观察点:偶尔能捕获到
Ready——这是 A 刚忙完、刚调 osDelay 让出、又或者刚好被时间片切走、正等着下次被调度的瞬间。
能捕获到 Ready 说明:任务从"运行结束"到"再次被选中运行"之间,会经历 Ready 态。这个窗口很短,所以需要高频采样才看得到。
五、观察挂起与恢复
挂起是一个"干净"的状态,容易演示:
/** * @brief 演示挂起/恢复对状态的影响 */ static void SuspendDemoTask(void *argument) { (void)argument; for (;;) { printf("[DEMO] alive @ tick %lu\r\n", (unsigned long)osKernelGetTickCount()); osDelay(500); } } /** * @brief 控制者:挂起 DEMO,观察状态,再恢复 */ static void ControllerTask(void *argument) { (void)argument; for (;;) { osDelay(2000); printf("[CTRL] suspending DEMO...\r\n"); osThreadSuspend(g_demo_handle); printf("[CTRL] DEMO state = %s\r\n", StateToStr(osThreadGetState(g_demo_handle))); osDelay(2000); /* 挂起期间 DEMO 不再打印 alive */ printf("[CTRL] resuming DEMO...\r\n"); osThreadResume(g_demo_handle); printf("[CTRL] DEMO state = %s\r\n", StateToStr(osThreadGetState(g_demo_handle))); } }串口结果
[DEMO] alive @ tick 500 [DEMO] alive @ tick 1000 [DEMO] alive @ tick 1500 [CTRL] suspending DEMO... [CTRL] DEMO state = Suspended [CTRL] resuming DEMO... [CTRL] DEMO state = Blocked [DEMO] alive @ tick 4500 [DEMO] alive @ tick 5000串口设置:USART1,115200-8-N-1。观察点:
1. 挂起前,DEMO 每 500ms 打印一次;
2.
[CTRL] suspending后,DEMO 状态变Suspended,接下来 2 秒 DEMO 不再打印——它被冻结了;3.
[CTRL] resuming后,DEMO 状态变回Blocked(注意是 Blocked 不是 Ready!因为它被挂起时正在 osDelay,恢复后继续那个 osDelay),然后恢复周期打印。
关键细节:恢复后状态显示Blocked而不是Ready。因为 DEMO 被挂起的瞬间正在 osDelay(阻塞态),挂起只是叠加了一层"冻结",恢复后它回到原本的阻塞状态,继续等 osDelay 到期。这解释了"挂起不会丢失任务原本的等待状态"。
六、一个容易误解的点:osDelay 期间到底算什么状态
有人以为 osDelay 期间任务是"挂起",错。osDelay 期间是阻塞。两者的区别要刻进脑子:
阻塞(osDelay 等) | 挂起(osThreadSuspend) | |
|---|---|---|
谁触发 | 任务自己调阻塞 API | 外部/自己调 Suspend |
怎么醒来 | 事件发生自动醒(延时到期等) | 只能外部调 Resume |
丢失等待吗 | 不丢失 | 恢复后继续之前的等待 |
是正常的吗 | 是,RTOS 的正常机制 | 异常控制手段 |
记住一句口诀:阻塞是"我在等",挂起是"我被罚站"。
七、状态查询 API 与调试技巧
/* 查指定任务状态 */ osThreadState_t osThreadGetState(osThreadId_t thread_id); /* 查自己状态(唯一能稳定查到 Running 的方式) */ osThreadState_t my_state = osThreadGetState(osThreadGetId()); /* 列出所有任务(需 configUSE_TRACE_FACILITY = 1) */ /* 原生 API:vTaskList(buf),打印所有任务名/状态/优先级/栈水线 */调试技巧:用 vTaskList 一次看全部
原生 FreeRTOS 的vTaskList能把所有任务的状态打成一张表,调试多任务问题时极其有用:
/** * @brief 打印所有任务的运行状态表 * @note 需在 FreeRTOSConfig.h 开 configUSE_TRACE_FACILITY = 1 * 输出格式:任务名 状态 优先级 栈水线 任务序号 * 状态字符:X=Running R=Ready B=Blocked S=Suspended D=Deleted */ void Debug_PrintAllTasks(void) { char buf[512]; vTaskList(buf); printf("=== Task List ===\r\n%s\r\n", buf); }输出示例:
=== Task List === Name State Prio Stack Num MonitorTask B 24 120 1 WorkerTask B 24 184 2 IDLE R 0 48 3 LedTask B 24 158 4状态字符:
B=Blocked,R=Ready,X=Running(只有一个),S=Suspended。一次就能看到所有任务此刻在哪个状态,排查"谁卡住了"时这是第一利器。
串口设置:USART1,115200-8-N-1。
vTaskList输出对齐格式取决于 FreeRTOS 版本,以实际为准。
八、本篇 API 速查
功能 | CMSIS-RTOS v2 | 原生 FreeRTOS |
|---|---|---|
查任务状态 | osThreadGetState(id) | eTaskGetState(handle) |
获取自己句柄 | osThreadGetId() | xTaskGetCurrentTaskHandle() |
列出所有任务 | (无直接 v2 封装) | vTaskList(buf) |
挂起任务 | osThreadSuspend(id) | vTaskSuspend(handle) |
恢复任务 | osThreadResume(id) | vTaskResume(handle) |
退出当前任务 | osThreadExit() | vTaskDelete(NULL) |
终止指定任务 | osThreadTerminate(id) | vTaskDelete(handle) |
osThreadState_t 枚举:
枚举值 | 含义 | 对应 vTaskList 字符 |
|---|---|---|
osThreadReady | 就绪 | R |
osThreadRunning | 运行 | X |
osThreadBlocked | 阻塞 | B |
osThreadSuspended | 挂起 | S |
osThreadTerminated | 终止 | D |
osThreadInactive | 未激活/未创建 | — |
总结
1. 任务有四个"活"状态(就绪/运行/阻塞/挂起)加一个终态(终止),转换路径明确。
2.同一时刻只有一个任务在 Running——查别人查不到 Running 是正常的,不是 bug。
3. 阻塞是"我在等事件会自动醒",挂起是"我被强制暂停只能被手动恢复"——两者本质不同。
4. 实验证明:任务绝大部分时间在 Blocked,这就是多任务能"并发"的原因。
5. 用
vTaskList一次打印所有任务状态,是排查"谁卡住了"的第一利器(需开configUSE_TRACE_FACILITY)。6. 恢复一个挂起任务,它回到的是被挂起前的状态(通常是 Blocked 继续等之前的事件)。