3. Linux调度器演进

Linux 内核调度器经历了多次重大重构,每一次演进都旨在解决特定场景下的性能瓶颈。对于实时系统开发者而言,理解调度器的演进历史,是掌握调度延迟根源、正确配置 Cyclictest 测试环境的前提。本章将依次剖析 O(n)、O(1)、CFS 以及实时调度类 (RT) 的设计哲学与核心差异。

3.1 O(n) 调度器:早期内核的遍历之痛

在 Linux 2.4 及更早版本中,调度器采用最简单的 O(n) 算法。其核心数据结构是一个全局运行队列,所有可运行进程被链接在一个链表中。

  • 调度流程:每次发生调度时,调度器必须遍历整个运行队列,计算每个进程的“好度 (goodness)”,选出得分最高的进程执行。
  • 时间复杂度:O(n),其中 n 为系统中可运行进程的数量。当进程数增多时,调度开销线性增长。
  • 多核问题:全局运行队列使用一把大内核锁,多核 CPU 无法并行调度,严重限制 SMP 扩展性。
  • 实时性缺陷:遍历时间不可预测,高优先级进程可能因遍历长链表而延迟获得 CPU。
关键指标: 在 O(n) 调度器中,若系统有 1000 个进程,每次调度决策可能需要遍历数千个条目,调度延迟可达毫秒甚至数十毫秒级,完全无法满足硬实时需求。
历史背景: O(n) 调度器设计于桌面服务器时代,当时系统进程数通常只有几十个,遍历开销尚可接受。但随着服务器负载增加和嵌入式实时需求的涌现,其缺陷迅速暴露。
O(n) 调度器全局运行队列遍历示意图 O(n) 调度器:全局链表遍历 全局运行队列 (runqueue) — 单链表 进程 A (prio=120) 进程 B (prio=110) 进程 C (prio=99) 进程 D (prio=115) 进程 E (prio=130) 调度器遍历所有节点计算 "goodness" ⚠ 时间复杂度 O(n):进程数越多,调度延迟越大。高优先级进程 C (prio=99) 必须等待遍历完成才能被选中。

3.2 O(1) 调度器:可扩展性的里程碑

Linux 2.6.0 引入的 O(1) 调度器彻底改变了调度算法的时间复杂度。它使用两个优先级数组(active 和 expired),每个优先级对应一个 FIFO 队列,并利用位图快速查找最高优先级非空队列。

  • 核心数据结构:140 个优先级队列(0-99 为实时,100-139 为普通),每个队列是一个链表头。
  • 调度决策:通过 __ffs() 位图指令在常数时间内找到最高优先级非空队列,时间复杂度降为 O(1)。
  • 时间片分配:基于优先级静态分配时间片,高优先级进程获得更长的时间片。
  • 多核友好:每个 CPU 拥有独立的运行队列,减少锁竞争,提升 SMP 扩展性。
局限性: O(1) 调度器虽然解决了遍历性能问题,但其时间片分配机制对交互式进程不够友好。静态优先级导致桌面响应迟钝,且对 NUMA 拓扑感知不足。
O(1) 调度器优先级位图与双数组结构 O(1) 调度器:优先级位图 + 双数组 优先级位图 (140 bit) — 通过 __ffs() 在 O(1) 内找到最高优先级 active 数组 (当前可运行进程) expired 数组 (时间片耗尽进程) prio=0 prio=1 prio=99 ... prio=100 prio=101 prio=139 交换 调度器通过位图直接定位最高优先级队列 ✅ 时间复杂度 O(1):无论系统有多少进程,调度决策时间恒定。 ✅ 每个 CPU 独立运行队列,支持 SMP 并行调度。 ⚠ 静态时间片分配对交互式进程不友好,桌面响应可能迟钝。

3.3 完全公平调度器 (CFS):公平与交互的平衡

Linux 2.6.23 引入的 CFS 由 Ingo Molnár 设计,彻底放弃了固定时间片和优先级数组,转而采用基于 虚拟运行时间 (vruntime) 的红黑树模型。CFS 的目标是让每个进程获得 CPU 时间的“公平份额”。

  • 核心数据结构:红黑树 (Red-Black Tree),键值为进程的 vruntime。最左侧节点(vruntime 最小)即为下一个被调度执行的进程。
  • vruntime 计算:vruntime += 实际运行时间 × (NICE_0_LOAD / 进程权重)。权重由优先级决定,高优先级进程 vruntime 增长更慢,从而获得更多 CPU 时间。
  • 调度延迟目标:引入 targeted_latency(默认 6ms)和 minimum_granularity(默认 0.75ms),确保每个进程在调度周期内至少运行一次。
  • 组调度:支持将进程分组,在组间按权重分配 CPU 时间,实现更细粒度的资源控制。
