真正开始调试多任务系统会发现状态机比想象的复杂一个任务为什么明明调了 osDelay 却显示 Ready为什么查另一个任务永远查不到 Running删除一个任务后它去哪了同一时刻到底有几个任务在运行这一篇把任务状态的完整转换图、每个状态的进入/退出条件、以及如何用代码亲眼看到状态变化讲透。理解了它你调试多任务系统时就不会再对着诡异的现象抓瞎。一、一张图看懂任务的一生先把完整的状态转换图画出来这是本篇的核心四个活状态加一个终止状态状态含义是否参与调度就绪 Ready等待被调度器选中运行是运行 Running正在占用 CPU是且唯一阻塞 Blocked在等某个事件延时/队列/信号量否挂起 Suspended被人为暂停不参与任何调度否终止 Terminated已被删除资源待回收否两个关键认知1.同一时刻只有一个任务在 Running单核 CPU。所有其它任务要么就绪、要么阻塞、要么挂起。2.阻塞和挂起都不参与调度但进入原因不同阻塞是在等事件会自动醒来挂起是被人强制暂停只能被人手动恢复。二、四种状态的进入与退出逐个讲清楚每个状态怎么进、怎么出。1. 就绪 Ready怎么进入• 任务刚创建osThreadNew• 从阻塞恢复osDelay 到期、队列收到数据、信号量被释放• 从挂起恢复osThreadResume• 从运行被切走被高优先级抢占、时间片到。怎么退出• 被调度器选中进入运行• 又调了阻塞 API进入阻塞• 被挂起。就绪态的任务调度器在每个调度点tick 中断、阻塞 API 调用等会重新挑选。2. 运行 Running怎么进入调度器从就绪列表里选中按优先级最高原则。怎么退出• 调了阻塞 APIosDelay/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, tick1 [OBS] Worker Blocked [OBS] Worker Blocked [OBS] Worker Blocked [OBS] Worker Blocked [WORK] running, tick501 [OBS] Worker Blocked [OBS] Worker Blocked ...串口设置USART1115200-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串口设置USART1115200-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串口设置USART1115200-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 */ /* 原生 APIvTaskList(buf)打印所有任务名/状态/优先级/栈水线 */调试技巧用 vTaskList 一次看全部原生 FreeRTOS 的vTaskList能把所有任务的状态打成一张表调试多任务问题时极其有用/** * brief 打印所有任务的运行状态表 * note 需在 FreeRTOSConfig.h 开 configUSE_TRACE_FACILITY 1 * 输出格式任务名 状态 优先级 栈水线 任务序号 * 状态字符XRunning RReady BBlocked SSuspended DDeleted */ 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状态字符BBlockedRReadyXRunning只有一个SSuspended。一次就能看到所有任务此刻在哪个状态排查谁卡住了时这是第一利器。串口设置USART1115200-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就绪RosThreadRunning运行XosThreadBlocked阻塞BosThreadSuspended挂起SosThreadTerminated终止DosThreadInactive未激活/未创建—总结1. 任务有四个活状态就绪/运行/阻塞/挂起加一个终态终止转换路径明确。2.同一时刻只有一个任务在 Running——查别人查不到 Running 是正常的不是 bug。3. 阻塞是我在等事件会自动醒挂起是我被强制暂停只能被手动恢复——两者本质不同。4. 实验证明任务绝大部分时间在 Blocked这就是多任务能并发的原因。5. 用vTaskList一次打印所有任务状态是排查谁卡住了的第一利器需开configUSE_TRACE_FACILITY。6. 恢复一个挂起任务它回到的是被挂起前的状态通常是 Blocked 继续等之前的事件。