本页目录
调度器
覆盖: CFS (vruntime/红黑树) → EEVDF (6.6+) → 调度类架构 → 负载均衡 → 抢占 内核版本: 2.6 ~ 6.x,重点标注 CFS→EEVDF 变革
概述
调度器是内核中调用最频繁的子系统之一。每一次 schedule() 调用都要回答一个问题:下一个该跑谁? 这个问题看似简单,但要在"交互响应快、吞吐高、公平、节能、多核扩展"之间取得平衡,复杂度极高。
Linux 的答案经历了三个时代:
- 2.4: O(n) 调度器 — 遍历所有进程选最高 priority
- 2.6 ~ 6.5: CFS (Completely Fair Scheduler) — vruntime 驱动的红黑树
- 6.6+: EEVDF — Earliest Eligible Virtual Deadline First
本文聚焦 CFS 和 EEVDF 的设计原理,追溯关键代码路径。
调度类架构
Linux 调度器不是单一算法,而是可堆叠的调度类:
优先级从高到低:
| 调度类 | 策略 | 典型场景 |
|---|---|---|
stop_sched_class | 最高优先级,抢占一切 | CPU hotplug, migration |
dl_sched_class | Deadline (EDF + CBS) | 硬实时任务 |
rt_sched_class | POSIX RT (FIFO / RR) | 软实时,chrt -f |
fair_sched_class | CFS / EEVDF | 所有普通进程 |
idle_sched_class | 只在无其他任务时运行 | idle task (PID 0 per-CPU) |
// kernel/sched/sched.h
;
调度类的可堆叠设计是 Linux 调度器最优雅的架构决策之一:实时和普通任务共享同一个 schedule() 入口,无需特殊处理。任何一个调度类返回 NULL,框架就往下问下一个类。最终 idle_sched_class 保证永远有进程可跑。
核心数据结构
runqueue (struct rq)
// kernel/sched/sched.h
;
每个 CPU 有自己的 struct rq。锁粒度是 per-rq,这意味着调度决策只需拿本 CPU 的锁——这是调度器在大核数系统上能扩展的关键。raw_spinlock_t 意味着拿这个锁时必须关中断——因为 scheduler tick 可能在任何时候触发。
sched_entity — 调度实体
// include/linux/sched.h
;
CFS: vruntime 驱动的公平性
核心思想
CFS 不按时间片分配 CPU,而是按照虚拟运行时间 (vruntime) 排序:
vruntime = 物理执行时间 × (NICE_0_LOAD / weight_of_process)
进程的 nice 值越低 (优先级越高),weight 越大 → 相同的物理时间累计更少的 vruntime → 排在红黑树更左边 → 更早被选中。
本质:CFS 追求的是 vruntime 的收敛。所有进程的 vruntime 应该趋同;优先调度 vruntime 最小的进程,就是在补偿"亏欠 CPU 最多"的进程。
nice → weight 映射
// kernel/sched/core.c
const int sched_prio_to_weight = ;
关键性质:每降低一个 nice 值,weight 大约增加 25%。这意味着 nice -20 的进程获得的 CPU 时间大约是 nice 0 的 87 倍(不是简单的线性关系)。这种指数衰减的设计来自 CFS 的原始论文——它确保相邻 nice 值的公平差是一致的。
关键函数
// kernel/sched/fair.c
// 更新 vruntime (每次 tick / enqueue / dequeue / put_prev 调用)
static void
// 挑选下一个: 红黑树最左节点
static struct sched_entity *
calc_delta_fair() 实现 delta * (NICE_0_LOAD / weight)。它使用 128 位定点运算避免溢出,并处理了精度问题(对于很小的 delta 不做缩水,防止 vruntime 完全不增长)。
vruntime 的边界处理
新进程的 vruntime 初始值不能从 0 开始——否则新 fork 的进程会垄断 CPU 很长时间。CFS 的做法:
// kernel/sched/fair.c: place_entity()
static void
sched_vslice() 返回的 penalty 约等于这个进程第一次能跑的时间片——这样新进程不会比已在运行的进程"更穷",保持了公平。
min_vruntime 追踪
// cfs_rq->min_vruntime 单调递增,追踪所有出队进程的最小 vruntime
// 在 update_min_vruntime() 中更新:
static void
// min_vruntime 永不倒退——这是保证公平的关键不变量
EEVDF: 6.6 的范式转移
为什么需要 EEVDF
CFS 的 vruntime 排序在 CPU-bound 场景下很公平,但在延迟敏感场景有缺陷:
- 进程 slept 了很久 → vruntime 极度低于所有人 → 被唤醒后长时间占据 CPU(延迟突发)
- CFS 没有"该什么时候完成"的概念,只有"谁最该跑"
- 响应延迟不可预测——sleep 后的进程可能要等很久才能上 CPU(vruntime 虽然低但不保证立即响应)
EEVDF 通过引入 deadline 解决了这个问题。每个实体在入队时计算一个 eligible time(何时有资格运行)和 deadline(期望完成时间)。调度器按 deadline 排序,但只从 eligible 的进程中选择。
EEVDF 核心算法
// kernel/sched/fair.c (6.6+)
// 请求时间 (request): scale the slice by weight
static u64
// 入队: 计算 deadline
static void
// pick_next: 从 eligible 实体中选 deadline 最早的
static struct sched_entity *
关键区别:
| CFS | EEVDF | |
|---|---|---|
| 选择标准 | vruntime 最小 | deadline 最早 (从 eligible 中) |
| slept 进程行为 | 长时间垄断 CPU (vruntime 极低) | 被唤醒后有 lag 补偿,但 slice 用完就得重排队 |
| 延迟保证 | 无 | 有 (每个进程有明确的完成截止) |
| 实现复杂度 | 较低 | 中等 (多了 eligibility 检查和 vlag) |
slice 与 deadline 的动态调整
// sysctl_sched_base_slice: 基准时间片 (默认 3ms for EEVDF)
// 每个进程的实际 slice = base × (weight / cfs_rq_weight)
// 权重大的进程获得更长的 slice → 更少的上下文切换
// 如果进程在 slice 用完之前就让出 CPU (如 block 在 IO):
// → vruntime 停止增长 → vlag 变大 → 唤醒时更容易 eligible
// → 交互式的"用一下就等"模式得到补偿
lag 补偿机制
// lag: 这是 EEVDF 与 CFS 最大的实现差异
// 定义: lag = avg_vruntime - se->vruntime
// lag > 0: 进程落后于平均 → 应该优先
// lag < 0: 进程超前 → 应该等待
// 被唤醒时:
static int
抢占
唤醒抢占 (wakeup preemption)
// kernel/sched/fair.c: check_preempt_wakeup()
static void
tick 抢占
// kernel/sched/fair.c: entity_tick()
// 每个 scheduler tick (CONFIG_HZ, 通常 250/500/1000) 触发
static void
NEED_RESCHED 与延迟调度
// resched_curr() 只设 TIF_NEED_RESCHED 标志
// 不是在这里做 schedule() —— 我们在中断上下文!
// 真正的上下文切换在返回用户态之前的 exit_to_user_mode_loop() 中:
//
// exit_to_user_mode_loop() {
// if (ti_work & _TIF_NEED_RESCHED)
// schedule();
// }
//
// 或者在抢占内核 (CONFIG_PREEMPT=y) 的 preempt_schedule_irq() 中
负载均衡
调度域层级
// 现代 2-socket AMD EPYC 的典型层级:
// SMT (超线程, L1 共享) < MC (多核, L3 共享) < DIE (同 socket) < NUMA (跨 socket)
// 每层有自己的 sched_domain,构成嵌套的 CPU mask
// kernel/sched/topology.c: 启动时由 arch 代码初始化
// x86: arch/x86/kernel/smpboot.c 构造调度域拓扑
// ARM: drivers/base/arch_topology.c 从 DT/ACPI 获取
均衡触发时机
有四类均衡:
// 1. idle balance: CPU 即将 idle → 在 schedule() 中调用 newidle_balance()
// → 主动从 busy CPU 拉任务 (启动快,避免 idle 延迟)
// 2. periodic balance: scheduler_tick() → trigger_load_balance()
// → SCHED_SOFTIRQ → run_rebalance_domains()
// → 由 softirq 异步执行,频率 = 1 / (4 * sched_domain->interval)
// 3. fork/exec balance: wake_up_new_task() → select_task_rq_fair()
// → find_idlest_cpu() → 在调度域内找最空闲的 CPU
// 4. wake balance: try_to_wake_up() → select_task_rq_fair()
// → find_idlest_cpu() 或 wake_affine() (优先唤醒 CPU)
load_balance 实现
// kernel/sched/fair.c: load_balance()
static int
组调度 (CGroup CPU controller)
// 启用: CONFIG_CGROUP_SCHED
// 接口: /sys/fs/cgroup/cpu/<group>/
// cpu.shares — 权重分配 (默认 1024)
// 各组按 shares 比例分 CPU。A=2048, B=1024 → A:B = 2:1
// cpu.cfs_quota_us — 周期内最大 CPU 时间
// cpu.cfs_period_us — 周期长度 (默认 100ms)
// quota=50000, period=100000 → 最多用 50% CPU
每个 cgroup 对应一个 task_group,包含一个 sched_entity 嵌入父 cfs_rq。这个 se "伪装"成普通进程参与父组的调度。收到 CPU 时间后,再在自己的 cfs_rq 中按组内进程的权重二次分配。
调试与观测
# 进程的调度统计 (最关键的文件)
# 内容: se.sum_exec_runtime, se.vruntime, se.statistics.wait_sum,
# nr_switches, nr_voluntary_switches, nr_involuntary_switches
# 系统级调度器调试信息
# 含: 每 CPU 的 cfs_rq 统计, nr_running, min_vruntime, 红黑树大小
# ftrace: 抓调度事件
|
# bpftrace: 按进程统计调度延迟
# 查看 EEVDF 是否启用
# Kernel cmdline: 调试用开关
# sched_verbose — 更多 dmesg 信息
# skew_tick=1 — 让各 CPU tick 偏移,减少锁竞争
参考与延伸
- 内核文档:
Documentation/scheduler/目录,尤其是sched-design-CFS.rst和sched-eevdf.rst - LWN:
- "EEVDF scheduler design" 系列 (2023, lwn.net/Articles/925371/)
- "The EEVDF scheduler" (lwn.net/Articles/940176/)
- "CFS group scheduling" (lwn.net/Articles/240474/)
- "The multi-queue block and CPU schedulers" (lwn.net/Articles/552451/)
- 源码文件:
kernel/sched/fair.c— CFS/EEVDF (~8000行,调度器主体)kernel/sched/core.c— schedule(), try_to_wake_up() 等通用接口kernel/sched/sched.h— struct rq, struct cfs_rq, sched_class 定义kernel/sched/topology.c— 调度域构建kernel/sched/debug.c— /proc/sched_debug 实现
- 经典论文:
- "EEVDF: Earliest Eligible Virtual Deadline First" (Stoica & Abdel-Wahab, 1995)
- "CFS: Completely Fair Process Scheduling in Linux" (MolNar, 2007 LWN)
关键词: CFS, EEVDF, vruntime, sched_entity, cfs_rq, wakeup preemption, load_balance, sched_domain, cgroup scheduling, vlag