任务基础:任务的概念、任务状态与优先级
好,咱们正式开始聊任务。说实话,任务这个概念是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是怎么用链表管理这些任务的。说实话,那部分代码写得相当漂亮,值得好好研究。