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调度的核心。你想想看,如果连任务在哪个状态都不清楚,那写出来的调度逻辑肯定漏洞百出。我个人建议:每创建一个新任务,先想清楚它会在哪些状态之间切换,会不会出现死锁或优先级反转

下一章,我会带大家深入任务优先级和调度策略。到时候你会明白,为什么有时候任务明明就绪了,却迟迟不运行——嗯,那才是真正考验你功底的地方。