3. 完全公平调度器(CFS)核心原理:红黑树、虚拟运行时间(vruntime)、权重与优先级

完全公平调度器(Completely Fair Scheduler, CFS)是 Linux 内核自 2.6.23 版本引入的默认调度器,旨在为每个进程提供“完全公平”的 CPU 时间分配。CFS 摒弃了传统的时间片轮转模型,转而采用基于虚拟运行时间(vruntime)的数学模型,通过红黑树数据结构高效管理就绪队列,并利用权重机制将进程优先级映射为实际的时间分配比例。

核心思想:CFS 的理想模型是让每个进程获得完全相同的 CPU 时间。优先级和权重的作用是打破这种绝对公平,允许某些进程(如交互式进程)获得更多或更少的执行机会,但始终以 vruntime 为基准进行调度决策。

3.1 虚拟运行时间(vruntime)

vruntime 是 CFS 调度器的核心度量指标。它表示一个进程在理想、完全公平的多任务系统下应该已经运行的时间。实际运行时间通过进程的权重进行加权,从而得到 vruntime。

vruntime 的计算公式:

vruntime = 实际运行时间 × (NICE_0_LOAD / 进程权重)

其中 NICE_0_LOAD 是 nice 值为 0 时的标准权重(1024)。权重越大的进程(优先级越高),其 vruntime 增长越慢;权重越小的进程(优先级越低),其 vruntime 增长越快。CFS 总是选择 vruntime 最小的进程来运行,从而保证所有进程的 vruntime 尽可能接近。

关键特性:
  • 单调递增:vruntime 随时间单调递增,不会回退。
  • 权重敏感:高优先级进程的 vruntime 增长慢,因此更容易被选中。
  • 睡眠补偿:进程睡眠时 vruntime 保持不变,唤醒后其 vruntime 相对较小,从而获得优先运行权(交互式进程优化)。
不同权重进程的 vruntime 增长曲线 实际运行时间 (ms) vruntime 高权重 (nice=-20) 标准权重 (nice=0) 低权重 (nice=19) 斜率 = 实际运行时间 / 权重

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

CFS 使用红黑树来组织所有可运行进程(就绪队列)。红黑树是一种自平衡二叉查找树,其查找、插入、删除操作的时间复杂度均为 O(log n),非常适合调度器频繁选择最小 vruntime 进程的场景。

红黑树在 CFS 中的角色:

  • 键值:每个节点的键值是进程的 vruntime。
  • 最左节点:树中最左侧的节点(vruntime 最小)即为下一个要运行的进程。
  • 缓存指针:内核缓存了最左节点的指针,避免每次调度都遍历整棵树。
  • 进程数量:树中节点数等于当前可运行进程的数量。
注意:红黑树只包含 TASK_RUNNING 状态的进程。睡眠、停止、僵尸状态的进程不在树中。当进程被唤醒时,会重新插入红黑树。
CFS 红黑树结构示意图 vruntime=100 vruntime=80 vruntime=120 vruntime=60 vruntime=90 vruntime=110 vruntime=130 ← 最左节点(下一个运行) 查找/插入/删除 时间复杂度 O(log n)

3.3 权重与优先级映射

Linux 内核使用 nice 值作为用户可见的优先级接口,范围从 -20(最高优先级)到 +19(最低优先级)。CFS 内部将 nice 值映射为权重(weight),权重决定了进程的 vruntime 增长速率。

nice 值与权重的映射表(部分):

nice 值 权重 相对比例(相对于 nice=0) 典型场景
-20 88761 86.7 倍 实时音频处理
-10 11051 10.8 倍 高优先级用户进程
0 1024 1.0 倍(基准) 普通进程
10 304 0.3 倍 后台批处理任务
19 15 0.015 倍 低优先级空闲任务
权重计算原理:内核使用一个预定义的权重数组 sched_prio_to_weight[40],将 nice 值 [-20, 19] 映射到对应的权重值。权重值的设计使得每降低一个 nice 值(优先级提高),CPU 时间分配增加约 10%。

3.4 CFS 调度决策流程

CFS 的调度决策是一个持续循环的过程,核心步骤包括:

  1. 选择下一个进程:从红黑树中获取 vruntime 最小的节点(最左节点)。
  2. 上下文切换:保存当前进程的状态,恢复目标进程的状态。
  3. 运行进程:允许进程在 CPU 上执行。
  4. 时钟中断触发:每次时钟中断(通常为 1ms 或 4ms)更新当前进程的 vruntime。
  5. 检查是否需要抢占:如果当前进程的 vruntime 超过了红黑树中最左节点的 vruntime,则设置重新调度标志。
  6. 重新插入红黑树:当进程时间片用完或被抢占时,将其重新插入红黑树,并回到步骤 1。
CFS 调度决策流程 选择 vruntime 最小进程 上下文切换 进程运行 时钟中断更新 vruntime vruntime > 最左节点? 是 设置重新调度 否 继续运行 循环调度

3.5 CFS 与实时调度器的对比

特性 CFS (SCHED_NORMAL/SCHED_BATCH) 实时调度器 (SCHED_FIFO/SCHED_RR)
调度依据 vruntime(虚拟运行时间) 静态优先级(0-99)
数据结构 红黑树(按 vruntime 排序) 位图 + 链表(按优先级队列)
公平性 完全公平(按权重比例分配) 严格优先级(高优先级先运行)
抢占点 时钟中断、进程唤醒、yield 更高优先级进程就绪时立即抢占
适用场景 普通用户进程、批处理、交互式 实时系统、音频/视频、工业控制
实践建议:在大多数通用 Linux 系统中,99% 的进程使用 CFS 调度策略。实时调度策略应仅用于明确需要确定性延迟的应用程序,滥用实时优先级可能导致系统响应异常甚至死锁。

3.6 内核代码关键路径

CFS 的核心实现在 kernel/sched/fair.c 文件中。以下是一些关键函数及其作用:

// 选择下一个要运行的进程(红黑树最左节点)
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
    struct sched_entity *se = __pick_first_entity(cfs_rq);
    // ... 缓存优化逻辑
    return se;
}

// 更新进程的 vruntime
static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_clock_task(rq_of(cfs_rq));
    u64 delta_exec = now - curr->exec_start;

    curr->vruntime += calc_delta_fair(delta_exec, curr);
    curr->exec_start = now;
}

// 将进程插入红黑树
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    update_curr(cfs_rq);
    // ... 更新统计信息
    __enqueue_entity(cfs_rq, se);  // 实际插入红黑树
    se->on_rq = 1;
}

// 权重计算:nice 值 -> 权重
static const int sched_prio_to_weight[40] = {
 /* -20 */ 88761, 71755, 56483, 46273, 36291,
 /* -15 */ 29154, 23254, 18705, 14949, 11916,
 /* -10 */ 9548, 7620, 6100, 4904, 3906,
 /*  -5 */ 3121, 2501, 1991, 1586, 1277,
 /*   0 */ 1024, 820, 655, 526, 423,
 /*   5 */ 335, 272, 215, 172, 137,
 /*  10 */ 110, 87, 70, 56, 45,
 /*  15 */ 36, 29, 23, 18, 15,
};
关键洞察:CFS 的设计哲学是“模拟理想多任务处理器”。在理想处理器上,n 个进程同时运行,每个进程获得 1/n 的 CPU 时间。CFS 通过 vruntime 和红黑树,在单 CPU 上近似实现了这种理想模型,同时通过权重机制保留了优先级控制的灵活性。