2. CFS调度器原理:完全公平调度器(CFS)的红黑树数据结构、虚拟运行时间(vruntime)计算、调度周期与最小粒度

完全公平调度器(Completely Fair Scheduler, CFS)是 Linux 内核自 2.6.23 版本引入的默认调度类,用于普通(SCHED_NORMAL / SCHED_OTHER)进程的 CPU 时间分配。CFS 的核心设计理念是:为每个进程提供 CPU 时间的“理想多任务处理器”模型,即假设存在一个无限快的 CPU,所有进程在同一时刻并行运行。在实际物理 CPU 上,CFS 通过精确的虚拟运行时间(vruntime)和红黑树数据结构,以 O(log n) 的复杂度选择下一个最需要 CPU 的进程。

核心思想:CFS 不直接分配时间片,而是通过 vruntime 衡量进程的“不公平程度”。vruntime 越小的进程,说明它获得的 CPU 时间越少,越应该被优先调度。

2.1 红黑树数据结构:CFS 的运行队列

CFS 为每个 CPU 维护一个红黑树(Red-Black Tree),作为其就绪队列(cfs_rq)。红黑树是一种自平衡的二叉搜索树,其插入、删除和查找操作的时间复杂度均为 O(log n)。CFS 使用红黑树来组织所有可运行的 SCHED_NORMAL 进程,以进程的 vruntime 作为键值。

  • 树节点:每个节点对应一个调度实体(struct sched_entity),通常内嵌在 task_struct 中。
  • 排序规则:左子树的 vruntime 小于父节点,右子树的 vruntime 大于父节点。
  • 最左节点:红黑树中最左侧的节点(vruntime 最小)即为下一个被调度执行的进程。
  • 缓存指针:cfs_rq 中缓存了指向最左节点的指针(rb_leftmost),使得选择下一个进程的操作在大多数情况下为 O(1)。
为什么选择红黑树? 相比传统的链表或优先级队列,红黑树在动态插入/删除频繁的调度场景下,能保持稳定的对数级性能,且内存开销可控。CFS 的调度点(时钟中断、进程唤醒、进程创建/退出)都会触发红黑树的更新操作。
CFS 红黑树数据结构与最左节点选择 CFS 红黑树结构(vruntime 排序) vruntime=100 vruntime=50 vruntime=150 vruntime=20 vruntime=80 vruntime=130 vruntime=180 ← 最左节点(下一个调度) 说明: 红黑树以 vruntime 为键值排序。最左侧节点(vruntime 最小)表示该进程获得的 CPU 时间最少, 因此 CFS 优先调度它。cfs_rq 缓存了 rb_leftmost 指针,使选择过程接近 O(1)。

2.2 虚拟运行时间(vruntime)计算

vruntime 是 CFS 调度决策的核心度量。它表示一个进程在理想多任务处理器上应该获得的运行时间。实际运行时间通过 权重(weight) 进行归一化,使得高优先级(低 nice 值)进程的 vruntime 增长更慢,从而获得更多 CPU 时间。

vruntime 的更新公式:

vruntime += delta_exec * (NICE_0_LOAD / se->load.weight)
  • delta_exec:进程本次实际运行的物理时间(单位:ns)。
  • NICE_0_LOAD:nice 值为 0 时的默认负载权重(1024)。
  • se->load.weight:当前进程的调度实体权重,由 nice 值映射得到。

权重映射表(部分):

nice 值 权重(weight) vruntime 增长速率(相对 nice=0) CPU 时间占比(2 个进程)
-20 88761 0.0115x(极慢) ≈ 98.9%
-10 11051 0.0927x ≈ 91.5%
0 1024 1.0x(基准) 50.0%
10 304 3.368x ≈ 22.9%
19 15 68.267x(极快) ≈ 1.4%
关键理解:vruntime 不是物理时间,而是“加权后的虚拟时间”。低 nice 值(高优先级)进程的权重更大,导致 vruntime 增长更慢,从而在红黑树中更长时间地保持靠左位置,获得更多 CPU 时间。反之,高 nice 值进程的 vruntime 增长极快,很快被推到红黑树右侧,调度优先级降低。
不同 nice 值下 vruntime 增长速率对比 不同 nice 值下 vruntime 增长速率对比(相同物理运行时间) 物理运行时间 (delta_exec) vruntime 增长量 nice=0 (基准) nice=-20 (极慢增长) nice=19 (极快增长) nice=-20 的进程权重极大,vruntime 几乎不增长,始终位于红黑树左侧;nice=19 的进程 vruntime 增长极快,迅速被调度器“惩罚”。