实时性影响: CFS 的 vruntime 机制对普通进程公平性极佳,但实时进程 (SCHED_FIFO/SCHED_RR) 不受 CFS 管理,直接由实时调度类处理。CFS 的调度延迟通常在微秒级,但受内核抢占点和中断处理影响。
CFS 红黑树与 vruntime 调度模型 CFS 调度器:红黑树 + vruntime vruntime=100 vruntime=80 vruntime=60 vruntime=90 vruntime=120 vruntime=110 vruntime=130 调度器选择最左侧节点 (vruntime 最小) 执行 ✅ 红黑树插入/删除时间复杂度 O(log n),兼顾公平与性能。 ✅ vruntime 动态调整,高优先级进程增长慢,获得更多 CPU 时间。

3.4 实时调度类 (RT):确定性优先

实时调度类独立于 CFS,专门管理 SCHED_FIFO 和 SCHED_RR 实时进程。它采用优先级驱动的调度策略,确保最高优先级的实时进程能够立即抢占 CPU。

  • SCHED_FIFO:先进先出调度。一旦获得 CPU,除非被更高优先级进程抢占或主动阻塞,否则会一直运行。
  • SCHED_RR:时间片轮转调度。与 FIFO 类似,但相同优先级的进程按时间片轮转执行。
  • 优先级范围:0-99,数值越大优先级越高。所有实时进程优先级高于普通进程 (100-139)。
  • 调度类层次:内核先检查 RT 调度类是否有可运行进程,若有则直接调度,否则回退到 CFS。
实时调度陷阱: 若实时进程进入无限循环且未设置 CPU 亲和性或 cpuset 限制,可能导致系统完全锁死,因为 CFS 进程永远无法获得 CPU。Cyclictest 通常以 SCHED_FIFO 优先级 80-99 运行,需谨慎配置。
实时调度类与 CFS 的层次关系 调度类层次:RT 优先于 CFS 核心调度器 (__schedule) 实时调度类 (RT) SCHED_FIFO / SCHED_RR (prio 0-99) 完全公平调度类 (CFS) SCHED_NORMAL / SCHED_BATCH (prio 100-139) 🔍 调度决策逻辑:先检查 RT 调度类是否有可运行进程,若有则直接调度;否则回退到 CFS。 🔍 实时进程优先级 (0-99) 始终高于普通进程 (100-139),确保实时响应。 💡 Cyclictest 通常以 SCHED_FIFO 优先级 80-99 运行,确保测试线程优先获得 CPU。 💡 实时调度类不进行公平性计算,调度延迟仅取决于抢占延迟和中断响应时间。

3.5 调度器演进对比总结

下表从时间复杂度、公平性、实时性、多核扩展性四个维度对比四种调度器,帮助理解为何实时测试必须关注调度类选择。

特性 O(n) 调度器 O(1) 调度器 CFS 调度器 RT 调度类
引入版本 Linux 2.4 Linux 2.6.0 Linux 2.6.23 Linux 2.6.0 (与 O(1) 共存)
时间复杂度 O(n) O(1) O(log n) O(1)
公平性 差 (基于 goodness 启发式) 中等 (静态时间片) 优秀 (vruntime 动态调整) 不适用 (优先级驱动)
实时性 差 (遍历延迟不可预测) 中等 (位图查找快,但受锁影响) 良好 (抢占点优化) 优秀 (立即抢占)
多核扩展性 差 (全局锁) 良好 (每 CPU 队列) 优秀 (负载均衡 + 组调度) 良好 (需配合 CPU 亲和性)
典型调度延迟 毫秒级 (1000+ 进程) 百微秒级 微秒级 (10-100μs) 微秒级 (1-10μs,理想情况)
实践建议: 运行 Cyclictest 时,务必确认测试线程绑定到 RT 调度类 (SCHED_FIFO)。若误将测试线程运行在 CFS 下,调度延迟将受 vruntime 和负载均衡影响,无法反映真实的实时性能。使用 chrt -f 80 cyclictest ... 确保实时优先级生效。