2. Linux 调度器演进:从 O(n) 到 EEVDF

Linux 内核的进程调度器经历了多次重大重构,每一次演进都旨在解决特定负载场景下的性能瓶颈与公平性问题。从早期的 O(n) 调度器,到广为人知的 O(1) 与 CFS,再到近年来引入的 EEVDF,调度算法的演进史就是一部操作系统对“吞吐、响应、公平”三者平衡的探索史。

核心演进脉络: O(n)(低效遍历) → O(1)(固定时间片) → CFS(红黑树 + 虚拟时间) → BFS(单核优化) → EEVDF(延迟边界 + 虚拟截止时间)。

2.1 O(n) 调度器:简单但低效

O(n) 调度器是 Linux 2.4 及早期版本使用的调度算法。其核心数据结构是一个全局的进程链表,每次调度时,调度器需要遍历整个链表来查找“最佳”候选进程。

  • 调度复杂度: O(n),其中 n 为系统中进程总数。进程数越多,调度开销越大。
  • 时间片分配: 基于静态优先级,所有进程共用一个时间片池,缺乏动态调整机制。
  • 多核支持: 极差。全局链表导致严重的锁竞争,无法有效利用 SMP 架构。
  • 交互性: 对 I/O 密集型进程不友好,容易导致桌面响应迟钝。
致命缺陷: 当系统中存在数百个进程时,调度器每次选择下一个进程都需要遍历整个链表,导致调度延迟随进程数线性增长,这在服务器场景下是不可接受的。
O(n) 调度器:全局链表遍历 进程 A 进程 B 进程 C 进程 D 进程 E 当前选中 调度器必须遍历所有节点才能找到优先级最高的进程

2.2 O(1) 调度器:里程碑式的改进

Linux 2.6.0 引入的 O(1) 调度器彻底解决了 O(n) 的扩展性问题。其核心思想是使用多级优先级队列(Multi-Level Queue),将进程按优先级分散到 140 个不同的队列中,调度复杂度降为 O(1)。

特性 O(n) 调度器 O(1) 调度器
调度复杂度 O(n) O(1)
数据结构 全局链表 140 个优先级队列 + 位图
时间片 静态分配 动态计算(基于优先级和交互性)
多核扩展 极差 较好(每个 CPU 独立运行队列)
交互性 差 好(通过睡眠时间补偿)

O(1) 调度器引入了 active 和 expired 双数组结构。进程在 active 数组中用完时间片后,被移动到 expired 数组并重新计算优先级。当 active 数组为空时,交换两个数组的指针,实现 O(1) 的队列切换。

O(1) 调度器:active / expired 双数组结构 Active 数组 优先级 0(实时进程) 优先级 100(普通进程) 优先级 139(空闲进程) Expired 数组 优先级 0(已耗尽) 优先级 100(已耗尽) 优先级 139(已耗尽) 时间片耗尽 Active 数组为空时,交换指针,O(1) 完成队列切换

2.3 CFS 调度器:完全公平的革新

Linux 2.6.23 合并了 CFS(Completely Fair Scheduler),由 Ingo Molnár 设计。CFS 抛弃了固定时间片和优先级队列,转而使用 红黑树 和 虚拟运行时间(vruntime) 来模拟“理想多任务处理器”。

  • 核心思想: 在理想多任务处理器中,每个进程获得 1/n 的 CPU 时间。CFS 通过 vruntime 衡量进程的运行进度,调度器总是选择 vruntime 最小的进程运行。
  • 数据结构: 红黑树(rbtree),插入和查找复杂度为 O(log n)。
  • 时间片: 不固定。CFS 使用 targeted latency(目标延迟)和 minimum granularity(最小粒度)动态计算时间片。
  • 权重: 通过 nice 值映射到权重,权重越高的进程 vruntime 增长越慢,从而获得更多 CPU 时间。
关键公式: vruntime += (实际运行时间 × 1024) / 进程权重。权重由 nice 值决定,nice 0 的权重为 1024。
CFS 调度器:红黑树 + vruntime vruntime: 100 vruntime: 80 vruntime: 120 vruntime: 60 vruntime: 90 ← 下一个运行

2.4 BFS 调度器:桌面响应优先

BFS(Brain Fuck Scheduler)由 Con Kolivas 开发,旨在优化桌面交互体验。BFS 使用 单链表 和 全局优先级位图,调度复杂度为 O(n),但针对桌面负载(通常进程数较少)做了极致优化。

特性 CFS BFS
目标场景 通用服务器/桌面 桌面交互(低延迟)
数据结构 红黑树 单链表 + 位图
调度复杂度 O(log n) O(n)
虚拟时间 vruntime 无(基于优先级和时间片)
多核扩展 优秀 较差(全局锁)
BFS 的哲学: 桌面系统通常只有几十个活跃进程,O(n) 的遍历开销可以接受。BFS 通过简化算法和减少锁竞争,在低负载下获得了比 CFS 更低的调度延迟。

2.5 EEVDF 调度器:延迟边界的新时代

Linux 6.6 合入了 EEVDF(Earliest Eligible Virtual Deadline First)调度器,由 Peter Zijlstra 实现。EEVDF 在 CFS 的 vruntime 基础上,引入了 虚拟截止时间(virtual deadline) 和 eligible time 的概念,为每个进程提供明确的延迟边界。

  • 核心思想: 每个进程被分配一个 虚拟截止时间,调度器选择 eligible(符合条件的)且截止时间最早的进程运行。
  • 数据结构: 红黑树 + 时间排序树,同时维护 vruntime 和 deadline 两个维度。
  • 优势: 提供更可预测的调度延迟,减少“饥饿”现象,对实时和交互负载更友好。
  • 兼容性: 完全兼容 CFS 的权重和 nice 值语义,用户态无感知。
重要变化: EEVDF 替换了 CFS 的 vruntime 选择逻辑,但保留了红黑树和权重体系。内核编译选项 CONFIG_SCHED_EEVDF 控制是否启用 EEVDF。
EEVDF 调度决策流程 计算 eligible time 筛选 eligible 进程 选择 deadline 最早者 调度执行

2.6 调度器演进对比总结

调度器 引入版本 核心数据结构 复杂度 主要优势 主要劣势
O(n) 2.4 全局链表 O(n) 实现简单 扩展性差,交互性差
O(1) 2.6.0 优先级队列 + 位图 O(1) 调度延迟恒定 时间片计算复杂,交互性不够精确
CFS 2.6.23 红黑树 + vruntime O(log n) 公平性极佳,交互性好 高负载下树操作开销
BFS 外部 单链表 + 位图 O(n) 桌面低延迟 多核扩展差
EEVDF 6.6 红黑树 + 截止时间 O(log n) 延迟可预测,防饥饿 实现复杂度高
学习建议: 理解 CFS 的 vruntime 和红黑树是掌握 EEVDF 的基础。EEVDF 可以看作是 CFS 的“超集”,在公平性基础上增加了延迟边界保证。建议读者先深入阅读 CFS 源码(kernel/sched/fair.c),再对比 EEVDF 的改动。