2. CFS调度器基础

完全公平调度器(Completely Fair Scheduler, CFS)是 Linux 内核自 2.6.23 版本起引入的默认调度类,用于普通进程(SCHED_NORMAL / SCHED_OTHER)的 CPU 时间分配。CFS 摒弃了传统基于固定时间片和优先级计算权重的 O(1) 调度器模型,转而采用一种基于“虚拟运行时间”的加权公平队列模型,其核心数据结构为红黑树。

2.1 CFS的设计哲学:模拟理想多任务处理器

CFS 的设计目标非常直观:让每个可运行的进程都能获得 CPU 时间的“公平”份额。其理想模型是假设存在一个“完美多任务处理器”,能够以无限小的粒度同时运行所有进程。在实际物理 CPU 上,CFS 通过以下原则逼近这一理想:

  • 公平性(Fairness): 在时间窗口内,所有进程获得的实际 CPU 时间与其权重(nice 值)成正比。
  • 低延迟(Low Latency): 交互式进程(如编辑器、终端)应能快速获得响应,避免长时间饥饿。
  • 高吞吐(High Throughput): 计算密集型进程在长时间运行中仍能获得应有的 CPU 份额。
  • 复杂度 O(log N): 调度决策(选择下一个运行进程)的时间复杂度与系统中可运行进程数量 N 呈对数关系,保证大规模服务器场景下的可扩展性。
核心思想: CFS 不直接分配时间片,而是通过 vruntime 衡量进程已消耗的 CPU 时间,并始终选择 vruntime 最小的进程运行。这等价于“谁受的苦最多,谁就优先获得 CPU”。

2.2 虚拟运行时间(vruntime)

vruntime 是 CFS 调度器的核心度量指标。它不是一个真实的墙上时钟时间,而是经过进程权重(优先级)加权后的“虚拟消耗时间”。

2.2.1 vruntime 的计算公式

每次时钟 tick 或进程状态切换时,内核会更新当前运行进程的 vruntime。其核心公式如下:

vruntime += delta_exec * (NICE_0_LOAD / se->load.weight)

其中:

  • delta_exec: 当前进程实际运行的物理时间(单位:ns)。
  • NICE_0_LOAD: nice 值为 0 时的默认权重(内核定义为 1024)。
  • se->load.weight: 当前进程的调度实体权重,由 nice 值映射而来。

从公式可以看出:

  • 权重越高(nice 值越低,优先级越高),load.weight 越大,vruntime 增长越慢。
  • 权重越低(nice 值越高,优先级越低),load.weight 越小,vruntime 增长越快。
  • nice 值为 0 的进程,其 vruntime 增长速率等于物理时间速率。

2.2.2 nice 值与权重的映射关系

内核维护一个全局数组 sched_prio_to_weight[40],将 -20 到 19 的 nice 值映射为权重。下表展示了部分映射关系:

nice 值 权重(load.weight) 相对 nice=0 的 CPU 占比 vruntime 增长速率(相对物理时间)
-20 88761 约 86.7 倍 极慢(1/86.7)
-10 29216 约 28.5 倍 慢(1/28.5)
0 1024 1 倍(基准) 1:1 同步
10 36 约 0.035 倍 快(约 28.5 倍)
19 15 约 0.015 倍 极快(约 68.3 倍)
关键理解: 高优先级(低 nice 值)进程的 vruntime 增长缓慢,因此它们能更长时间地保持在红黑树的最左侧(最小值),从而获得更多的 CPU 执行机会。低优先级进程的 vruntime 增长极快,一旦运行就会迅速“落后”,被调度器换出。

2.3 红黑树:CFS 的就绪队列数据结构

CFS 使用红黑树(Red-Black Tree)来组织每个 CPU 上所有可运行的调度实体(se)。红黑树是一种自平衡二叉查找树,其核心特性保证了 CFS 的高效性:

  • 有序性: 所有节点按照 vruntime 排序,最左侧节点(最小值)即为当前最需要 CPU 的进程。
  • 平衡性: 插入、删除、查找操作的时间复杂度均为 O(log N),其中 N 为可运行进程数。
  • 缓存友好: 红黑树的内存布局相对紧凑,有利于 CPU 缓存命中。

2.3.1 CFS 调度周期与红黑树操作流程

下图展示了 CFS 在一个调度周期内的核心操作流程:

CFS 调度周期核心流程 进程 A 正在运行 时钟中断 / 自愿调度点触发 更新进程 A 的 vruntime 检查 A 的 vruntime 是否仍为最小 是:继续运行 A 否:选择红黑树最左节点 说明:CFS 在每个调度点更新 vruntime,若当前进程不再是红黑树最左节点,则触发抢占。

