4. 任务优先级:静态优先级与动态优先级、优先级位图算法

好,咱们今天聊一个RTOS里绕不开的话题——任务优先级。

你想想看,一个系统里同时跑着好几个任务,谁先执行谁后执行?总得有个规矩。这个规矩,就是优先级。说白了,优先级就是给每个任务贴个标签,告诉调度器:“我比较急,先让我跑。”

但这里有个问题——这个标签是贴死的,还是可以撕下来换的?这就引出了我们今天要讲的两个概念:静态优先级动态优先级

4.1 静态优先级:简单粗暴,但不够灵活

静态优先级,顾名思义,就是任务创建的时候就把优先级定死了,运行期间不能改。我记得我最早接触μC/OS-II的时候,用的就是这种方案。

它的好处很明显:

  • 实现简单——调度器不用操心优先级变化,每次就找最高优先级的任务跑
  • 确定性高——高优先级任务一定能抢占低优先级任务,响应时间可预测
  • 资源开销小——不需要额外的机制来调整优先级

但缺点也很致命:

  • 不够灵活——任务优先级一旦定下来,遇到特殊情况没法调整
  • 容易饿死低优先级任务——高优先级任务一直就绪,低优先级任务永远没机会跑
  • 优先级反转问题——这个后面章节会细讲,静态优先级下这个问题特别突出

核心要点:静态优先级适合任务数量固定、优先级关系明确的系统。比如一个简单的传感器采集系统,采集任务优先级最高,通信任务次之,显示任务最低——这种场景用静态优先级完全够用。

4.2 动态优先级:灵活但复杂

动态优先级就不一样了。任务的优先级可以在运行过程中改变。我做过一个项目,需要处理多种类型的网络数据包,有的包紧急但量小,有的包不紧急但量大——这时候静态优先级就搞不定了。

动态优先级常见的策略有:

  • 优先级继承——低优先级任务持有高优先级任务需要的资源时,临时提升低优先级任务的优先级
  • 优先级天花板——任务访问共享资源时,优先级提升到该资源对应的天花板优先级
  • 时间片轮转+优先级衰减——任务执行时间越长,优先级逐渐降低,防止一个任务霸占CPU

动态优先级的优势:

  • 适应性强——能根据系统状态动态调整,应对突发情况
  • 公平性好——低优先级任务也有机会得到CPU
  • 能解决优先级反转——通过优先级继承等机制

但代价也不小:

  • 实现复杂——需要额外的数据结构和算法来管理优先级变化
  • 开销大——每次调度都要重新计算优先级
  • 确定性降低——优先级变化导致任务响应时间难以精确预测
特性 静态优先级 动态优先级
实现复杂度
灵活性
确定性
资源开销
适用场景 任务固定、实时性要求高 任务多变、需要公平调度

4.3 优先级位图算法:调度器的“杀手锏”

好,优先级定下来了,调度器怎么快速找到最高优先级的就绪任务?

你可能会说:“遍历一遍就绪列表不就行了?”

嗯,如果只有几个任务,遍历确实没问题。但如果有64个、128个甚至256个任务呢?每次调度都遍历一遍,CPU时间全花在找任务上了。

这时候,优先级位图算法就派上用场了。

我在项目中用过这个算法,说实话,第一次看到它的实现时,我拍了一下大腿——这设计太巧妙了!

4.3.1 基本原理

优先级位图的核心思想是:用一个位(bit)来表示一个优先级是否有就绪任务。比如,优先级0对应bit0,优先级1对应bit1,以此类推。某个优先级上有就绪任务,就把对应的bit置1。

举个例子,假设系统支持32个优先级(0~31),我们可以用一个32位的整数来表示:

// 优先级位图
uint32_t ready_bitmap;

// 设置优先级p的就绪状态
ready_bitmap |= (1 << p);

// 清除优先级p的就绪状态
ready_bitmap &= ~(1 << p);

// 查找最高优先级(即最低的置1位)
// 这里用到了__builtin_ctz——Count Trailing Zeros
int highest_prio = __builtin_ctz(ready_bitmap);

你看,查找最高优先级的操作,从O(n)变成了O(1)——一条指令就搞定了!

小提示:不同的编译器提供的查找最低置1位的函数名可能不同。GCC用__builtin_ctz,ARM编译器用__clz,Visual Studio用_BitScanForward。移植的时候要注意。

4.3.2 多级位图——支持更多优先级

如果系统需要支持256个优先级呢?一个32位整数就不够用了。这时候可以用多级位图。

我记得FreeRTOS就用了这种方案。它把256个优先级分成8组,每组32个。先查哪一组有就绪任务,再查该组中哪个优先级有任务。

// 两级位图
uint32_t group_bitmap;   // 8个组,每组对应1位
uint32_t ready_bitmap[8]; // 每组32个优先级

// 设置优先级p的就绪状态
int group = p >> 5;      // p / 32
int bit = p & 0x1F;      // p % 32
ready_bitmap[group] |= (1 << bit);
group_bitmap |= (1 << group);

// 查找最高优先级
int highest_group = __builtin_ctz(group_bitmap);
int highest_bit = __builtin_ctz(ready_bitmap[highest_group]);
int highest_prio = (highest_group << 5) + highest_bit;

这个算法的时间复杂度仍然是O(1)——两次位运算就搞定了。

4.3.3 实际应用中的注意事项

我在一个项目中用过三级位图,支持1024个优先级。当时遇到一个坑——位图变量没有初始化。系统刚启动时,位图里全是随机值,调度器一查,以为有高优先级任务就绪,直接跑飞了。

警告:位图变量一定要初始化为0!这个错误我曾经犯过,排查了整整两天。另外,多任务环境下,对位图的操作必须是原子的,否则会出现竞态条件。

还有一点要注意:位图算法虽然查找快,但维护起来需要小心。比如,一个优先级上有多个任务时,不能简单地清除位图——要等该优先级上所有任务都变成非就绪状态后,才能清除对应的位。

通常的做法是:每个优先级维护一个任务计数。计数为0时,才清除位图中的对应位。

// 每个优先级的任务计数
uint8_t prio_count[32];

// 任务变为就绪
void task_ready(int prio) {
    if (prio_count[prio] == 0) {
        ready_bitmap |= (1 << prio);
    }
    prio_count[prio]++;
}

// 任务变为非就绪
void task_not_ready(int prio) {
    prio_count[prio]--;
    if (prio_count[prio] == 0) {
        ready_bitmap &= ~(1 << prio);
    }
}

4.4 如何选择?

嗯,讲到这里,你可能会问:“那我到底该用静态还是动态?位图算法是不是必须的?”

我的建议是:

  • 小系统(任务数 < 16)——静态优先级就够了,遍历查找也不慢
  • 中等系统(16~64个任务)——静态优先级+位图算法,性能和复杂度平衡得很好
  • 大系统(64个任务以上)——考虑动态优先级,但一定要用位图算法,否则调度开销会拖垮系统

我个人习惯是:能用静态优先级解决的问题,绝不用动态的。因为动态优先级带来的复杂度,往往比它解决的问题还要多。但如果你遇到优先级反转、任务饿死这些问题,动态优先级就是不得不用的武器了。

至于位图算法——不管系统大小,我都建议用。它带来的性能提升,值得那几行代码的代价。