审校基线:本页引用的内核符号、行号与源码链接以 Linux 7.3-rc4(torvalds/linux master,2026-09-26 实测)为基准;审校当日 master 的确切 commit 未记录,后续版本可能已有漂移。
CFS 的挑选算法已经退役——6.6 起 fair 类里跑的是 EEVDF:挑选分两步,先看"有没有资格"(被亏欠者),再在有资格里选虚拟 deadline 最早的。
2026-09-26 · Scheduler, EEVDF and vruntime

第 6 关 · 调度器:EEVDF 与 vruntime

定位

这一关回答 CPU 的终极问题:下一个时间片给谁。答案很激进:CFS 的挑选算法已经退役了——6.6 起,fair 调度类里跑的是 EEVDF(Earliest Eligible Virtual Deadline First)。挑选分两步:先看"有没有资格"(eligible:被亏欠服务的才有资格),再在有资格的里面选"虚拟 deadline 最早"的。vruntime 没有死——它变成了 deadline 的输入,是"亏欠"的记账本。

这一关的三个机制支点:sched_entity 的三元组(vruntime/deadline/min_vruntime,include/linux/sched.h)→ pick_eevdf() 的增强红黑树(按 deadline 排序、按 vruntime 剪枝,kernel/sched/fair.c)→ 调度类层级 stop>dl>rt>fair>idle(__pick_next_task(),kernel/sched/core.c)。M01 的 task_struct 就是调度实体(se 嵌在里面);M07 将回答"选出来之后,换过去要花多少钱"(上下文切换)。

核心机制深讲

6.1 EEVDF 的两条挑选标准

pick_eevdf()(kernel/sched/fair.c:1187)头顶的注释就是全部考纲(fair.c:1172):"1) the task must be eligible (must be owed service); 2) from those tasks that meet 1), we select the one with the earliest virtual deadline."——先看有没有资格,再看谁的虚拟 deadline 最早。

eligible 的意思是"被亏欠服务":用 lag 衡量,lag_i = w_i * (V - v_i)——拿到的比应得的少(lag 为负)才有资格。这一步是刹车:防止一个任务不断"预支"未来时间,deadline 永远最早、永远赖在 CPU 上。第二步才是油门:有资格者里,deadline 最早的先跑。公式写在 fair.c:1303 的注释里:vd_i = ve_i + r_i / w_i,代码形态是 se->deadline = se->vruntime + calc_delta_fair(se->slice, se)——权重 w 越大,deadline 越近:"权重"被翻译成了"时间承诺"。

6.2 vruntime 没死:它变成了 deadline 的输入

struct sched_entity(include/linux/sched.h)里躺着三元组:deadline(577)、min_vruntime(578)、vruntime(592)。cfs_rq 的红黑树是增强树:按 deadline 排序,同时是按 vruntime 的堆——se->min_vruntime = min(se->vruntime, se->{left,right}->min_vruntime),让 eligibility 检查可以剪枝,挑选保持 O(log n)。

铁证:fair.c 里 vruntime 还有 142 处引用。CFS 的遗产是地基,不是废墟——EEVDF 没有扔掉 vruntime,只是把它从"排序键"降格为"输入":deadline 和 lag 都是从它算出来的。

6.3 base_slice:0.7ms 的新节拍

sysctl_sched_base_slice = 700000ULL(kernel/sched/fair.c:85)——0.7ms,这是 EEVDF 公式里"请求时间" r_i 的基准:每个任务每轮默认"预支" 0.7ms 的虚拟时间再算 deadline。

更有意思的是消失的东西:CFS 时代的 sched_latency 在 fair.c 里已经 grep 不到了(0 处)。"6ms 调度周期"的概念死了,被 base slice 取代——一个概念死了,看它的 sysctl 还在不在,就是铁证。base_slice 是可调旋钮:调大,吞吐高但交互延迟差;调小,延迟好但上下文切换多(M07 会算这笔账)。

6.4 调度类层级:fair 只是第四顺位