2.3.2 红黑树在 CFS 中的具体实现

在 Linux 内核源码中,CFS 的红黑树实现位于 kernel/sched/fair.c 文件中。每个 CPU 有一个 struct cfs_rq 结构体,其中包含红黑树的根节点:

// 内核源码片段:struct cfs_rq 中的红黑树相关字段
struct cfs_rq {
    struct load_weight  load;           // 该队列的总权重
    unsigned int        nr_running;     // 可运行进程数
    u64                 min_vruntime;   // 队列中最小的 vruntime(用于新进程初始化)
    
    struct rb_root_cached  tasks_timeline; // 红黑树根节点(带缓存最左节点)
    struct sched_entity    *curr;          // 当前正在运行的调度实体
    struct sched_entity    *next;          // 下一个将要运行的调度实体(抢占优化)
    // ... 其他字段
};
性能优化细节: rb_root_cached 是内核提供的一种红黑树变体,它在标准红黑树基础上额外缓存了最左侧节点(即 vruntime 最小的节点)。这使得“选择下一个运行进程”的操作从 O(log N) 降为 O(1),因为调度器只需读取缓存的最左节点即可,无需遍历整棵树。

2.4 CFS 调度实体的核心数据结构

每个进程或线程在内核中对应一个 struct sched_entity(调度实体),它是 CFS 管理的基本单元。以下是其关键字段:

字段名 类型 说明
load.weight unsigned long 调度实体的权重,由 nice 值映射而来,决定 CPU 时间分配比例。
vruntime u64 虚拟运行时间,CFS 调度的核心排序依据。
run_node struct rb_node 红黑树节点,用于将调度实体插入到 cfs_rq 的红黑树中。
on_rq int 标记该实体是否在就绪队列上(1=在队列,0=不在)。
exec_start u64 当前运行片段的起始时间戳,用于计算 delta_exec。
sum_exec_runtime u64 该实体累计的总物理运行时间。
prev_sum_exec_runtime u64 上次切换出 CPU 时的累计运行时间,用于计算本次运行片段。

2.5 CFS 调度决策的完整过程

当调度器需要选择下一个运行进程时(例如在时钟中断、进程睡眠、yield 等场景),CFS 执行以下步骤:

  1. 更新当前进程的 vruntime: 根据公式 vruntime += delta_exec * (NICE_0_LOAD / load.weight) 更新当前运行实体的 vruntime。
  2. 检查是否触发抢占: 比较当前进程的 vruntime 与红黑树最左节点的 vruntime。如果当前进程的 vruntime 大于最左节点,说明有更“饥饿”的进程等待运行,设置 TIF_NEED_RESCHED 标志。
  3. 选择下一个进程: 在 pick_next_task_fair() 函数中,从 cfs_rq 的红黑树中取出最左节点(vruntime 最小)作为下一个运行实体。
  4. 上下文切换: 执行 context_switch(),保存当前进程的寄存器状态,恢复新进程的寄存器状态,完成切换。
  5. 新进程开始运行: 新进程的 exec_start 被设置为当前时间,开始累积其 vruntime。
实战要点: 在 cgroup CPU 带宽控制中,CFS 的 vruntime 机制被扩展为 组调度(Group Scheduling)。每个 cgroup 本身也是一个调度实体(struct task_group),拥有自己的 vruntime 和红黑树。这使得 CPU 带宽可以在 cgroup 之间按权重分配,而 cgroup 内部的进程再按 CFS 规则竞争。这是后续章节的核心基础。

2.6 CFS 与 O(1) 调度器的关键差异

对比维度 O(1) 调度器(2.6.0-2.6.22) CFS 调度器(2.6.23+)
核心数据结构 140 个优先级链表(active/expired 数组) 红黑树(按 vruntime 排序)
时间片分配 固定时间片,基于优先级静态计算 动态时间片,基于 vruntime 和权重动态调整
交互式进程识别 启发式算法(睡眠时间、平均运行时间等) 天然支持:交互式进程睡眠时间长,vruntime 小,唤醒后立即获得 CPU
公平性 优先级高的进程可能长时间霸占 CPU 严格按权重比例分配,vruntime 保证长期公平
复杂度 O(1) 选择进程,但负载均衡复杂 O(log N) 选择进程,但实际最左节点缓存为 O(1)
组调度支持 需要额外补丁,不原生支持 原生支持,通过 struct task_group 实现层级调度

2.7 本章小结