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)中的任务组。
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,
};
1.4 调度流程与抢占点
CFS 的调度决策发生在以下关键点:
- 周期性调度(schedule_tick):每个时钟中断触发,检查当前进程是否应该被抢占。
- 进程唤醒(try_to_wake_up):唤醒进程时检查是否可抢占当前进程。
- 自愿调度(cond_resched):内核抢占点,进程主动让出 CPU。
- 新进程创建(wake_up_new_task):新进程加入就绪队列时触发调度。
抢占判断条件: 当红黑树最左节点的 vruntime 与当前进程的 vruntime 之差超过
sysctl_sched_min_granularity(默认 0.75ms)时,触发抢占。这保证了 CFS 不会频繁切换,也不会让一个进程长时间独占 CPU。
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 | 一次负载均衡最多迁移的进程数 |