2.3 调度周期与最小粒度

CFS 通过 调度周期(sched_latency) 和 最小粒度(min_granularity) 两个参数来控制进程切换的频率和公平性。

2.3.1 调度周期(sched_latency)

调度周期是 CFS 保证每个可运行进程至少获得一次 CPU 执行的时间窗口。在默认配置下,sched_latency 为 6ms(可通过内核参数 kernel.sched_latency_ns 调整)。

时间片计算:

timeslice = sched_latency * (se->load.weight / cfs_rq->load.weight)
  • 当可运行进程数量较少时,每个进程获得的时间片较大,减少切换开销。
  • 当进程数量增多时,CFS 会启用 nice 0 负载倍数(nr_running > sched_nr_latency) 机制,将调度周期按比例放大,避免时间片过小导致频繁切换。

2.3.2 最小粒度(min_granularity)

最小粒度是 CFS 允许进程连续运行的最小时间单位,默认值为 0.75ms(可通过 kernel.sched_min_granularity_ns 调整)。

  • 作用:防止调度器过于频繁地抢占进程,避免上下文切换开销超过实际计算收益。
  • 抢占条件:当当前进程的运行时间超过其时间片,或者新唤醒进程的 vruntime 比当前进程的 vruntime 小至少 min_granularity 时,触发调度。
  • 边界情况:在实时性要求极高的场景(如音频处理),可以适当减小 min_granularity,但会增加 CPU 开销。
性能调优建议:
  • 减少上下文切换:对于 CPU 密集型工作负载,适当增大 sched_latency(如 12ms~24ms),让进程运行更长时间。
  • 降低延迟:对于交互式或延迟敏感型应用,减小 min_granularity(如 0.5ms),提高调度响应速度。
  • 注意:过度减小 min_granularity 会导致大量上下文切换,增加 CPU 开销和缓存污染。
调度周期内时间片分配 调度周期内时间片分配(3 个进程,nice 值不同) 0ms 6ms (sched_latency) 进程 A (nice=-10) 时间片 ≈ 4.0ms 进程 B (nice=0) 时间片 ≈ 1.5ms 进程 C (nice=10) 时间片 ≈ 0.5ms min_granularity=0.75ms 说明: 调度周期固定为 6ms,时间片按权重比例分配。高权重进程(nice=-10)获得更大时间片, 低权重进程(nice=10)时间片接近最小粒度。任何进程的时间片不得小于 min_granularity。

2.4 关键数据结构与代码路径

CFS 的核心数据结构定义在 kernel/sched/sched.h 和 kernel/sched/fair.c 中。以下是最关键的几个结构体:

数据结构 定义位置 关键字段 作用
struct sched_entity sched.h vruntime, load, run_node, on_rq 调度实体,内嵌在 task_struct 中,是红黑树的节点
struct cfs_rq sched.h tasks_timeline (rb_root), rb_leftmost, min_vruntime CFS 就绪队列,管理红黑树和调度统计
struct sched_class sched.h enqueue_task, dequeue_task, pick_next_task, put_prev_task 调度类接口,CFS 通过 fair_sched_class 实现

核心函数调用链(选择下一个进程):

pick_next_task_fair()
  ├── __pick_first_entity()    // 获取 rb_leftmost 节点
  ├── set_next_entity()        // 更新调度实体状态
  │     └── __dequeue_entity() // 从红黑树中移除当前进程
  └── return se->task          // 返回 task_struct
调试技巧:可以通过 /proc/sched_debug 查看每个 CPU 的 CFS 红黑树状态,包括当前最左节点的 vruntime、min_vruntime、nr_running 等信息。使用 perf sched 工具可以可视化调度延迟和切换频率。

2.5 总结:CFS 调度器的核心机制

机制 描述 性能影响
红黑树 以 vruntime 为键值排序,最左节点为下一个调度进程 O(log n) 插入/删除,O(1) 选择最左节点
vruntime 加权虚拟运行时间,低 nice 值增长慢,高 nice 值增长快 实现公平性,nice 值差异影响 CPU 分配比例
调度周期 保证所有进程在 sched_latency 内至少运行一次 增大可减少切换,但增加延迟;减小则提高响应性
最小粒度 进程连续运行的最小时间单位,防止过度抢占 过小导致大量上下文切换,过大降低交互响应