Linux 6.6(2023 年 10 月)做了一件载入内核史册的事:服役 16 年的 CFS 核心调度逻辑被 EEVDF 取代。本文不重复教科书概念,而是用一个 150 行的 Python 模拟器完整复现两个调度器的决策过程,用真实运行数据回答:EEVDF 到底解决了 CFS 的什么问题?

一、从 CFS 到 EEVDF:为什么要换

CFS(Completely Fair Scheduler)自 2.6.23(2007)起成为 Linux 默认调度器,核心思想是维护每个任务的虚拟运行时间 vruntime,每次选择 vruntime 最小的任务运行,长期看 CPU 时间按权重精确分配。但 CFS 有一个结构性缺陷:它只回答『谁最吃亏』,不回答『谁最等不起』。

交互式任务(如 GUI 线程、音频渲染)的特点是频繁睡眠、短时突发运行。CFS 用启发式的唤醒抢占规则(wakeup_granularity、place_entity 的 sleeper bonus)去补偿这类任务,补丁摞补丁,边界 case 层出不穷——音频毛刺、游戏掉帧的调度侧归因常年出现在内核邮件列表。EEVDF(Earliest Eligible Virtual Deadline First)把『延迟』从启发式补丁变成了一等公民:每个任务不仅有虚拟截止时间,调度器只在『已到期(eligible)』的任务中,选虚拟截止时间最早者运行。

二、CFS 核心机制:vruntime 与时间片

CFS 的三个关键公式:

nice 值到权重的映射查自内核数组 sched_prio_to_weight:nice 每降 1,权重约放大 1.25 倍,nice=-5 的权重 3121 约为 nice=0(1024)的 3 倍。

三、EEVDF 核心机制:合格时间与虚拟截止

EEVDF 为每个任务维护两个量:

调度决策分两步:先在 eligible 集合中筛选,再取 vd 最小者。全局 V 按 V += Δexec × NICE_0_LOAD / Σweight 推进。这一设计的美妙之处在于:睡眠后醒来的任务 ve 已落后于 V,立即可运行;而它此前的睡眠时间不计入服务,于是 vd 往往最早——延迟敏感任务的优待不再需要启发式补丁,而是算法的自然推论。EEVDF 还顺便消灭了 CFS 的时间片不均问题:所有任务统一使用基础时间片(内核默认 3ms 量级),权重差异通过被选中频率体现,而非单次运行时长。

四、完整可运行模拟器(Python 3,约 150 行)

下面的程序实现了两个调度器的核心决策逻辑,用 4 个任务(交互型 nice=0 短突发、普通型 nice=0、批处理 nice=+5、高优先级 nice=-5)做对照实验,输出完整调度序列、每个任务的首次运行/完成时间和长期 CPU 占比:

#!/usr/bin/env python3
"""CFS 与 EEVDF 调度器模拟对比:公平性与交互响应延迟"""

NICE_0_LOAD = 1024
# 内核 sched_prio_to_weight[](nice -20..+19,节选核心思路:
# nice 每变化 1 级,权重约乘/除 1.25)
PRIO_TO_WEIGHT = [
    88761, 71755, 56483, 46273, 36291,
    29154, 23254, 18705, 14949, 11916,
     9548,  7620,  6100,  4904,  3906,
     3121,  2501,  1991,  1586,  1277,
     1024,   820,   655,   526,   423,
      335,   272,   215,   172,   137,
      110,    87,    70,    56,    45,
       36,    29,    23,    18,    15,
]

def nice_to_weight(nice):
    return PRIO_TO_WEIGHT[nice + 20]

class Task:
    def __init__(self, name, nice, burst):
        self.name, self.nice, self.burst = name, nice, float(burst)
        self.weight = nice_to_weight(nice)
        self.exec_done = 0.0
        self.vruntime = 0.0     # CFS 用
        self.ve = 0.0           # EEVDF: virtual eligible time
        self.vd = 0.0           # EEVDF: virtual deadline
        self.first_run = None
        self.finish = None

    def delta_v(self, dt):
        """物理时间 dt 折算为该任务的虚拟时间增量"""
        return dt * NICE_0_LOAD / self.weight