__pick_next_task()(kernel/sched/core.c:6153)用 for_each_active_class 从高到低遍历调度类,第一个有可运行任务的类赢。五个类按 DEFINE_SCHED_CLASS 的优先级排列:stop(停机/迁移)> dl(SCHED_DEADLINE,真实时间 deadline)> rt(FIFO/RR)> fair(EEVDF)> idle——定义分别在 stop_task.c、deadline.c、rt.c、fair.c、idle.c。

含义很冷:"公平"只在 fair 类内部谈,类之间是硬优先级。实时任务永远先跑——你的 EEVDF 调得再漂亮,也排在一个 SCHED_FIFO 任务后面。还有个优化:全是 fair 任务时直接走 pick_task_fair() 快路径,跳过遍历。

6.5 唤醒抢占:睡醒的凭什么抢

唤醒路径 wakeup_preempt_fair()(kernel/sched/fair.c:9910)返回四档决策(fair.c:9836):PREEMPT_WAKEUP_NONE(不抢占)、SHORT(忽略 slice 保护)、PICK("Let pick_eevdf() decide")、RESCHED(强制重调度)。

刚睡醒的任务 vruntime 怎么放?placement 策略(fair.c:6206 注释 "placement strategy #1/#2"):用 lag_i = w_i * (V - v_i)、vl_i = V - v_i 把新任务放在 min_vruntime 附近——防止"睡一觉攒 vruntime,醒来霸占 CPU"的作弊。睡觉不能攒亏欠,这是 EEVDF 对 CFS 时代"睡眠奖励"的修正。

6.6 为什么 deadline 比 vruntime 更能给延迟保证

CFS 按 vruntime 排序:保证"长期公平",但一个权重小的任务可能要等很久才轮到——延迟无上限。EEVDF 的 deadline 是"你最晚什么时候能跑上"的承诺;eligibility 保证"承诺不会被超前消费透支"。lag 有界 → 延迟有界——这正是 EEVDF 论文的核心结论,也是它替换 CFS 挑选算法的理由。

一句话总结两代算法:CFS 回答"谁最亏",EEVDF 回答"谁最等不起"——而"等不起"的前提,是先证明"你确实亏了"(eligible)。

为什么这样设计

为什么挑选要"先资格、后 deadline"两步,而不是直接按 deadline 排?

因为光按 deadline,一个任务可以不断"预支"未来时间——deadline 永远最早,永远赖在 CPU 上。eligibility 是"不许超前消费"的刹车:lag 为负(确实被亏欠了)才有资格参选,超前了就先靠边。两步缺一不可:没 deadline,延迟无保证;没 eligibility,公平被预支掏空。

为什么 vruntime 不删掉,留着当"遗产"?

因为 deadline 和 lag 都是从 vruntime 算出来的(vd = ve + r/w,lag = w*(V-v))。vruntime 是"亏欠"的记账本,删了它,eligibility 和 deadline 都成了无源之水。EEVDF 的聪明之处恰恰在于:没重写记账体系,只换了"怎么花账本"的挑选逻辑——142 处引用就是迁移成本最小的证据。

为什么调度类之间是硬优先级,而不是全扔进 EEVDF 统一排?

因为 dl/rt 的 deadline 是真实时间的 deadline(错过就出事),fair 的是虚拟时间的 deadline(错过只是慢)。两种"时间"不能混排——虚拟时间的"早"和真实时间的"早"不是一个量纲。stop 更是停机迁移用的特权通道。硬分层是"正确性优先于优雅"。

与已学模块的关联

自测

出题原则:10 题中记忆性题目不超过 3 道,其余为应用/推理/权衡类——场景预测、设计 trade-off 比较、从代码路径找问题、解释"为什么这样设计而不是那样"。难度递进,最后两道需要想一会儿。

Q1. EEVDF 挑选的两条标准是什么?(引用源码注释原文)

