3、任务优先级管理:优先级范围、优先级映射表、优先级反转问题

好,咱们接着聊任务调度。前面讲了任务怎么创建、怎么切换,但有个核心问题一直没细说——优先级。说白了,优先级就是任务的「插队权」。你想想看,一个系统里几十个任务同时抢CPU,谁先跑谁后跑,总得有个规矩吧?这个规矩就是优先级。

我个人习惯把优先级管理比作「医院急诊分诊」。高优先级任务就像危重病人,必须优先处理。低优先级任务就像普通感冒,等一等没关系。但这里有个坑——如果分诊台搞错了优先级,或者病人之间互相「卡脖子」,那整个系统就乱套了。嗯,这就是咱们今天要聊的重点。

3.1 优先级范围:不是越大越好

鸿蒙OS的优先级范围是多少?我直接告诉你:0~31,共32级。其中0是最高优先级,31是最低优先级。这个设计跟很多RTOS类似,比如FreeRTOS也是0~31,但注意——鸿蒙的0是最高,而有些系统是0最低。千万别搞混了。

我在项目中遇到过一个问题:有个同事把优先级设成了32,结果任务直接不跑了。为什么?因为优先级范围是0~31,32已经越界了。系统会把它当作非法参数,创建任务直接失败。所以记住:优先级范围是硬约束,不是建议

核心原则:优先级数量不是越多越好。32级对于绝大多数嵌入式场景已经足够。优先级太多反而会增加调度器的查找开销,拖慢系统响应。

那怎么分配优先级呢?我一般这样分:

  • 0~3:留给中断处理、时间关键型任务(比如电机控制、刹车系统)
  • 4~15:普通实时任务(比如传感器数据采集、通信协议栈)
  • 16~31:后台任务、非实时任务(比如日志记录、界面刷新)

当然,这只是我的习惯。具体怎么分,得看你的业务场景。但有一条铁律:高优先级任务必须短小精悍。你想想看,如果一个高优先级任务跑了几百毫秒,那低优先级任务就永远没机会了。这叫「优先级饥饿」,是实时系统的大忌。

3.2 优先级映射表:调度器的「作弊器」

好,现在问题来了:调度器怎么知道当前哪个优先级有任务就绪?难道每次调度都遍历32个优先级?那太慢了。鸿蒙OS用了一个经典技巧——优先级映射表

说白了,这就是一个位图(bitmap)。32个优先级对应32个bit位。哪个优先级有任务就绪,就把对应的bit置1。调度器只需要查一下位图里最低的1在哪,就能立刻找到最高优先级的就绪任务。

举个例子:

// 优先级映射表(简化版)
uint32_t g_prio_bitmap;  // 32位,每位对应一个优先级

// 当优先级5有任务就绪时
g_prio_bitmap |= (1 << 5);

// 调度器查找最高优先级(最低的1)
int highest_prio = __builtin_ctz(g_prio_bitmap);  // 返回5

这个__builtin_ctz是GCC内置函数,用来计算尾部0的个数。比如位图值是0b0000000000100000,尾部有5个0,所以最高优先级就是5。这个操作是O(1)的,不管有多少个优先级,查找时间都一样。

我的经验:如果你自己写RTOS,优先级映射表一定要用位图实现。我曾经见过有人用链表存就绪任务,每次调度都要遍历链表,那效率简直惨不忍睹。位图查找是常数时间,链表是线性时间,差了一个数量级。

鸿蒙OS的实现更复杂一些,它用了两级位图来支持更多优先级。但核心思想是一样的——用空间换时间。你想想看,一个32位的变量才4个字节,换来的是每次调度节省几十微秒,这笔买卖太划算了。

3.3 优先级反转:一个经典的坑

好,前面都是基础。接下来这个才是真正的「拦路虎」——优先级反转。我在面试嵌入式工程师时,几乎必问这个问题。为什么?因为它在实际项目中太容易踩坑了。

