Linux 调度器演进:从 O(n) 到 BFS

Linux 内核的进程调度器经历了多次重大重构,每一次演进都针对特定硬件时代(单核 → 多核 → 大规模异构)的负载特征进行优化。理解这四代调度器的设计哲学与缺陷,是掌握内核抢占配置与实时性调优的基础。

核心脉络: O(n) 调度器(2.4)→ O(1) 调度器(2.6.0)→ CFS 调度器(2.6.23)→ BFS 调度器(桌面/实时场景)。前三个为主线内核调度器,BFS 为外部维护的替代方案。

2.1 O(n) 调度器(Linux 2.4 ~ 2.6.0)

O(n) 调度器是 Linux 早期使用的 基于优先级的时间片轮转 调度器。其名称来源于调度算法的时间复杂度为 O(n),其中 n 为系统中可运行进程的数量。

核心设计

  • 单一运行队列: 所有可运行进程被放入一个全局链表。
  • 静态优先级 + 时间片: 每个进程分配一个固定时间片,优先级高的进程获得更长的时间片。
  • 每次调度遍历全链表: 调度器每次选择下一个进程时,需要遍历整个运行队列,计算每个进程的“好度(goodness)”,选出最大值。
// O(n) 调度器核心伪代码(简化)
for_each_process_in_runqueue(p) {
    goodness = p->counter + p->priority;
    if (goodness > best_goodness) {
        best = p;
        best_goodness = goodness;
    }
}
switch_to(best);
致命缺陷: 当系统中进程数达到数百个时,每次调度都需要遍历整个链表,调度延迟随进程数线性增长。在服务器高并发场景下,调度开销会严重拖累吞吐量。

O(n) 调度器结构示意

O(n) 调度器:单一全局运行队列 全局运行队列(runqueue) 进程 A 进程 B 进程 C 进程 D … 调度器遍历所有进程计算 goodness 选出 best 进程切换

2.2 O(1) 调度器(Linux 2.6.0 ~ 2.6.22)

为解决 O(n) 调度器的性能瓶颈,Ingo Molnár 在 Linux 2.6.0 中引入了 O(1) 调度器。其核心思想是 用空间换时间,将调度算法的时间复杂度降低为常数 O(1)。

关键改进

  • 多级优先级队列(140 级): 每个优先级对应一个链表,进程按优先级入队。
  • 活跃队列 + 过期队列双结构: 活跃队列中的进程用完时间片后进入过期队列,所有进程时间片用完后交换指针。
  • 位图查找: 使用一个 140 位的 bitmap 标记非空队列,通过 __ffs() 指令在常数时间内找到最高优先级非空队列。
// O(1) 调度器核心:位图查找最高优先级
struct prio_array {
    unsigned long bitmap[BITMAP_SIZE]; // 140位位图
    struct list_head queue[MAX_PRIO];  // 140个队列
};

// 找到最高优先级非空队列(O(1))
idx = __ffs(array->bitmap);  // 返回第一个置位位
next = list_entry(array->queue[idx].next, struct task_struct, run_list);
历史意义: O(1) 调度器在 Linux 2.6 时代统治了超过 3 年,完美支撑了早期多核服务器和桌面系统。但其“交互式进程识别启发式算法”过于复杂,导致在某些负载下出现调度不公平现象。

O(1) 调度器双队列结构

O(1) 调度器:活跃队列 + 过期队列 活跃队列 (active) 优先级 0 优先级 1 优先级 … 优先级 139 过期队列 (expired) 优先级 0 优先级 1 优先级 … 优先级 139 140 位 bitmap:通过 __ffs() 常数时间找到最高优先级非空队列 时间片用完 → 移入过期队列 → 交换 active/expired 指针 优点:调度延迟与进程数无关;缺点:交互式识别启发式复杂,公平性不足

2.3 CFS 调度器(Linux 2.6.23 至今)

CFS(Completely Fair Scheduler,完全公平调度器)由 Con Kolivas 的“楼梯调度器”启发,由 Ingo Molnár 在 Linux 2.6.23 中合入主线。它彻底抛弃了固定时间片和优先级队列,采用 红黑树 + 虚拟运行时间(vruntime) 模型。

核心原理

  • 虚拟运行时间(vruntime): 每个进程的 vruntime 按实际运行时间加权计算(权重由优先级决定)。
  • 红黑树排序: 所有可运行进程按 vruntime 排序,最左侧节点(vruntime 最小)即为下一个被调度进程。
  • 目标: 让所有进程的 vruntime 尽可能相等,实现“完全公平”。
// CFS 核心:vruntime 更新与红黑树选择
// 每次时钟 tick 更新当前进程的 vruntime
curr->vruntime += delta_exec * NICE_0_LOAD / curr->load.weight;

// 选择下一个进程:红黑树最左节点
next = rb_entry(cfs_rq->rb_leftmost, struct sched_entity, run_node);
关键优势: CFS 的调度延迟是 O(log n),但实际中红黑树深度通常不超过 10(即使进程数上千)。更重要的是,CFS 的公平性模型是数学上可证明的,不再依赖启发式算法。

CFS 调度器红黑树结构

CFS 调度器:红黑树按 vruntime 排序 vruntime: 100 vruntime: 80 vruntime: 120 最左节点 vruntime: 60 vruntime: 90 vruntime: 110 vruntime: 130 调度选择:始终选择红黑树最左节点(vruntime 最小) 时间复杂度 O(log n),但实际深度通常 ≤ 10

2.4 BFS 调度器(Con Kolivas,2009)

BFS(Brain Fuck Scheduler)由 Con Kolivas 开发,旨在解决 CFS 在桌面交互场景下的延迟问题。BFS 采用 单链表 + 全局优先级队列,专为 桌面和实时交互 优化。

设计特点

  • 单链表运行队列: 所有进程按优先级排序,插入时保持有序。
  • 虚拟截止时间(Virtual Deadline): 每个进程计算一个“截止时间”,调度器选择截止时间最早的进程。
  • 全局队列(非 per-CPU): 所有 CPU 共享一个运行队列,通过锁保护。
争议与局限: BFS 在桌面低负载(≤16 核)下表现出极低的调度延迟和优秀的交互响应,但在大规模服务器(≥32 核)上,全局锁竞争导致扩展性严重下降。因此 BFS 从未进入 Linux 主线内核,主要用于 Android(早期版本)和桌面优化内核(如 linux-ck)。

四代调度器对比

特性 O(n) 调度器 O(1) 调度器 CFS 调度器 BFS 调度器
引入版本 Linux 2.4 Linux 2.6.0 Linux 2.6.23 外部(2009)
时间复杂度 O(n) O(1) O(log n) O(n)(但 n 通常很小)
数据结构 全局链表 140 级位图 + 双队列 红黑树 有序单链表
公平性模型 时间片轮转 优先级 + 启发式 vruntime 完全公平 虚拟截止时间
多核扩展性 差(全局锁) 好(per-CPU 队列) 优秀(per-CPU + 负载均衡) 差(全局锁)
交互延迟 高 中等 低 极低
适用场景 单核、小负载 早期多核服务器 通用(服务器/桌面/嵌入式) 桌面、实时交互

2.5 调度器演进总结

演进驱动力: 从 O(n) 到 CFS 的演进,本质上是 调度公平性 与 调度延迟 的不断优化。CFS 通过 vruntime 模型在数学上实现了公平性,同时保持了 O(log n) 的可扩展性,成为 Linux 主线调度器的最终选择。BFS 则证明了“针对特定场景的极致优化”在桌面领域仍有价值。