3. 任务优先级与抢占:谁先跑,谁后跑,谁插队

各位做单片机开发的同行,今天我们来聊聊任务调度里最核心、也最容易出幺蛾子的部分——优先级与抢占。说白了,就是决定哪个任务先执行、哪个任务后执行,以及高优先级的任务能不能把正在运行的低优先级任务一脚踢开。

我在项目里见过太多因为优先级没搞明白导致的诡异故障:系统偶尔卡死、关键任务响应延迟、甚至莫名其妙地复位。嗯,这些问题十有八九都跟优先级和抢占机制有关。

3.1 优先级定义:给任务排个座次

优先级,就是给每个任务分配一个数字,用来表示它的“紧急程度”。数字越小优先级越高,还是数字越大优先级越高?这取决于你用的RTOS。我个人习惯用“数值越小优先级越高”的体系,比如FreeRTOS就是这样的。

RTOS 优先级范围 数值与优先级关系
FreeRTOS 0 ~ configMAX_PRIORITIES-1 数值越大,优先级越高
uC/OS-III 0 ~ OS_CFG_PRIO_MAX-1 数值越小,优先级越高
RT-Thread 0 ~ RT_THREAD_PRIORITY_MAX-1 数值越小,优先级越高
注意: 不同RTOS的优先级数值含义可能相反。移植代码时一定要确认清楚,否则你的高优先级任务可能一直在“排队等”。

我曾经接手过一个项目,前任把FreeRTOS的优先级配置搞反了,最高优先级的任务设成了0(最低),结果系统响应慢得像蜗牛。排查了两天才发现这个低级错误……从那以后,我每次新建工程都会在注释里写明优先级规则。

3.2 静态优先级 vs 动态优先级

优先级怎么定?有两种方式:静态和动态。

静态优先级

任务创建时就定死了优先级,运行期间不能改。简单、确定性强,适合大多数嵌入式场景。

// FreeRTOS 静态优先级示例
xTaskCreate(vTask1, "Task1", 128, NULL, 3, NULL);  // 优先级3
xTaskCreate(vTask2, "Task2", 128, NULL, 5, NULL);  // 优先级5(更高)

动态优先级

任务运行过程中可以调整优先级。灵活性高,但增加了系统复杂度和不确定性。

// FreeRTOS 动态修改优先级
vTaskPrioritySet(xTaskHandle, 4);  // 把任务优先级改为4
我的建议: 除非有明确需求(比如优先级继承、紧急任务临时提升优先级),否则优先使用静态优先级。动态优先级容易引入难以复现的时序问题。

你想想看,动态优先级就像在高速公路上突然给一辆车“加塞权”,其他车(任务)的运行时间全被打乱了。调试起来非常头疼。

3.3 抢占式调度 vs 非抢占式调度

这是调度策略的核心区别。我画了一张图,帮你快速理解两者的差异。

抢占式 vs 非抢占式调度对比 非抢占式调度 低优先级任务运行中 高优先级任务就绪(等待) 低优先级任务主动让出CPU 高优先级任务开始运行 特点:任务主动让出CPU,高优先级任务必须等待 抢占式调度 低优先级任务运行中 高优先级任务就绪(立即抢占) 高优先级任务抢占CPU 低优先级任务被挂起 特点:高优先级任务就绪时立即抢占CPU

非抢占式调度(协作式)

任务自己决定什么时候让出CPU。要么任务执行完毕,要么主动调用延时或等待函数。高优先级任务即使就绪了,也得等当前任务“发善心”。

  • 优点: 共享资源不需要互斥保护,不会出现优先级反转
  • 缺点: 一个任务死循环,整个系统就挂了
  • 适用场景: 任务简单、可预测性强的系统

抢占式调度

高优先级任务就绪时,立即打断当前运行的低优先级任务。这是现代RTOS的主流方式。

  • 优点: 实时性好,高优先级任务响应快
  • 缺点: 需要处理临界区保护、优先级反转等问题
  • 适用场景: 对实时性要求高的系统
核心区别: 非抢占式是“你让我跑我才跑”,抢占式是“我想跑我就跑”。大多数嵌入式实时系统都使用抢占式调度。

3.4 优先级反转问题:一个经典坑

优先级反转,说白了就是高优先级任务被低优先级任务“拖住”了,中间还夹着一个中优先级任务捣乱。这是抢占式调度里最经典的坑,没有之一。

问题场景

假设有三个任务:Task_H(高优先级)、Task_M(中优先级)、Task_L(低优先级)。它们共享一个互斥信号量保护的资源。

  1. Task_L 获得信号量,开始访问共享资源
  2. Task_H 就绪,抢占 Task_L,但发现信号量被占,进入阻塞等待
  3. Task_L 恢复运行,但此时 Task_M 就绪,抢占 Task_L
  4. Task_M 运行,Task_H 还在等 Task_L 释放信号量
  5. 结果:高优先级的 Task_H 被中优先级的 Task_M 间接阻塞了
严重后果: 优先级反转可能导致高优先级任务错过截止时间,在实时系统中这是致命的。我见过一个无人机飞控因为优先级反转导致姿态控制任务延迟,差点炸机。

解决方案

主要有三种方法:

方法 原理 优缺点
优先级继承 低优先级任务临时提升到高优先级任务的优先级 实现简单,但可能引入死锁
优先级天花板 访问共享资源的任务优先级提升到最高可能优先级 实现更简单,但可能过度提升优先级
禁止中断/临界区 访问共享资源时关中断 简单粗暴,但影响中断响应
// FreeRTOS 使用互斥信号量(自带优先级继承)
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();

void Task_L(void *pvParameters) {
    xSemaphoreTake(xMutex, portMAX_DELAY);  // 获得互斥量
    // 访问共享资源
    xSemaphoreGive(xMutex);  // 释放互斥量
}
避坑指南: 我曾经在一个项目中用了二值信号量保护共享资源,结果出现了严重的优先级反转。后来换成互斥信号量(带优先级继承),问题立刻解决。记住:保护共享资源时,优先使用互斥信号量而不是二值信号量。

3.5 优先级设计实战建议

说了这么多理论,最后给几条我在项目中总结的优先级设计原则:

  • 关键任务优先级最高: 比如安全相关的任务、控制环路任务
  • 中断服务任务次之: 需要快速响应外设事件的任务
  • 周期性任务居中: 数据采集、状态更新等
  • 后台任务最低: 日志记录、统计等非实时任务
  • 避免优先级数量过多: 一般5-8个优先级就够用了,太多反而增加调度开销
记住: 优先级设计不是越复杂越好。简单、清晰、可预测的优先级体系,比花里胡哨的动态调整方案更可靠。调试的时候你就知道这句话的分量了。