第四章 任务管理与调度:让系统真正“多任务”起来
任务调度,说白了就是操作系统决定“谁先跑、谁后跑、谁歇会儿”的规则。很多刚入行的朋友觉得调度就是配个优先级完事,其实坑多着呢。我见过一个产品,就因为优先级反转没处理好,整个系统卡死,最后只能按复位键——那场面,真叫一个尴尬。
这一章,咱们把任务创建、删除、挂起、恢复,优先级配置,调度策略,还有那个让人头疼的优先级反转与继承,一次性讲透。
4.1 任务的生命周期:创建、删除、挂起、恢复
一个任务从生到死,经历哪些状态?我习惯用一张图来记:
4.1.1 任务创建
创建任务,每个RTOS都有自己的API。以FreeRTOS为例,核心函数就是xTaskCreate()。我习惯把任务栈大小设成4字节对齐,并且留够余量——曾经有个同事栈只给了128字节,结果任务跑起来就栈溢出,查了两天才找到原因。
// FreeRTOS 任务创建示例
TaskHandle_t xTaskHandle = NULL;
void vTaskFunction(void *pvParameters) {
// 任务主体,通常是一个死循环
for (;;) {
// 做点有意义的事
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void vCreateMyTask(void) {
BaseType_t xReturned;
xReturned = xTaskCreate(
vTaskFunction, // 任务函数指针
"MyTask", // 任务名称(调试用)
256, // 栈深度,单位是字(4字节)
NULL, // 参数
2, // 优先级,数值越大优先级越高
&xTaskHandle // 任务句柄,用于后续操作
);
if (xReturned != pdPASS) {
// 创建失败,通常是内存不足
// 我建议这里加个断言或者错误日志
}
}
uxTaskGetStackHighWaterMark()看剩余栈空间,再逐步调小。别一开始就抠门,系统跑飞了更浪费时间。
4.1.2 任务删除
任务删除不是真的“销毁”,而是把任务从调度队列里移除,回收栈和TCB内存。注意:千万别在中断里直接删任务!
// 删除任务
void vDeleteMyTask(void) {
if (xTaskHandle != NULL) {
vTaskDelete(xTaskHandle); // 传入NULL表示删除自己
xTaskHandle = NULL; // 好习惯:置空句柄
}
}
4.1.3 任务挂起与恢复
挂起和阻塞不一样。挂起是“我主动不跑了,谁叫我都别理我”;阻塞是“我在等资源,资源来了我就继续”。
// 挂起任务
vTaskSuspend(xTaskHandle); // 任务进入挂起态
// 恢复任务
vTaskResume(xTaskHandle); // 任务回到就绪态
// 从中断恢复任务(更高效)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskResumeFromISR(xTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
挂起常用于低功耗场景:系统空闲时挂起所有非关键任务,只留一个低功耗管理任务。等需要干活了,再逐个恢复。
4.2 优先级配置:别让“高优先级”变成“高负担”
优先级是调度的核心。配置不对,系统要么响应慢,要么低优先级任务永远饿死。
| 优先级数值 | 含义(FreeRTOS) | 典型用途 |
|---|---|---|
| 0 | 最低优先级(空闲任务) | 系统空闲钩子、背景统计 |
| 1~3 | 低优先级 | 后台处理、日志记录、非实时显示 |
| 4~6 | 中优先级 | 普通控制逻辑、通信协议栈 |
| 7~9 | 高优先级 | 传感器采集、快速响应控制 |
| 10~configMAX_PRIORITIES-1 | 最高优先级 | 硬实时任务、中断辅助任务 |
我个人习惯:优先级尽量少用,够用就行。一个系统里优先级超过5级,调度逻辑就会变得复杂,调试也费劲。你想想看,10个优先级,每个任务都觉得自己很重要,结果就是高优先级任务把CPU吃满,低优先级任务永远轮不到。
4.3 调度策略:抢占式 vs 时间片轮转
调度策略就两种主流:抢占式和时间片轮转。很多RTOS两者都支持,可以混用。
4.3.1 抢占式调度
抢占式,说白了就是“高优先级任务一来,低优先级任务立马让位”。这是实时系统的基石。
// FreeRTOS 配置抢占式调度(默认就是)
// FreeRTOSConfig.h
#define configUSE_PREEMPTION 1 // 1=抢占式,0=合作式
// 典型场景:高优先级任务就绪,立即抢占
void vHighPriorityTask(void *pvParameters) {
for (;;) {
// 处理紧急事件
// 处理完后,可以主动让出CPU
taskYIELD();
}
}
抢占式的优点:实时性好,高优先级任务响应快。缺点:如果高优先级任务一直不阻塞,低优先级任务就永远没机会跑——这就是“优先级饿死”。
4.3.2 时间片轮转
时间片轮转,就是同等优先级的任务轮流跑,每人分一个时间片。时间片到了,自动切换到下一个。
// FreeRTOS 配置时间片轮转
#define configUSE_TIME_SLICING 1 // 1=启用时间片轮转
#define configTICK_RATE_HZ 1000 // 系统时钟节拍,单位Hz
// 时间片长度 = 1 / configTICK_RATE_HZ = 1ms
// 每个同优先级任务跑1ms,然后切换
时间片轮转适合那些“大家差不多重要”的场景。比如多个传感器数据采集任务,优先级相同,轮流采集,谁也不饿着谁。
4.4 优先级反转与继承:一个让系统“假死”的经典问题
优先级反转,是实时系统里最隐蔽、最坑人的问题之一。我当年在一个无人机飞控项目里就遇到过——飞着飞着,电机突然停了,差点炸机。查了三天,最后发现是优先级反转导致高优先级任务拿不到互斥量。
4.4.1 什么是优先级反转?
看个经典场景:三个任务,优先级从高到低:H(高)、M(中)、L(低)。
- L任务拿到互斥量,开始干活。
- H任务就绪,抢占L,但H也需要同一个互斥量——拿不到,H被阻塞。
- M任务就绪,抢占L(因为M优先级比L高),M开始跑。
- M一直跑,L一直没机会释放互斥量,H就一直等。
结果:最高优先级的H,被中等优先级的M“间接”阻塞了。这就是优先级反转。
4.4.2 优先级继承:解决方案
优先级继承的思路很简单:当低优先级任务持有高优先级任务需要的互斥量时,临时把低优先级任务的优先级提升到和高优先级任务一样高。这样,低优先级任务就能尽快跑完、释放互斥量。
// FreeRTOS 使用优先级继承的互斥量
// 注意:不要用普通的二进制信号量,要用互斥量
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
void vHighTask(void *pvParameters) {
for (;;) {
// 请求互斥量
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
// ...
xSemaphoreGive(xMutex);
}
}
}
void vLowTask(void *pvParameters) {
for (;;) {
xSemaphoreTake(xMutex, portMAX_DELAY);
// 低优先级任务持有互斥量时,如果高优先级任务在等,
// 低优先级任务会被临时提升优先级
// 处理完赶紧释放
xSemaphoreGive(xMutex);
}
}
- 二进制信号量:不支持优先级继承,容易导致优先级反转。
- 互斥量:支持优先级继承,能有效避免优先级反转。
所以,保护共享资源时,我永远用互斥量,不用二进制信号量。除非你明确知道自己在做什么。
4.4.3 优先级继承的局限
优先级继承不是万能的。它只能解决“直接”优先级反转,解决不了“链式”优先级反转(多个互斥量嵌套)。链式反转需要更复杂的协议,比如优先级天花板协议。
不过说实话,在大多数嵌入式系统里,只要做到以下几点,优先级反转基本不会出现:
- 尽量少用互斥量,能用队列传递数据就别共享内存。
- 如果必须用互斥量,保持临界区代码极短(几十微秒级别)。
- 避免互斥量嵌套——一个任务不要同时持有多个互斥量。
- 高优先级任务尽量不要等低优先级任务的资源。
4.5 本章小结:调度调得好,系统稳如狗
任务调度看着简单,但坑都在细节里。我总结几个关键点:
- 任务创建:栈大小留余量,用HighWaterMark调优。
- 任务删除:先释放资源,再删任务。
- 挂起/恢复:适合低功耗场景,别和阻塞搞混。
- 优先级:够用就行,别超过5级。
- 调度策略:抢占式保实时,时间片轮转保公平。
- 优先级反转:用互斥量,别用二进制信号量。
嗯,这一章就到这儿。调度是RTOS的灵魂,把调度搞明白了,你的系统就成功了一半。