第1章:任务优先级与调度
各位同学,咱们今天聊点实在的。任务优先级与调度,说白了就是RTOS的「交通指挥系统」。你想想看,一个系统里跑着好几个任务,谁先跑、谁后跑、谁跑多久,总得有个规矩吧?
我个人习惯把调度器比作一个「急性子的交警」。它时刻盯着每个任务的优先级,一旦有更高优先级的任务就绪了,立马让当前任务靠边站。嗯,这里要注意——这个「靠边站」的过程,就是我们常说的任务切换。
1.1 优先级抢占规则
先说说最核心的规则:高优先级任务就绪时,立即抢占低优先级任务的CPU使用权。
我在项目中遇到过这样一个场景:一个电机控制任务跑在优先级5,一个通信任务跑在优先级3。电机控制需要每1ms执行一次,通信任务偶尔发个数据包。按理说没问题,对吧?但有一次,通信任务突然来了个大包,处理时间长了点,电机控制就被延迟了——结果电机出现了明显的抖动。
为什么会这样?因为通信任务的优先级虽然低,但它一旦开始执行,如果电机控制任务还没就绪,CPU就一直被通信任务占着。等电机控制任务就绪时,通信任务还没跑完——但注意,这里电机控制任务的优先级更高,所以它会立即抢占通信任务。
代码层面怎么体现?看个简单的FreeRTOS示例:
// 创建两个任务
xTaskCreate(motor_control_task, "Motor", 256, NULL, 5, NULL); // 优先级5
xTaskCreate(comm_task, "Comm", 256, NULL, 3, NULL); // 优先级3
void motor_control_task(void *pvParameters) {
while(1) {
// 电机控制代码
vTaskDelay(pdMS_TO_TICKS(1)); // 每1ms执行一次
}
}
void comm_task(void *pvParameters) {
while(1) {
// 通信处理
vTaskDelay(pdMS_TO_TICKS(10)); // 每10ms执行一次
}
}
你看,motor_control_task优先级更高,它每次就绪时,comm_task都得乖乖让路。这就是抢占规则。
1.2 时间片轮转
那如果两个任务优先级相同呢?这时候就轮到时间片轮转上场了。
说白了,就是每个任务轮流跑一小段时间。这个「一小段时间」就是时间片,通常几个毫秒到几十个毫秒不等。时间片到了,调度器就强制切换给下一个同优先级的任务。
我记得有一次调试一个多轴电机同步的项目,三个电机控制任务优先级都设成了5。结果发现,其中一个电机总是慢半拍。查了半天,原来是时间片设置得太短,任务切换太频繁,导致每个电机控制任务都没跑完一个完整的控制周期就被切走了。
时间片轮转的配置,以FreeRTOS为例:
// 在FreeRTOSConfig.h中配置
#define configUSE_TIME_SLICING 1 // 开启时间片轮转
#define configTICK_RATE_HZ 1000 // 系统时钟节拍,1ms一个tick
// 时间片长度 = configTICK_RATE_HZ / 同优先级任务数
// 如果只有一个同优先级任务,时间片就是整个CPU时间
1.3 优先级反转问题
好,接下来这个坑,我敢说每个做RTOS的人都会踩到——优先级反转。
什么叫优先级反转?简单说:一个高优先级任务,被一个低优先级任务「拖住」了,导致它迟迟无法执行。
场景是这样的:
- 任务A(优先级高)和任务C(优先级低)共享一个资源(比如一个全局变量)
- 任务B(优先级中等)不共享这个资源
- 任务C先拿到了资源,然后被任务B抢占了
- 任务A就绪了,想拿资源,但资源被任务C占着
- 任务C又被任务B抢占了,所以任务A只能等任务B跑完,任务C才能继续跑,跑完才能释放资源
你看,任务A优先级最高,却要等任务B跑完——这就是反转。
优先级反转的后果很严重。轻则任务延迟,重则系统崩溃。那怎么解决?
1.4 优先级继承机制
解决方案就是优先级继承。说白了,就是当高优先级任务等待低优先级任务释放资源时,低优先级任务临时「继承」高优先级任务的优先级,这样它就不会被中等优先级的任务抢占了。
还是刚才那个例子:任务C占着资源,任务A在等。这时候任务C的优先级临时提升到和任务A一样高。任务B(中等优先级)就抢不了任务C了。任务C赶紧跑完,释放资源,然后优先级恢复原样。任务A拿到资源,继续执行。
代码层面,使用互斥量(Mutex)而不是二值信号量,就能自动启用优先级继承:
// 使用互斥量,自动支持优先级继承
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
void high_priority_task(void *pvParameters) {
while(1) {
// 等待互斥量
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
// ...
xSemaphoreGive(xMutex);
}
}
}
void low_priority_task(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
// ...
xSemaphoreGive(xMutex);
}
}
}
不过要注意,优先级继承不是万能的。它只能解决「直接反转」的问题,对于「链式反转」(多个任务层层等待)效果有限。更极端的情况,可以考虑使用优先级天花板协议——直接把所有可能访问共享资源的任务的优先级都设成一样高。但这样会降低系统的实时性,属于「以空间换时间」的思路。
小结
今天咱们聊了四个核心概念:
- 优先级抢占:高优先级任务就绪,立即抢CPU
- 时间片轮转:同优先级任务轮流跑,时间片别太短
- 优先级反转:高优先级被低优先级拖住,是个大坑
- 优先级继承:用互斥量,让低优先级临时「升级」
下节课,咱们会深入讲任务状态与切换,看看任务到底是怎么「挂起」「就绪」「运行」的。到时候我会分享一个我调试过的「任务莫名其妙卡死」的案例,保证让你印象深刻。
好,今天就到这儿。有问题随时交流。
公众号:蓝海资料掘金营,微信deep3321