一、优先级与就绪队列:任务优先级的概念
各位同学,咱们今天聊聊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,以及如何处理优先级反转问题。这些都是我在实际项目中踩过的坑,希望能帮到大家。