2. 任务的概念:任务状态机详解
好,咱们接着聊。上一章我们把任务创建和删除的API过了一遍,但说实话,如果你不理解任务在系统里到底是怎么“活”着的,那写出来的代码很容易出问题。今天我就带大家深入看看任务的状态机——说白了,就是任务在FreeRTOS里到底有几种“活法”。
2.1 任务的四种状态
FreeRTOS里的任务,不是一直在跑的。它会在几个状态之间跳来跳去。我刚开始学的时候,总觉得任务要么在跑,要么没跑,其实没那么简单。
任务一共有四种状态:
- 运行态(Running)——任务正在占用CPU,代码正在执行。
- 就绪态(Ready)——任务啥都准备好了,就等CPU空下来。说白了就是“我能跑,但还没轮到我”。
- 阻塞态(Blocked)——任务在等某个事件。比如等延时结束、等信号量、等队列消息。这时候CPU不会给它。
- 挂起态(Suspended)——任务被强行“冷冻”了。调用vTaskSuspend()就会进这个状态。除非有人调用vTaskResume(),否则它永远不会被调度。
核心要点:同一时刻,只有一个任务能处于运行态(单核CPU)。其他任务要么在等CPU,要么在等事件,要么被挂起。
2.2 状态切换的“套路”
任务不会无缘无故变状态。我给大家画个简单的逻辑:
- 从就绪→运行:调度器选中了你,给你CPU。
- 从运行→就绪:时间片到了,或者有更高优先级的任务抢走了CPU。
- 从运行→阻塞:你调用了vTaskDelay(),或者等一个还没到的信号量。
- 从阻塞→就绪:你等的事件到了,比如延时结束、信号量来了。
- 从运行→挂起:你调用了vTaskSuspend(NULL)把自己挂起,或者别人挂了你。
- 从挂起→就绪:有人调用了vTaskResume()把你解冻了。
嗯,这里要注意:阻塞态不能直接跳到挂起态。必须先变成就绪态,再被挂起。这个细节我踩过坑,后面会讲。
2.3 任务控制块(TCB)结构分析
每个任务在FreeRTOS里都有一个“身份证”——任务控制块(Task Control Block,简称TCB)。说白了,它就是内核用来管理任务的一个数据结构。你创建任务时,内核会偷偷给你分配一个TCB。
TCB里都存了啥?我挑几个关键的说说:
| 字段 | 作用 |
|---|---|
pxTopOfStack |
栈顶指针。任务切换时,CPU的寄存器值就存在栈里。 |
uxPriority |
任务优先级。数值越大优先级越高。 |
pxStack |
指向任务栈的起始地址。 |
uxTCBNumber |
任务编号,调试时很有用。 |
eTaskState |
当前任务状态(运行、就绪、阻塞、挂起)。 |
xEventListItem |
用于将任务挂到事件链表上(比如等待队列)。 |
xStateListItem |
用于将任务挂到状态链表上(就绪列表、阻塞列表等)。 |
你看,TCB里最重要的就是栈指针和优先级。栈指针决定了任务切换时现场怎么恢复,优先级决定了调度器该先让谁跑。
我的小技巧:调试时,我习惯把uxTCBNumber打印出来。这样能快速知道当前跑的是哪个任务,尤其是在多任务切换频繁的时候。
2.4 状态机在实际项目中的应用
我记得有一次做智能家居项目,有个传感器采集任务,需要每100ms采集一次数据。我当时直接写了个死循环,里面放vTaskDelay(100)。结果发现任务有时候会“卡死”——其实不是卡死,是它被更高优先级的任务抢占了,但自己不知道。
后来我仔细看了状态切换逻辑才发现:vTaskDelay()让任务进入阻塞态,但阻塞时间到了之后,它只是进入就绪态,不是直接运行。如果此时有更高优先级的任务在跑,它就得等着。这个细节,很多人一开始都会忽略。
避坑指南:我曾经在项目里犯过一个低级错误——在中断服务函数里直接调用了vTaskSuspend()挂起一个正在阻塞的任务。结果呢?任务状态乱了,系统直接崩溃。记住:不要在中断里直接操作任务状态,要用API的FromISR版本。
2.5 代码示例:查看任务状态
FreeRTOS提供了一个API叫uxTaskGetTaskState(),可以查看任务当前的状态。返回值是eTaskState枚举类型:
// 获取任务状态
eTaskState state = uxTaskGetTaskState(xTaskHandle);
switch(state) {
case eRunning:
printf("任务正在运行\n");
break;
case eReady:
printf("任务就绪\n");
break;
case eBlocked:
printf("任务阻塞\n");
break;
case eSuspended:
printf("任务挂起\n");
break;
default:
printf("未知状态\n");
break;
}
这个API在调试时特别有用。我一般会在系统运行一段时间后,打印所有任务的状态,看看有没有任务“卡”在某个状态出不来。
2.6 小结
任务状态机是FreeRTOS调度的核心。你想想看,如果连任务在哪个状态都不清楚,那写出来的调度逻辑肯定漏洞百出。我个人建议:每创建一个新任务,先想清楚它会在哪些状态之间切换,会不会出现死锁或优先级反转。
下一章,我会带大家深入任务优先级和调度策略。到时候你会明白,为什么有时候任务明明就绪了,却迟迟不运行——嗯,那才是真正考验你功底的地方。