这一关回答 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 将回答"选出来之后,换过去要花多少钱"(上下文切换)。
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 越近:"权重"被翻译成了"时间承诺"。
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 都是从它算出来的。
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 会算这笔账)。
__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() 快路径,跳过遍历。
唤醒路径 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 时代"睡眠奖励"的修正。
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 更是停机迁移用的特权通道。硬分层是"正确性优先于优雅"。
struct sched_entity se 就嵌在 task_struct 里——调度器"选 task",EEVDF"排 se",是同一个东西的两面;M01 说"调度器只认 task",这一关是这句话的展开。出题原则:10 题中记忆性题目不超过 3 道,其余为应用/推理/权衡类——场景预测、设计 trade-off 比较、从代码路径找问题、解释"为什么这样设计而不是那样"。难度递进,最后两道需要想一会儿。
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 最早的。
stop > dl > rt > fair > idle。__pick_next_task()(kernel/sched/core.c:6153)用 for_each_active_class 从 __sched_class_highest 到 __sched_class_lowest 遍历,第一个有可运行任务的类赢。"公平"只在 fair 类内部谈,类之间是硬优先级——实时任务永远先跑。
两处用:一是 deadline 的输入(vd = ve + r/w),二是 eligibility 剪枝(增强红黑树用 min_vruntime 做堆,O(log n) 跳过没资格的子树)。没被删掉是因为 deadline 和 lag 都从它算出来——vruntime 是"亏欠"的记账本,删了它,eligibility 和 deadline 都成无源之水。fair.c 里 142 处引用就是证据。
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 更近,排得更靠前。"权重"被翻译成了"时间承诺":权重大的任务,内核承诺它更早跑上。
说明 CFS 时代"固定调度周期"的概念已死。CFS 用 sched_latency(默认 6ms)定义"一轮多长";EEVDF 改用 sysctl_sched_base_slice(0.7ms)作为每个任务的"请求时间"基准。一个概念死了,看它的 sysctl 还在不在,就是铁证——挑选逻辑从"周期内分时间"变成了"按 deadline 排队"。
调大:每轮预支的虚拟时间多,deadline 拉开——任务跑得久、切换少,吞吐高,但交互任务要等更久,延迟差。调小:deadline 排得密,轮转快,延迟好,但上下文切换暴增——而每次切换的代价(M07:switch_mm、CR3、TLB)在那里摆着,切换本身吃掉 CPU。base_slice 是"延迟 vs 吞吐"的旋钮,0.7ms 是兼顾两者的经验值。
PREEMPT_WAKEUP_PICK(fair.c:9838)= "Let pick_eevdf() decide"——把抢占决策交给 EEVDF 本人:按 deadline 重新算一遍,而不是用启发式规则。什么时候不抢占?PREEMPT_WAKEUP_NONE——比如刚睡醒的任务 lag 还不够负(没资格),或者当前任务的 deadline 仍然更早。抢占不是"醒了就抢",是"算过之后才抢"。
凭 lag 为负:睡觉期间没消耗 CPU,lag = w*(V-v) 为负——确实"被亏欠"了,有 eligibility 资格。但 placement 策略(fair.c:6206)把它放在 min_vruntime 附近起算,而不是保留睡眠前的 vruntime——防的作弊是"睡觉攒亏欠,醒来霸占 CPU"。睡觉不能无限攒 lag,这是 EEVDF 对 CFS 时代"睡眠奖励"的修正。
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——中间隔着一次完整的重调度。
(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 排队"。
在页面里每题写下你的答案(关键词即可),卡住的题点一下"卡住了"标记,
点"生成作答汇总"后复制、贴到聊天里——对错由我来判定,我逐题批改并据此
调整下一关的深浅;准备好了说"下一关"。
点下面按钮生成汇总文本,复制后贴到「内核二十关」侧聊里,对错由我判定。