# ---------------- CFS ----------------
def simulate_cfs(spec, sched_latency=24.0, min_gran=3.0):
    tasks = [Task(*s) for s in spec]
    runnable = list(tasks)
    now, seq = 0.0, []
    nr_latency = int(sched_latency // min_gran)
    while runnable:
        n = len(runnable)
        period = sched_latency if n <= nr_latency else n * min_gran
        total_w = sum(t.weight for t in runnable)
        t = min(runnable, key=lambda x: (x.vruntime, x.name))  # 红黑树最左节点
        slice_t = max(min_gran, period * t.weight / total_w)
        run = min(slice_t, t.burst - t.exec_done)
        if t.first_run is None:
            t.first_run = now
        now += run
        t.exec_done += run
        t.vruntime += t.delta_v(run)
        seq.append((t.name, round(run, 2)))
        if t.exec_done >= t.burst - 1e-9:
            t.finish = now
            runnable.remove(t)
    return tasks, seq

# ---------------- EEVDF ----------------
def simulate_eevdf(spec, base_slice=3.0):
    tasks = [Task(*s) for s in spec]
    for t in tasks:
        t.vd = t.delta_v(base_slice)   # 初始截止时间
    runnable = list(tasks)
    V, now, seq = 0.0, 0.0, []         # V: 全局虚拟时间
    while runnable:
        total_w = sum(t.weight for t in runnable)
        eligible = [t for t in runnable if V - t.ve >= -1e-9]
        if not eligible:               # 无合格任务:快进 V
            V = min(t.ve for t in runnable)
            eligible = [t for t in runnable if V - t.ve >= -1e-9]
        t = min(eligible, key=lambda x: (x.vd, x.name))  # 截止最早者
        run = min(base_slice, t.burst - t.exec_done)
        if t.first_run is None:
            t.first_run = now
        now += run
        t.exec_done += run
        t.ve += t.delta_v(run)
        V += run * NICE_0_LOAD / total_w
        if t.exec_done >= t.burst - 1e-9:
            t.finish = now
            runnable.remove(t)
        else:
            t.vd = t.ve + t.delta_v(base_slice)  # 续发一个请求
        seq.append((t.name, round(run, 2)))
    return tasks, seq

SPEC = [('A-interactive', 0, 12), ('B-normal', 0, 120),
        ('C-batch', 5, 120), ('D-hiprio', -5, 120)]

def report(tag, tasks, seq):
    print('--- ' + tag + ' ---')
    print('schedule(first 16):',
          ' '.join('%sx%g' % (n, d) for n, d in seq[:16]))
    for t in tasks:
        print('  %s: nice=%+d weight=%d first_run=%.1fms finish=%.1fms'
              % (t.name, t.nice, t.weight, t.first_run, t.finish))
    long_tasks = [t for t in tasks if not t.name.startswith('A')]
    tot = sum(t.exec_done for t in long_tasks)
    print('  long-task CPU share:',
          ', '.join('%s=%.1f%%' % (t.name.split('-')[0],
                    t.exec_done / tot * 100) for t in long_tasks))
    print()

cfs_tasks, cfs_seq = simulate_cfs(SPEC)
report('CFS simulation', cfs_tasks, cfs_seq)
ee_tasks, ee_seq = simulate_eevdf(SPEC)
report('EEVDF simulation', ee_tasks, ee_seq)
a_cfs = [t for t in cfs_tasks if t.name.startswith('A')][0]
a_ee  = [t for t in ee_tasks  if t.name.startswith('A')][0]
d_cfs = [t for t in cfs_tasks if t.name.startswith('D')][0]
d_ee  = [t for t in ee_tasks  if t.name.startswith('D')][0]
print('interactive A finish: CFS=%.1fms  EEVDF=%.1fms'
      % (a_cfs.finish, a_ee.finish))
print('hiprio D finish:      CFS=%.1fms  EEVDF=%.1fms'
      % (d_cfs.finish, d_ee.finish))

五、真实运行输出与解读

以上代码在 Python 3 下原样运行(无任何依赖),输出如下:

--- CFS simulation ---
schedule(first 16): A-interactivex4.47 B-normalx4.47 C-batchx3 D-hipriox13.61
                    A-interactivex4.47 B-normalx4.47 D-hipriox13.61 ...
  A-interactive: nice=+0 weight=1024 first_run=0.0ms finish=51.1ms
  B-normal:      nice=+0 weight=1024 first_run=4.5ms finish=293.6ms
  C-batch:       nice=+5 weight=335  first_run=8.9ms finish=372.0ms
  D-hiprio:      nice=-5 weight=3121 first_run=11.9ms finish=185.8ms
  long-task CPU share: B=33.3%, C=33.3%, D=33.3%

--- EEVDF simulation ---
schedule(first 16): D-hipriox3 A-interactivex3 D-hipriox3 B-normalx3 D-hipriox3
                    C-batchx3 D-hipriox3 A-interactivex3 D-hipriox3 ...
  A-interactive: nice=+0 weight=1024 first_run=3.0ms finish=54.0ms
  B-normal:      nice=+0 weight=1024 first_run=9.0ms finish=291.0ms
  C-batch:       nice=+5 weight=335  first_run=15.0ms finish=372.0ms
  D-hiprio:      nice=-5 weight=3121 first_run=0.0ms finish=183.0ms
  long-task CPU share: B=33.3%, C=33.3%, D=33.3%

interactive A finish: CFS=51.1ms  EEVDF=54.0ms
hiprio D finish:      CFS=185.8ms  EEVDF=183.0ms

这组数据值得逐条咀嚼:

六、理论分析

6.1 公平性的形式化

CFS 的公平性可表述为:任意时间区间 [t1, t2] 内,任意两个可运行任务 i, j 的加权服务时间差有界。EEVDF 继承自 Stoica 等人 1997 年的 EEVDF 原始论文,证明了更强的延迟性质:对于请求大小为 r 的任务,从请求发起到开始服务的延迟不超过一个与系统总权重相关的常数上界,且与任务权重无关——小任务不会被大任务的权重挤出时间线。

6.2 lag 守恒与 place_entity 的简化

CFS 中任务睡眠后 vruntime 冻结,醒来时与 min_vruntime 拉开差距,内核用 place_entity() 做复杂的 vruntime 补偿(这正是 sleeper bonus 的来源)。EEVDF 把『欠账』显式建模为 lag:lag > 0 表示欠服务、lag < 0 表示透支。ve 的定义直接内嵌了 lag 的结算,唤醒路径不再需要特判。内核 6.6 的提交(commit 147f3efaa241 等)显示,EEVDF 化之后 fair.c 净删掉了数百行启发式代码。

6.3 为什么截止时间能表达延迟需求

请求驱动(request-driven)模型下,任务每次需要 CPU 时声明一个请求;vd 就是『按当前负载,这个请求应该完成的最晚时刻』。选择 vd 最早者运行等价于在所有已到期请求中做 EDF,而 EDF 在单机可调度性上是最优的。EEVDF 相当于把实时调度里最锋利的武器(EDF)嫁接到公平共享调度上,这是它理论上比 CFS 优雅的根本原因。

七、工程应用

八、常见误区

九、参考文献

  1. I. Stoica, H. Abdel-Wahab, et al. (1997). Earliest Eligible Virtual Deadline First: A Flexible and Accurate Mechanism for Proportional Share Resource Allocation. OGI Technical Report.
  2. Peter Zijlstra (2023). EEVDF scheduler merge for Linux 6.6, kernel commits 147f3efaa241 及相关系列,kernel/sched/fair.c。
  3. J. Lozi et al. (2016). The Linux Scheduler: a Decade of Wasted Cores. EuroSys '16.
  4. Linux kernel documentation: Scheduler Design — CFS(Documentation/scheduler/sched-design-CFS.rst)与 sched-nice-design.rst。
  5. Wolfgang Mauerer 著,郭旭译. 专业 Linux 内核架构(Professional Linux Kernel Architecture)第 2 章:进程管理与调度。
← 返回文章列表