第1章:位图调度核心原理

大家好,欢迎来到《RTOS内核之位图调度算法实现》的第一章。

今天咱们聊点硬核的——位图调度。说白了,就是RTOS里怎么快速找到那个“最该跑”的任务。我做了这么多年嵌入式,见过太多调度器翻车的案例,归根结底都是“找最高优先级任务”这一步慢了。

1.1 基于优先级的抢占式调度

先说说抢占式调度。这个概念其实很简单:每个任务都有个优先级,数字越小优先级越高(或者反过来,看你怎么定义)。系统运行时,调度器时刻盯着当前所有就绪任务,一旦有更高优先级的任务出现,立马切换过去。

举个例子:你正在写一个低优先级的后台任务,突然来了个紧急的中断,唤醒了一个高优先级任务。这时候调度器会怎么做?它会毫不犹豫地把当前任务挂起,让高优先级任务先跑。这就是“抢占”的含义。

我在项目中遇到过一个问题:有个同事把两个任务的优先级设反了,结果低优先级的任务一直抢不到CPU,系统直接“假死”。嗯,这里要注意——优先级设计一定要谨慎,否则调度器再快也没用。

核心要点:抢占式调度保证高优先级任务总能及时获得CPU,但前提是调度器能快速找到最高优先级的就绪任务。

1.2 位图如何快速找到最高优先级任务

好,问题来了:假设系统里有32个优先级,你怎么知道当前哪个优先级有任务就绪?

最笨的办法:从优先级0开始,一个一个查。如果最高优先级是31,你得查32次。这在实时系统里是致命的——你想想看,每次调度都要花不确定的时间,这还叫“实时”吗?

位图就是来解决这个问题的。它的思路很巧妙:用一个32位的整数(比如叫ready_bitmap),每一位代表一个优先级。如果优先级3有任务就绪,就把第3位置1。

// 假设优先级0最高,优先级31最低
uint32_t ready_bitmap = 0;

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

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

// 现在 ready_bitmap 的二进制是:0b00000000_00000000_00000000_10001000
// 最高优先级是3(因为第3位是1,且比第7位更靠右?等等,这里要看你的定义)

等等,这里有个细节:如果优先级0最高,那么位0就是最高位。但实际编码中,我们通常用“位0表示优先级0”,然后通过查找最低置位位(LSB)来找到最高优先级。

我个人习惯用__builtin_ctz(Count Trailing Zeros)这个内置函数。它能直接返回最低位1的位置,一条指令搞定。

// 找到最高优先级(假设优先级值越小优先级越高)
int highest_prio = __builtin_ctz(ready_bitmap);
// 如果 ready_bitmap = 0b10001000,返回3

你看,原来需要循环32次的操作,现在一条指令就解决了。这就是位图的威力。

小技巧:如果你的编译器不支持__builtin_ctz,可以用查表法或者二分法。但说实话,现在主流编译器都支持,直接用就好。

1.3 时间片轮转与位图的结合

讲完优先级调度,咱们再聊聊时间片轮转。这个机制是为了解决“同优先级任务怎么公平共享CPU”的问题。

我记得有一次做物联网网关,多个传感器采集任务优先级相同。如果没有时间片轮转,第一个任务会一直占着CPU,后面的任务永远没机会跑。这显然不合理。

时间片轮转的思路:每个任务分配一个固定时间片(比如10ms),时间到了就切换到下一个同优先级任务。但这里有个问题——怎么知道当前优先级下有哪些任务?

答案还是位图。我们可以为每个优先级维护一个就绪任务链表,然后用位图标记哪些优先级有任务。调度时:

  1. 用位图找到最高优先级
  2. 从该优先级的任务链表中取出第一个任务
  3. 运行一个时间片后,把任务放回链表尾部
  4. 重复步骤1

这样,位图负责“找优先级”,链表负责“轮转同优先级任务”,两者完美结合。

// 简化的调度循环
void scheduler(void) {
    while (1) {
        // 用位图找到最高优先级
        int prio = __builtin_ctz(ready_bitmap);
        
        // 从该优先级的链表中取出一个任务
        task_t *task = get_next_task(prio);
        
        // 运行任务(一个时间片)
        run_task(task);
        
        // 时间片用完,把任务放回链表尾部
        put_task_back(task);
    }
}
注意:时间片轮转只适用于同优先级任务。如果高优先级任务一直就绪,低优先级任务永远得不到CPU。这就是“优先级反转”的根源之一,后面章节会详细讲。

1.4 位图调度的实际考量

讲到这里,你可能觉得位图调度已经完美了。但实际项目中,有几个坑要注意:

  • 优先级数量限制:32位整数最多支持32个优先级。如果你需要更多,得用64位或者多个位图组合。
  • 位图操作是原子的吗?在多核系统中,多个CPU可能同时修改位图,必须用原子操作或关中断保护。
  • 空位图检查:如果ready_bitmap == 0,说明没有任务就绪,这时候要跑空闲任务。

我曾经在一个项目中,因为忘记检查空位图,导致__builtin_ctz(0)返回了一个未定义值,系统直接跑飞。嗯,从那以后我每次都用if (ready_bitmap == 0) goto idle_task;来兜底。

总结一下:位图调度用O(1)的时间复杂度找到最高优先级任务,配合时间片轮转实现公平调度。它简单、高效、可预测,是RTOS调度器的经典实现。

下一章,咱们会深入代码,手把手实现一个完整的位图调度器。到时候你会看到,这些原理怎么变成实实在在的C代码。

好,今天就到这里。有什么问题,欢迎在评论区交流。