拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FreeRTOS任务的一生:就绪/运行/阻塞/挂起的完整旅程

FreeRTOS任务的一生:就绪/运行/阻塞/挂起的完整旅程

真正开始调试多任务系统,会发现状态机比想象的复杂:一个任务为什么"明明调了 osDelay 却显示 Ready"?为什么查另一个任务永远查不到 Running?删除一个任务后它去哪了?同一时刻到底有几个任务在"运行"?

这一篇把任务状态的完整转换图、每个状态的进入/退出条件、以及"如何用代码亲眼看到状态变化"讲透。理解了它,你调试多任务系统时就不会再对着诡异的现象抓瞎。


一、一张图看懂任务的一生

先把完整的状态转换图画出来,这是本篇的核心:

四个"活"状态加一个"终止"状态:

状态

含义

是否参与调度

就绪 Ready

等待被调度器选中运行

是

运行 Running

正在占用 CPU

是(且唯一)

阻塞 Blocked

在等某个事件(延时/队列/信号量)

否

挂起 Suspended

被人为暂停,不参与任何调度

否

终止 Terminated

已被删除,资源待回收

否

两个关键认知:

  1. 1.同一时刻只有一个任务在 Running(单核 CPU)。所有其它任务要么就绪、要么阻塞、要么挂起。

  2. 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. 1. Worker 每 500ms 打印一次"running"(它真正运行的那一瞬间);

  2. 2. Observer 每 100ms 查一次,绝大多数时候看到的是Blocked——因为 Worker 90% 时间在 osDelay 睡觉;

  3. 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. 1. 挂起前,DEMO 每 500ms 打印一次;

  2. 2.[CTRL] suspending后,DEMO 状态变Suspended,接下来 2 秒 DEMO 不再打印——它被冻结了;

  3. 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. 1. 任务有四个"活"状态(就绪/运行/阻塞/挂起)加一个终态(终止),转换路径明确。

  2. 2.同一时刻只有一个任务在 Running——查别人查不到 Running 是正常的,不是 bug。

  3. 3. 阻塞是"我在等事件会自动醒",挂起是"我被强制暂停只能被手动恢复"——两者本质不同。

  4. 4. 实验证明:任务绝大部分时间在 Blocked,这就是多任务能"并发"的原因。

  5. 5. 用vTaskList一次打印所有任务状态,是排查"谁卡住了"的第一利器(需开configUSE_TRACE_FACILITY)。

  6. 6. 恢复一个挂起任务,它回到的是被挂起前的状态(通常是 Blocked 继续等之前的事件)。

返回列表