一、优先级与就绪队列:任务优先级的概念

各位同学,咱们今天聊聊RTOS里最核心的一个概念——优先级。说白了,优先级就是给每个任务贴个标签,告诉调度器:“这个任务比较急,先处理它”。

我在做第一个RTOS项目时,就吃过没搞懂优先级的亏。当时有个通信任务和显示任务,我把显示优先级设高了,结果通信数据老丢包。后来才发现,通信任务才是真正的“急性子”。

1.1 什么是任务优先级

任务优先级,就是一个整数。数值越小,优先级越高。比如优先级0的任务,比优先级1的任务更“尊贵”。

你想想看,一个系统里可能有几十个任务。有的任务要响应按键,有的要刷新屏幕,有的要处理网络数据。如果大家都平等,那谁先谁后?这时候优先级就派上用场了。

核心原则:高优先级任务优先获得CPU使用权。只有高优先级任务阻塞或完成后,低优先级任务才能运行。

1.2 优先级的分层设计

我习惯把优先级分成三个层次:

  • 实时优先级(0-15):给中断处理、紧急通信这类任务。响应时间必须毫秒级。
  • 普通优先级(16-31):给常规业务逻辑。比如数据处理、状态机。
  • 后台优先级(32-63):给空闲任务、日志打印这类不着急的活。

嗯,这里要注意:优先级数量不是越多越好。我记得有个项目,设计了128级优先级,结果调度器光比较优先级就花了不少时间。后来我建议砍到32级,性能反而上去了。

二、就绪队列的数据结构设计

就绪队列,就是存放“准备好运行”的任务的地方。每个任务一旦就绪,就会被放进这个队列里。

2.1 传统链表方案

最简单的做法是用链表。每个优先级对应一个链表,任务就绪了就挂到对应链表上。

// 链表节点
typedef struct tcb_node {
    struct tcb *task;        // 任务控制块指针
    struct tcb_node *next;   // 下一个节点
} tcb_node_t;

// 就绪队列
tcb_node_t *ready_list[32];  // 32个优先级,每个一个链表

这个方案有什么问题?我踩过坑。每次调度时,要从优先级0开始遍历,找到第一个非空链表。最坏情况下要遍历32次。如果系统有64个优先级,那就更慢了。

避坑指南:我曾经在一个项目里用链表管理64个优先级,结果调度延迟达到几十微秒。对于实时系统来说,这简直是灾难。

2.2 位图方案的诞生

那有没有更快的方法?有,就是位图。位图本质上是一个整数,每个bit代表一个优先级。bit为1表示该优先级上有就绪任务。

// 32位位图,每个bit对应一个优先级
uint32_t ready_bitmap = 0;

// 优先级3有任务就绪
ready_bitmap |= (1 << 3);

// 优先级7有任务就绪  
ready_bitmap |= (1 << 7);

// 查找最高优先级:找到最低位的1
// 比如 ready_bitmap = 0x88 (10001000)
// 最高优先级是 bit3,值为8

你想想看,用位图查找最高优先级,只需要一条指令。而链表可能要循环几十次。这就是为什么我选择位图。

三、为什么选择位图来管理就绪队列

3.1 性能对比

方案 查找最高优先级 插入任务 删除任务
链表 O(n),最坏32次 O(1) O(1)
位图 O(1),1次指令 O(1) O(1)

看到没?位图在查找最高优先级时,是常数时间。不管系统有多少优先级,都是一条指令搞定。

3.2 位图的具体实现

我常用的位图实现是这样的:

// 就绪位图
typedef struct {
    uint32_t bitmap;          // 32位位图
    tcb_t *tasks[32];         // 每个优先级对应的任务链表头
} ready_queue_t;

// 任务就绪
void ready_task(tcb_t *tcb, uint8_t priority) {
    // 设置位图
    ready_queue.bitmap |= (1 << priority);
    
    // 将任务插入对应优先级的链表
    list_add_tail(&ready_queue.tasks[priority], &tcb->node);
}

// 获取最高优先级任务
tcb_t* get_highest_ready_task(void) {
    // 查找最低位的1
    uint8_t highest_prio = __builtin_ctz(ready_queue.bitmap);
    
    // 从对应链表取出第一个任务
    return list_first_entry(&ready_queue.tasks[highest_prio], tcb_t, node);
}

小技巧:__builtin_ctz 是GCC内置函数,用于计算尾部0的个数。如果不用GCC,可以自己写查表法。我一般用256字节的查表,速度也很快。

3.3 实际项目中的经验

我记得有个项目,需要同时管理64个优先级。用位图的话,需要两个32位整数:

uint32_t bitmap_low;   // 优先级0-31
uint32_t bitmap_high;  // 优先级32-63

// 查找时先看高位,再看低位
if (bitmap_high) {
    prio = 32 + __builtin_ctz(bitmap_high);
} else {
    prio = __builtin_ctz(bitmap_low);
}

说白了,位图方案就是空间换时间。多花几个字节,换来确定性的调度延迟。对于实时系统来说,这个交换很值。

3.4 位图的局限性

当然,位图也不是万能的。我遇到过一个问题:如果某个优先级上有多个任务,位图只能告诉我们“这个优先级有任务”,但不知道具体是哪个。所以还需要配合链表来管理同优先级的任务。

嗯,这里要注意:同优先级的任务一般采用时间片轮转调度。每个任务运行一个时间片,然后切换到下一个。这个我们后面会详细讲。

总结一下:位图管理就绪队列,核心优势就是快。O(1)的查找时间,让调度器能在微秒级别做出决策。对于硬实时系统来说,这是必须的。

好了,这一章就到这里。下一章我们讲位图的具体实现,包括如何用查表法实现__builtin_ctz,以及如何处理优先级反转问题。这些都是我在实际项目中踩过的坑,希望能帮到大家。