1. CFS 调度器概述

完全公平调度器(Completely Fair Scheduler, CFS)自 Linux 2.6.23 引入以来,一直是 Linux 内核默认的 CPU 调度器。CFS 的核心目标是在所有可运行进程之间公平地分配 CPU 时间,同时保持极低的调度延迟和良好的交互性。与传统的基于时间片轮转的调度器不同,CFS 采用基于虚拟运行时间的红黑树模型,实现了 O(log n) 的调度复杂度。

1.1 设计哲学:从时间片到虚拟时间

CFS 的设计哲学可以概括为“理想的多任务处理器”模型:假设存在一个完全理想的 CPU,能够同时运行所有进程,每个进程获得 1/n 的 CPU 算力。CFS 通过虚拟运行时间(vruntime)来逼近这一理想状态。

传统调度器 CFS 调度器
固定时间片(如 100ms) 动态时间片,基于权重和负载
优先级通过时间片长度体现 优先级通过 vruntime 增长速率体现
就绪队列为多级链表 就绪队列为红黑树(按 vruntime 排序)
调度点固定(时钟中断) 调度点动态(tickless + 抢占点)
交互进程需特殊启发式 低延迟自然获得更多 CPU
核心公式: vruntime 增长速率 = 实际运行时间 × (NICE_0_LOAD / 进程权重)。权重越大的进程(优先级越高),vruntime 增长越慢,从而在红黑树中更靠左,被调度器优先选择。

1.2 核心数据结构:三层调度实体

CFS 的调度数据组织为三层结构,从全局到进程粒度逐层细化:

  • rq(runqueue):每个 CPU 一个,是调度器的顶级结构。包含当前运行的进程、CFS 就绪队列、实时就绪队列、停止调度器队列等。
  • cfs_rq(CFS runqueue):每个 CPU 的 CFS 就绪队列,内嵌在 rq 中。维护红黑树根节点、最小 vruntime、运行中的进程计数等。
  • sched_entity(调度实体):可被调度的基本单元。每个进程(task_struct)内嵌一个 sched_entity,记录 vruntime、权重、负载贡献等。
关键理解: 在 CFS 中,调度器不直接操作 task_struct,而是操作 sched_entity。这使得 CFS 可以统一调度普通进程和组调度(group scheduling)中的任务组。
CFS 三层调度数据结构关系图 struct rq (per-CPU) nr_running | curr | stop | rt | cfs struct cfs_rq rb_root | min_vruntime | nr_running 红黑树根 (rb_root) sched_entity A vruntime=100 | weight=1024 sched_entity B vruntime=150 | weight=2048 sched_entity C vruntime=200 | weight=512 说明: 每个 CPU 拥有一个 rq,rq 内嵌 cfs_rq,cfs_rq 维护一棵按 vruntime 排序的红黑树。 红黑树最左节点(vruntime 最小)即为下一个被调度运行的进程。

1.3 虚拟时钟(vruntime)概念

vruntime 是 CFS 调度器的核心度量指标,它衡量一个进程在理想 CPU 上应该获得的运行时间。实际运行时间越短的进程,其 vruntime 越小,越容易被调度器选中。

vruntime 的计算

/* kernel/sched/fair.c */
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
    if (unlikely(se->load.weight != NICE_0_LOAD))
        delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
    return delta;
}

/* 每次时钟 tick 更新 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;

    delta_exec = now - curr->exec_start;
    curr->vruntime += calc_delta_fair(delta_exec, curr);
    curr->exec_start = now;
    /* 更新 min_vruntime */
    update_min_vruntime(cfs_rq);
}

vruntime 的关键特性

特性 说明 影响
单调递增 进程运行时 vruntime 只增不减 保证调度公平性
权重加权 高权重进程 vruntime 增长慢 高优先级进程获得更多 CPU
min_vruntime cfs_rq 维护的最小 vruntime 新进程/唤醒进程的 vruntime 初始化基准
睡眠补偿 长时间睡眠的进程 vruntime 被重置 交互进程获得响应优先权
跨 CPU 迁移 迁移时 vruntime 按目标 CPU 的 min_vruntime 归一化 保证多核间的公平性
常见误区: vruntime 不是实际运行时间,而是“归一化后的虚拟时间”。两个权重不同的进程运行相同的实际时间,权重大的进程 vruntime 增量更小。因此,CFS 总是选择 vruntime 最小的进程运行,这等价于选择“欠 CPU 时间最多”的进程。

权重与 nice 值的映射

CFS 使用 prio_to_weight[] 数组将用户空间的 nice 值(-20 ~ 19)映射为调度权重。权重越大,vruntime 增长越慢,获得的 CPU 时间比例越高。

/* kernel/sched/core.c */
static const int 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,
};
vruntime 增长速率与 nice 值的关系 高 低 vruntime 增长速率 -20 -10 0 (NICE_0) 10 19 nice 值 nice=0 weight=1024 nice=-20 weight=88761 nice=19 weight=15 nice 值越低 → 权重越大 → vruntime 增长越慢 → 获得更多 CPU

1.4 调度流程与抢占点

CFS 的调度决策发生在以下关键点:

  1. 周期性调度(schedule_tick):每个时钟中断触发,检查当前进程是否应该被抢占。
  2. 进程唤醒(try_to_wake_up):唤醒进程时检查是否可抢占当前进程。
  3. 自愿调度(cond_resched):内核抢占点,进程主动让出 CPU。
  4. 新进程创建(wake_up_new_task):新进程加入就绪队列时触发调度。
抢占判断条件: 当红黑树最左节点的 vruntime 与当前进程的 vruntime 之差超过 sysctl_sched_min_granularity(默认 0.75ms)时,触发抢占。这保证了 CFS 不会频繁切换,也不会让一个进程长时间独占 CPU。
CFS 调度决策流程 时钟中断触发 update_curr() 检查抢占条件 触发 schedule() 继续运行当前进程 vruntime 差 > 阈值 vruntime 差 ≤ 阈值 调度点: schedule_tick() try_to_wake_up()

1.5 关键内核参数预览

以下 sysctl 参数直接控制 CFS 的调度行为,后续章节将逐一深入调优:

参数名 默认值 作用
kernel.sched_min_granularity_ns 750000 (0.75ms) 进程最小运行时间,防止频繁切换
kernel.sched_latency_ns 6000000 (6ms) 目标调度延迟,决定时间片分配
kernel.sched_wakeup_granularity_ns 1000000 (1ms) 唤醒抢占的 vruntime 阈值
kernel.sched_migration_cost_ns 500000 (0.5ms) 进程迁移的缓存热度评估时间
kernel.sched_nr_migrate 32 一次负载均衡最多迁移的进程数