kernel/sched/fair.c:1172 注释原文:"1) the task must be eligible (must be owed service); 2) from those tasks that meet 1), we select the one with the earliest virtual deadline."——先看有没有资格(被亏欠服务),再在有资格里选虚拟 deadline 最早的。

Q2. 五个调度类的优先级顺序是什么?__pick_next_task 按什么顺序遍历?

stop > dl > rt > fair > idle。__pick_next_task()(kernel/sched/core.c:6153)用 for_each_active_class 从 __sched_class_highest 到 __sched_class_lowest 遍历,第一个有可运行任务的类赢。"公平"只在 fair 类内部谈,类之间是硬优先级——实时任务永远先跑。

Q3. vruntime 在 EEVDF 里还剩什么用?它为什么没被删掉?

两处用:一是 deadline 的输入(vd = ve + r/w),二是 eligibility 剪枝(增强红黑树用 min_vruntime 做堆,O(log n) 跳过没资格的子树)。没被删掉是因为 deadline 和 lag 都从它算出来——vruntime 是"亏欠"的记账本,删了它,eligibility 和 deadline 都成无源之水。fair.c 里 142 处引用就是证据。

Q4. 写出 deadline 公式。权重翻倍,deadline 怎么变?

vd_i = ve_i + r_i / w_i(fair.c:1303 注释),代码:se->deadline = se->vruntime + calc_delta_fair(se->slice, se)。权重 w 翻倍,同样 vruntime 下 r/w 项减半——deadline 更近,排得更靠前。"权重"被翻译成了"时间承诺":权重大的任务,内核承诺它更早跑上。

Q5. sched_latency 在 fair.c 里 grep 不到了(0 处),说明什么?

说明 CFS 时代"固定调度周期"的概念已死。CFS 用 sched_latency(默认 6ms)定义"一轮多长";EEVDF 改用 sysctl_sched_base_slice(0.7ms)作为每个任务的"请求时间"基准。一个概念死了,看它的 sysctl 还在不在,就是铁证——挑选逻辑从"周期内分时间"变成了"按 deadline 排队"。

Q6. sysctl_sched_base_slice 调大 / 调小,各自的代价是什么?

调大:每轮预支的虚拟时间多,deadline 拉开——任务跑得久、切换少,吞吐高,但交互任务要等更久,延迟差。调小:deadline 排得密,轮转快,延迟好,但上下文切换暴增——而每次切换的代价(M07:switch_mm、CR3、TLB)在那里摆着,切换本身吃掉 CPU。base_slice 是"延迟 vs 吞吐"的旋钮,0.7ms 是兼顾两者的经验值。

Q7. PREEMPT_WAKEUP_PICK 是什么意思?什么时候唤醒不抢占?

PREEMPT_WAKEUP_PICK(fair.c:9838)= "Let pick_eevdf() decide"——把抢占决策交给 EEVDF 本人:按 deadline 重新算一遍,而不是用启发式规则。什么时候不抢占?PREEMPT_WAKEUP_NONE——比如刚睡醒的任务 lag 还不够负(没资格),或者当前任务的 deadline 仍然更早。抢占不是"醒了就抢",是"算过之后才抢"。

Q8. 一个刚睡醒的交互任务和一个一直跑的计算任务,凭什么睡醒的能抢?placement 策略防的是什么作弊?

凭 lag 为负:睡觉期间没消耗 CPU,lag = w*(V-v) 为负——确实"被亏欠"了,有 eligibility 资格。但 placement 策略(fair.c:6206)把它放在 min_vruntime 附近起算,而不是保留睡眠前的 vruntime——防的作弊是"睡觉攒亏欠,醒来霸占 CPU"。睡觉不能无限攒 lag,这是 EEVDF 对 CFS 时代"睡眠奖励"的修正。

Q9. 从 try_to_wake_up 到 pick_eevdf,写出唤醒抢占的调用链。

