任务基础:任务的概念、任务状态与优先级

好,咱们正式开始聊任务。说实话,任务这个概念是FreeRTOS的灵魂。你想想看,一个嵌入式系统里跑着好几个功能——读传感器、刷屏幕、处理按键、发数据——如果全塞在一个大循环里,那代码写起来得多痛苦?任务就是来解决这个问题的。

任务到底是什么?

任务,说白了就是一个独立的执行流。每个任务都有自己的栈空间、自己的优先级、自己的运行节奏。我习惯把任务想象成一个“独立的小程序”,它们共享CPU时间,但互不干扰。

在FreeRTOS里,任务是用C函数实现的。一个典型的任务函数长这样:

void vTaskFunction(void *pvParameters)
{
    // 任务初始化
    for(;;)
    {
        // 任务主体代码
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

注意看,任务函数不能返回。它必须是一个死循环。为什么?因为任务一旦返回,就相当于这个执行流结束了。FreeRTOS会把它清理掉。嗯,这里要注意——如果你不小心让任务函数返回了,系统可能会崩溃。我在项目中遇到过新手同事犯这个错,排查了半天。

任务状态:四种状态切换

任务不是一直运行的。它会在不同状态之间切换。FreeRTOS定义了四种任务状态:运行态就绪态阻塞态挂起态

我刚开始学的时候,觉得这四个状态有点绕。后来我画了个状态转换图,一下子就清楚了。咱们一个一个说。

运行态(Running)

正在使用CPU的任务,就是运行态。单核CPU上,同一时刻只能有一个任务处于运行态。说白了,CPU一次只能干一件事。

就绪态(Ready)

任务已经准备好了,随时可以运行。但CPU正被别的任务占着,所以它只能等着。就绪态的任务都在就绪列表里排队,调度器会挑一个优先级最高的让它跑。

阻塞态(Blocked)

任务在等某个事件。比如等延时结束、等信号量、等队列消息。阻塞态的任务不参与调度,CPU不会浪费时间在它身上。我个人觉得,阻塞态是FreeRTOS最巧妙的设计——它让CPU能高效利用,不会空转等待。

挂起态(Suspended)

任务被主动暂停了。调用vTaskSuspend()就能挂起一个任务。挂起态的任务,调度器完全忽略它。只有调用vTaskResume()才能让它恢复。

这四种状态的切换关系,我整理了一个表格:

当前状态 触发条件 下一状态
运行态 调度器切换 就绪态
运行态 调用阻塞函数(如vTaskDelay) 阻塞态
运行态 调用vTaskSuspend 挂起态
就绪态 被调度器选中 运行态
阻塞态 等待的事件发生 就绪态
挂起态 调用vTaskResume 就绪态

核心要点:只有就绪态和运行态的任务才会被调度器考虑。阻塞态和挂起态的任务,调度器直接跳过。这能大大减少调度开销。

任务优先级:谁先跑?

优先级决定了任务在就绪队列里的位置。数值越大,优先级越高。FreeRTOS支持0到configMAX_PRIORITIES-1的优先级范围。

举个例子:

// 创建两个任务,优先级不同
xTaskCreate(vHighPriorityTask, "High", 1000, NULL, 3, NULL);
xTaskCreate(vLowPriorityTask,  "Low",  1000, NULL, 1, NULL);

高优先级任务(优先级3)和低优先级任务(优先级1)同时就绪时,高优先级的会先运行。只有高优先级任务阻塞或挂起后,低优先级任务才有机会。

这里有个坑——优先级反转。我曾经在一个项目中遇到过:一个低优先级任务占着共享资源,高优先级任务等着用,结果中优先级任务插进来抢了CPU,高优先级任务反而被卡住了。嗯,这个问题后来用优先级继承机制解决了。后面讲互斥量的时候咱们再细聊。

我的建议:优先级别设太多。一般3-5个优先级就够用了。优先级太多,调度器判断起来也费劲,反而影响实时性。

任务创建与删除

创建任务用xTaskCreate()。它的参数不少,我列一下:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度(单位是字,不是字节)
    void *pvParameters,             // 传给任务的参数
    UBaseType_t uxPriority,         // 优先级
    TaskHandle_t *pxCreatedTask     // 任务句柄(可选)
);

删除任务用vTaskDelete()。注意,删除任务时要确保它不再使用任何资源。我习惯在任务退出前手动释放所有动态分配的内存。

警告:千万不要在中断服务函数里调用vTaskDelete()!FreeRTOS的API函数,很多都不能在中断里调用。具体哪些能用,看官方文档的“FromISR”后缀。

任务栈:别小看它

每个任务都有自己的栈。栈的大小在创建任务时指定。栈太小,任务运行时会栈溢出;栈太大,浪费内存。

怎么估算栈大小?我一般这样操作:先给一个较大的值(比如512字),跑一遍任务,然后用uxTaskGetStackHighWaterMark()查看剩余栈空间。根据结果再调整。这样比较稳妥。

// 获取任务栈的剩余空间
UBaseType_t uxHighWaterMark;
uxHighWaterMark = uxTaskGetStackHighWaterMark(xTaskHandle);
printf("剩余栈空间: %u 字\n", uxHighWaterMark);

嗯,这个函数返回的是任务运行以来栈使用的最小剩余量。数值越大,说明栈越安全。

总结一下

任务的概念其实不复杂。记住三点:

  • 任务是独立的执行流,用函数实现,不能返回
  • 四种状态:运行、就绪、阻塞、挂起。状态切换由调度器管理
  • 优先级决定谁先跑,但要注意优先级反转的问题

下一章咱们会深入调度器的实现细节。到时候你会看到,FreeRTOS是怎么用链表管理这些任务的。说实话,那部分代码写得相当漂亮,值得好好研究。