什么叫优先级反转?简单说就是:高优先级任务被低优先级任务「拖后腿」了。具体场景是这样的:

  1. 任务A(高优先级)和任务C(低优先级)共享一个互斥锁
  2. 任务C先拿到了锁,然后被任务B(中优先级)抢占了CPU
  3. 任务A想拿锁,但锁在C手里,C又被B抢占了,所以A只能等B跑完
  4. 结果:最高优先级的A,反而被中优先级的B「卡脖子」了

你看,优先级反转的本质是:资源竞争导致优先级关系「倒挂」。高优先级任务因为等锁,实际执行优先级被拉低了。

我曾经踩过的坑:在一个无人机飞控项目中,我用了简单的互斥锁保护传感器数据。结果发现,只要GPS任务(高优先级)和日志任务(低优先级)同时访问数据,GPS就会莫名其妙地延迟几十毫秒。查了两天才发现是优先级反转。从那以后,我所有共享资源的保护都改用「优先级继承」协议了。

3.4 解决方案:优先级继承与优先级天花板

怎么解决优先级反转?鸿蒙OS提供了两种主流方案:

3.4.1 优先级继承

这个方案很巧妙:当高优先级任务被低优先级任务阻塞时,低优先级任务临时「继承」高优先级。还是刚才的例子:

  • 任务C持有锁,任务A在等锁
  • 系统检测到A被C阻塞,就把C的优先级临时提升到A的级别
  • 这样C就能继续运行,不会被B抢占了
  • C释放锁后,优先级恢复原样

说白了,就是让「挡路」的任务临时获得高优先级,赶紧把路让开。这个方案的好处是实现简单,坏处是可能发生「链式继承」——如果多个任务嵌套等待,继承链会变得复杂。

3.4.2 优先级天花板

这个方案更「暴力」:给每个互斥锁设定一个「天花板优先级」,任何任务拿到锁后,优先级自动提升到天花板。比如:

// 创建互斥锁时设定天花板优先级
osMutexAttr_t attr = {
    .attr_bits = osMutexPrioCeiling,
    .ceiling_priority = 10  // 天花板优先级
};
osMutexId_t mutex = osMutexNew(&attr);

这样,不管谁拿到锁,优先级都变成10。低优先级任务拿到锁后,不会被中优先级任务抢占,高优先级任务等锁的时间也大大缩短。

我个人更推荐优先级天花板。为什么?因为它简单粗暴,不容易出bug。优先级继承虽然更「优雅」,但实现复杂,容易引入死锁。在嵌入式系统里,简单可靠比花哨更重要。

避坑指南:我曾经在一个项目中同时用了优先级继承和嵌套锁,结果出现了「优先级反转链」——三个任务互相继承优先级,最后调度器直接崩溃了。后来我改用优先级天花板,问题立刻解决。所以我的建议是:能用天花板就别用继承,能不用嵌套锁就别用

3.5 实战建议:优先级管理的「黄金法则」

好,讲了这么多,最后总结几条我自己的实战经验:

  1. 优先级分配要「留白」:不要把32级全用满,留几级给未来扩展。我一般只用0~20,21~31留着备用。
  2. 高优先级任务要「短平快」:优先级越高,代码执行时间越短。如果高优先级任务要跑很久,考虑拆分成多个小任务。
  3. 共享资源一定要用「优先级保护」:不管是优先级继承还是天花板,总得选一个。裸用互斥锁就是给自己挖坑。
  4. 用工具检测优先级反转:鸿蒙OS的Trace工具可以记录任务调度历史。如果发现高优先级任务频繁被阻塞,赶紧查是不是反转了。

嗯,优先级管理这块就聊到这儿。说白了,优先级就是一把双刃剑——用好了系统响应飞快,用不好就是各种玄学bug。记住:优先级不是越高越好,合适才是王道。下一章咱们聊聊任务同步与通信,那又是另一个大坑了。