try_to_wake_up() → wakeup_preempt()(调度类钩子)→ wakeup_preempt_fair()(kernel/sched/fair.c:9910,返回 PREEMPT_WAKEUP_* 决策)→ resched_curr() → schedule() → __pick_next_task()(kernel/sched/core.c:6153,遍历调度类)→ pick_task_fair() → pick_eevdf()(kernel/sched/fair.c:1187)。唤醒只负责"提名",挑选只认 deadline——中间隔着一次完整的重调度。

Q10. 设计题:音频播放任务偶发卡顿。从 EEVDF 角度列出排查清单(至少 4 个检查点,并说明每个背后的机制)。

(1) cgroup 权重被挤占:同组里起了大权重任务 → 音频任务的 r/w 相对变大 → deadline 被推后。查 cpu.weight。(2) base_slice 被调小:deadline 排得密,音频任务频繁被抢占 → 每次抢占的上下文切换代价(M07)在 0.7ms 尺度下被放大。(3) eligibility 长期不满足:任务的 lag 一直没机会转负——说明它前面总有"更亏欠"的任务,可能是个 CPU 密集型邻居在吸时间。(4) 被 dl/rt 类插队:类间是硬优先级,一个 SCHED_FIFO 的实时任务可以直接让 fair 类全员等待——查 chrt 和 rt throttling。(5) 跨 NUMA/亲和性:任务被绑在远端 CPU,wakee 迁移的 WF_RQ_SELECTED 路径让抢占阈值变化。排查顺序:先类(4),再权重(1),再 slice(2),最后看 lag(3)。

延伸阅读

cfs_rq 红黑树(按 deadline 排序)
  │
  ├─ ① eligibility 过滤:lag < 0?(被亏欠的才有资格)
  │     └─ min_vruntime 堆做剪枝,O(log n) 跳过没资格的子树
  │
  └─ ② 有资格者里选 deadline 最早的
            │
            ▼
   deadline = vruntime + slice / weight
            (权重越大,承诺越早)

两步缺一不可:没 deadline,延迟无保证;没 eligibility,公平被"预支"掏空。CFS 只回答"谁最亏",EEVDF 回答"谁最等不起"——而"等不起"的前提是先证明"你确实亏了"。

  ┌───────────┐
  │   stop    │  停机 / CPU 迁移,特权通道
  ├───────────┤
  │    dl     │  SCHED_DEADLINE:真实时间的 deadline
  ├───────────┤
  │    rt     │  SCHED_FIFO/RR:99 个优先级
  ├───────────┤
  │   fair    │  EEVDF:eligible + 最早虚拟 deadline
  ├───────────┤
  │   idle    │  没人可跑时的 idle 任务
  └───────────┘
  __pick_next_task 自上而下,第一个命中的赢

"公平"只在 fair 类内部谈,类之间是硬优先级。你的 EEVDF 调得再漂亮,也排在一个 SCHED_FIFO 任务后面——dl/rt 的 deadline 是真实时间的 deadline,和 fair 的虚拟时间不是一个量纲,不能混排。

vruntime(亏欠记账本,142 处引用,没死)
   │  + slice / weight
   ▼
deadline(时间承诺:你最晚什么时候跑上)
   │  lag = w·(V − v) < 0 才有资格(不许超前消费)
   ▼
pick_eevdf:有资格者里最早的 deadline
   │  base_slice = 0.7ms(每轮预支的虚拟时间)
   ▼
下一时间片的归属

CFS 时代的 sched_latency 在 fair.c 里已经 grep 不到了——一个概念死了,看它的 sysctl 还在不在,就是铁证。"6ms 周期"被 0.7ms 的 base slice 取代:挑选逻辑从"周期内分时间"变成了"按 deadline 排队"。

在页面里每题写下你的答案(关键词即可),卡住的题点一下"卡住了"标记,
点"生成作答汇总"后复制、贴到聊天里——对错由我来判定,我逐题批改并据此
调整下一关的深浅;准备好了说"下一关"。

作答汇总

点下面按钮生成汇总文本,复制后贴到「内核二十关」侧聊里,对错由我判定。