一个请求的数据已经准备好,处理线程也被唤醒了,却迟迟没有响应。此时需要沿调度器的几个判断继续追踪:在哪运行、能分多少 CPU、这一刻轮到谁 。
你观察到的现象 阅读位置
线程已唤醒,却没立即执行 第 1 节:运行资格与重新调度
nice 不同,CPU 时间怎样分? 第 2~3 节:权重与 EEVDF 推演
主机还有空闲,容器却在排队 第 5 节:组权重与 quota
多核负载不均,迁移却未必更快 第 6~7 节:放置与观察
本文以普通公平调度为主。Linux 从 6.6 开始引入 EEVDF;数值模型用于理解算法,实现细节以本机 Linux 7.2 为对照。
1. 运行队列回答“谁现在有机会运行”
Linux 主要按 CPU 维护运行队列。当前任务和等待执行的任务都可能是 TASK_RUNNING,但睡在 I/O 或条件等待中的任务不会因为优先级高就继续消耗 CPU。唤醒路径更新任务状态、选择合适的 CPU 并入队;如果新任务值得抢占当前任务,会触发重新调度请求。
重新调度请求不是任意指令点上的强制跳转。内核要在相应可调度位置检查请求;中断、抢占关闭、锁约束和内核抢占模型都会影响交接时机。调度器即使运行了一次,也可能仍选中当前任务,因此“执行 schedule”与“发生一次 prev != next 的上下文切换”也不能画等号。
每 CPU 队列减少了全局争用,但不代表调度完全是本地操作。唤醒远端任务、拉取负载、迁移任务需要协调多个 CPU 和队列锁。CPU 亲和性、cpuset、生效中的调度策略先限定候选范围;负载均衡只能在允许的范围内改善分配,不能为了看起来均匀就绕过隔离设置。
2. 公平的计量:把实际时间换算成虚拟服务
先看一个只有 A、B 的理想模型:两者持续可运行,权重为 2:1,共享一颗 CPU 的 9 ms 时间。按权重分配,A 得到 6 ms,B 得到 3 ms。
实际时间不同,归一化后的服务进度相同
A:权重 2
6 ms
÷ 权重
3
B:权重 1
3 ms
÷ 权重
3
实际 CPU 时间
虚拟增量
单 CPU、两任务持续可运行、理想份额;基准权重归一化为 1。总实际时间为 9 ms。
两者实际时间不同,但除以各自权重后,都前进了 3 个虚拟单位。权重大者的虚拟时钟走得慢,需要运行更久才能追上相同的进度。
常用表达为:
Δ v i = Δ t i w i w 0
其中 w 0 是基准权重。
例如 nice 0 的基础权重为 1024,nice 5 为 335。若只有它们在同一公平调度层级、同一 CPU 上持续竞争,前者的理想份额约为 1024 / (1024 + 335) = 75.35%。
这个比例随竞争集合变化:对方阻塞后,前者可以使用更多空闲时间;加入新任务或 cgroup 层级后,需要重新计算份额。nice 表达相对权重,不是固定 CPU 百分比。
下面把权重画成宽度、实际时间画成面积。调节分配和权重,高度便显示谁得到了更多归一化服务。
正在呈现知识画面 重新加载
相同的虚拟进度,需要多少 CPU 时间? 虚拟运行增量等于实际时间乘基准权重、再除以任务权重。图用宽度表示权重、高度表示归一化服务、面积表示实际 CPU 时间。
{"id":"scheduler-virtual-service","title":"相同的虚拟进度,需要多少 CPU 时间?","summary":"虚拟运行增量等于实际时间乘基准权重、再除以任务权重。图用宽度表示权重、高度表示归一化服务、面积表示实际 CPU 时间。","height":1000,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"controls\"><div class=\"control\"><label for=\"weight\" data-i18n=\"weight\"></label><output id=\"weight-value\" for=\"weight\"></output><input id=\"weight\" type=\"range\" min=\"1\" max=\"4\" step=\"1\" value=\"2\"></div><div class=\"control\"><label for=\"service\" data-i18n=\"service\"></label><output id=\"service-value\" for=\"service\"></output><input id=\"service\" type=\"range\" min=\"0\" max=\"9\" step=\"0.05\" value=\"6\"></div></div><div class=\"service-plot\" role=\"img\" data-i18n-label=\"chart\"><span class=\"axis-top\">Δv = 9</span><span class=\"axis-bottom\">0</span><div class=\"service-areas\"><div id=\"area-a\" class=\"service-area\"><b>A</b></div><div id=\"area-b\" class=\"service-area other\"><b>B</b></div><i id=\"level-a\"></i><i id=\"level-b\"></i></div></div><p class=\"caption\" data-i18n=\"geometry\"></p><div class=\"metrics\"><div><span data-i18n=\"clockA\"></span><strong id=\"virtual-a\"></strong></div><div><span data-i18n=\"clockB\"></span><strong id=\"virtual-b\"></strong></div></div><div class=\"formula\"><div id=\"formula-a\"></div><div id=\"formula-b\"></div></div><div class=\"actions\"><button id=\"balance\" data-i18n=\"balance\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"equal\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"aheadA\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"aheadB\"></p></div><details class=\"scope\"><summary data-i18n=\"scope\"></summary><p class=\"note\" data-i18n=\"note\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.note,.caption{font-size:12px;color:var(--muted);margin-top:14px}.controls{display:grid;gap:12px;margin:18px 0}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center;font-size:13px}.control input{grid-column:1/3;width:100%;margin:0}.control output{color:var(--accent);font-variant-numeric:tabular-nums}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{flex:1;min-width:0;min-height:42px;font-size:13px}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0}.metrics>div{min-width:0;border-left:2px solid var(--accent);padding-left:12px}.metrics span{display:block;color:var(--muted);font-size:12px;min-height:2.8em}.metrics strong{display:block;font-size:22px;font-weight:500;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.formula{font-size:13px;overflow-wrap:anywhere;border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-variant-numeric:tabular-nums}.formula div+div{margin-top:6px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.card{min-width:0;border:1px solid var(--rule);border-radius:9px;padding:14px}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card code{font-size:12px;overflow-wrap:anywhere}.card .value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums}.card small{display:block;color:var(--muted);font-size:12px}.card button{width:100%;min-width:0;font-size:13px;min-height:42px}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{accent-color:var(--accent);width:16px;height:16px;flex-shrink:0}.pill{display:inline-block;padding:3px 8px;border-radius:5px;background:var(--accent-soft);color:var(--accent);font-size:12px}.plot{position:relative;margin:18px 0}.plot svg{width:100%;height:auto;overflow:visible}.plot text{font-size:12px;fill:var(--muted)}.plot .ink{fill:var(--ink)}.plot .blue{fill:var(--accent);stroke:var(--accent)}.plot .gold{fill:var(--second);stroke:var(--second)}.plot .area{fill:var(--accent-soft);stroke:var(--accent);transition:all .25s}.plot .other{fill:var(--second-soft);stroke:var(--second)}.plot line,.plot path,.plot rect,.plot circle{transition:all .25s}.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:12px;color:var(--muted);margin-top:10px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}.legend .gold:before{background:var(--second)}details{margin-top:14px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:8px 0}button:disabled{cursor:default}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.card button{font-size:12px}.metrics{gap:10px}.metrics strong{font-size:20px}.actions button{font-size:12px}.plot text{font-size:13px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.service-plot{height:230px;position:relative;padding:25px 0 25px 28px}.service-areas{height:180px;position:relative;display:flex;gap:16px;align-items:flex-end;border-bottom:1px solid var(--rule)}.service-area{position:relative;background:var(--accent-soft);border:0;box-shadow:inset 0 0 0 1px var(--accent);transition:width .25s,height .25s;min-height:0}.service-area.other{background:var(--second-soft);box-shadow:inset 0 0 0 1px var(--second)}.service-area b{position:absolute;bottom:-26px;left:50%;transform:translateX(-50%);font-size:13px;font-weight:600}.axis-top,.axis-bottom{position:absolute;left:0;font-size:11px;color:var(--muted)}.axis-top{top:0}.axis-bottom{bottom:19px}.service-areas>i{position:absolute;left:0;right:0;border-top:1px dashed var(--accent);pointer-events:none;transition:bottom .25s}#level-b{border-color:var(--second)}","js":"const $=s=>document.querySelector(s),num=(n,d=2)=>viz.number(n,d);\nfunction draw(){const w=+$('#weight').value,t=+$('#service').value,b=9-t,va=t/w,vb=b;$('#weight-value').textContent=w+' : 1';$('#service-value').textContent=num(t,2)+' / 9 ms';$('#area-a').style.width='calc((100% - 16px) * '+w/5+')';$('#area-b').style.width='calc((100% - 16px) / 5)';$('#area-a').style.height=va*20+'px';$('#area-b').style.height=vb*20+'px';$('#level-a').style.bottom=va*20+'px';$('#level-b').style.bottom=vb*20+'px';$('#virtual-a').textContent=num(va);$('#virtual-b').textContent=num(vb);$('#formula-a').textContent='ΔvA = '+num(t)+' / '+w+' = '+num(va);$('#formula-b').textContent='ΔvB = '+num(b)+' / 1 = '+num(vb);$('#explanation').textContent=viz.t(Math.abs(va-vb)<1e-8?'equal':va>vb?'aheadA':'aheadB');}\nfor(const id of ['weight','service'])$('#'+id).addEventListener('input',draw);$('#balance').addEventListener('click',()=>{$('#service').value=9*(+$('#weight').value)/(+$('#weight').value+1);draw();});draw();\n","audio":false,"strings":{"aheadA":"A 更高:相对各自权重,A 已得到更多服务。增加实际时间会增高面积块,增加权重则会降低同一用时对应的高度。","aheadB":"B 更高:相对各自权重,B 已得到更多服务。可以调整时间分配,或改变 A 的权重,观察高度怎样重新接近。","balance":"按权重分配这 9 ms","chart":"宽度为权重、高度为虚拟服务、面积为实际 CPU 时间","clockA":"A 的虚拟服务 Δv","clockB":"B 的虚拟服务 Δv","equal":"同样的高度代表相同的归一化服务。权重越大、块越宽,达到同一高度所需的实际时间也越多。","eyebrow":"01 · 公平的计量","geometry":"宽度按固定单位表示权重;高度是 Δv,面积是实际运行时间。同样高才表示归一化服务相同。","heading":"相同的虚拟进度,需要多少 CPU 时间?","intro":"总共分配 9 ms。调整 A 的权重和实际用时,比较两个面积块的高度。B 的权重固定为 1。","note":"单 CPU、两任务持续可运行,基准权重归一化为 1。图解释计量公式,不模拟真实调度的每次选择;nice、组层级和唤醒会改变实际竞争。","scope":"模型范围与约定","service":"分给 A 的实际时间","weight":"权重 A : B"}}
经典 CFS 用 vruntime 接近“谁获得的归一化服务最少”,优先让落后者补上。这样能解释长期份额,却还不充分表达一个短请求应该多快获得响应。EEVDF 在此基础上将是否有资格运行 与合格请求的先后次序 分开。
3. EEVDF:资格门槛与虚拟截止期的分工
第一步:用 lag 判断是否有资格
在一个固定可运行集合的简化模型中,令任务虚拟运行时间为 vᵢ,权重为 wᵢ,系统虚拟时间 V 为其加权平均。任务服务差额可写成:
V = ∑ i w i ∑ i w i v i , lag i = w i ( V − v i )
零 lag 也具有资格。正 lag 表示相对公平基准少得到了一些服务,有资格参与选择;负 lag 表示已经超前,暂时不该仅凭短请求继续抢占别人。
第二步:在合格请求中比较期限
若当前请求希望获得实际运行量 rᵢ,其虚拟截止期可用下式理解:
d i = v i , start + r i w i w 0
上式描述一项新请求建立期限时的关系,此时 vᵢ 是该请求的起始虚拟位置。请求执行期间 vruntime 继续增长,已经确定的 deadline 不应随它不断向后平移;新的请求才重新确定期限。下面的快照选择各任务在请求起点的状态,以便直接比较。
在合格实体中,优先选择虚拟截止期较早者。这里的 deadline 是虚拟服务坐标 ,不是墙钟时刻,也不是 SCHED_DEADLINE 的实时完成保证。
推演:为什么最后选 B
考虑三项等权任务,基准权重归一化为 1:
任务 v 请求量 r d = v + r V − v 选择资格
A 98 10 108 2 有
B 99 5 104 1 有
C 103 0.25 103.25 -3 无
此时 V = (98 + 99 + 103) / 3 = 100。如果只取最小 vruntime,会选 A;如果只取最小 deadline,会选 C;EEVDF 在这个快照选择 B。C 的短请求不能抵消它已多得服务的事实,而 B 比 A 更早到期的合格请求获得优先响应。这就是两个判断必须分开的原因。
一次 EEVDF 选择:先有资格,再比较虚拟截止期
A
vruntime = 98
虚拟 deadline = 108
有资格,lag > 0
B
vruntime = 99
虚拟 deadline = 104
有资格,选中
C
vruntime = 103
虚拟 deadline = 103.25
服务超前,暂不参与
等权快照 V = 100:C 虽然 deadline 最早,但不符合资格
A 比 B 少运行过;B 的合格请求更早到期,因此选择 B
这是固定任务集合的教学快照;真实实现还处理组层次、睡眠记账和当前请求保护。
改变 C 的服务位置会影响资格;改变 B 的请求量会影响期限,但不改变已经得到的服务。每次调参都是新的请求快照。
正在呈现知识画面 重新加载
期限最早,为什么仍然轮不到它? EEVDF 先以 lag ≥ 0 筛选资格,再比较虚拟截止期。默认快照中 C 的期限最早却不合格,B 在合格集合里胜出。
{"id":"scheduler-eevdf","title":"期限最早,为什么仍然轮不到它?","summary":"EEVDF 先以 lag ≥ 0 筛选资格,再比较虚拟截止期。默认快照中 C 的期限最早却不合格,B 在合格集合里胜出。","height":1000,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"controls\"><div class=\"control\"><label for=\"position\" data-i18n=\"position\"></label><output id=\"position-value\" for=\"position\"></output><input id=\"position\" type=\"range\" min=\"94\" max=\"106\" step=\"0.25\" value=\"103\"></div><div class=\"control\"><label for=\"request\" data-i18n=\"request\"></label><output id=\"request-value\" for=\"request\"></output><input id=\"request\" type=\"range\" min=\"0.25\" max=\"12\" step=\"0.25\" value=\"5\"></div></div><div class=\"eevdf-chart\"><div id=\"system-v\"><span id=\"system-label\"></span></div><div class=\"request-row\" id=\"task-A\"><header><strong>A</strong><span id=\"status-A\"></span></header><div class=\"request-rail\"><i id=\"request-A\"></i><b class=\"v-point\" id=\"v-A\"></b><b class=\"d-point\" id=\"d-A\"></b></div><div class=\"values\" id=\"values-A\"></div></div><div class=\"request-row\" id=\"task-B\"><header><strong>B</strong><span id=\"status-B\"></span></header><div class=\"request-rail\"><i id=\"request-B\"></i><b class=\"v-point\" id=\"v-B\"></b><b class=\"d-point\" id=\"d-B\"></b></div><div class=\"values\" id=\"values-B\"></div></div><div class=\"request-row\" id=\"task-C\"><header><strong>C</strong><span id=\"status-C\"></span></header><div class=\"request-rail\"><i id=\"request-C\"></i><b class=\"v-point\" id=\"v-C\"></b><b class=\"d-point\" id=\"d-C\"></b></div><div class=\"values\" id=\"values-C\"></div></div><div class=\"scale\"><span>94</span><span>118</span></div></div><p class=\"caption\" data-i18n=\"markers\"></p><div class=\"formula\" id=\"formula\"></div><div class=\"winner\"><span data-i18n=\"winner\"></span><strong id=\"winner\"></strong></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"whyB\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"whyA\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"whyC\"></p></div><details class=\"scope\"><summary data-i18n=\"scope\"></summary><p class=\"note\" data-i18n=\"note\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.note,.caption{font-size:12px;color:var(--muted);margin-top:14px}.controls{display:grid;gap:12px;margin:18px 0}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center;font-size:13px}.control input{grid-column:1/3;width:100%;margin:0}.control output{color:var(--accent);font-variant-numeric:tabular-nums}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{flex:1;min-width:0;min-height:42px;font-size:13px}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0}.metrics>div{min-width:0;border-left:2px solid var(--accent);padding-left:12px}.metrics span{display:block;color:var(--muted);font-size:12px;min-height:2.8em}.metrics strong{display:block;font-size:22px;font-weight:500;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.formula{font-size:13px;overflow-wrap:anywhere;border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-variant-numeric:tabular-nums}.formula div+div{margin-top:6px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.card{min-width:0;border:1px solid var(--rule);border-radius:9px;padding:14px}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card code{font-size:12px;overflow-wrap:anywhere}.card .value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums}.card small{display:block;color:var(--muted);font-size:12px}.card button{width:100%;min-width:0;font-size:13px;min-height:42px}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{accent-color:var(--accent);width:16px;height:16px;flex-shrink:0}.pill{display:inline-block;padding:3px 8px;border-radius:5px;background:var(--accent-soft);color:var(--accent);font-size:12px}.plot{position:relative;margin:18px 0}.plot svg{width:100%;height:auto;overflow:visible}.plot text{font-size:12px;fill:var(--muted)}.plot .ink{fill:var(--ink)}.plot .blue{fill:var(--accent);stroke:var(--accent)}.plot .gold{fill:var(--second);stroke:var(--second)}.plot .area{fill:var(--accent-soft);stroke:var(--accent);transition:all .25s}.plot .other{fill:var(--second-soft);stroke:var(--second)}.plot line,.plot path,.plot rect,.plot circle{transition:all .25s}.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:12px;color:var(--muted);margin-top:10px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}.legend .gold:before{background:var(--second)}details{margin-top:14px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:8px 0}button:disabled{cursor:default}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.card button{font-size:12px}.metrics{gap:10px}.metrics strong{font-size:20px}.actions button{font-size:12px}.plot text{font-size:13px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.eevdf-chart{position:relative;padding-top:30px;margin-top:20px}.request-row{position:relative;padding:10px 0}.request-row header{display:flex;justify-content:space-between;gap:10px;font-size:12px}.request-row header span{color:var(--muted)}.request-rail{height:22px;position:relative;margin:8px 0;border-bottom:1px solid var(--rule)}.request-rail>i{position:absolute;height:6px;bottom:0;transform:translateY(50%);background:var(--accent);transition:left .25s,width .25s}.v-point{position:absolute;width:10px;height:10px;border-radius:50%;bottom:0;transform:translate(-50%,50%)}.d-point{position:absolute;width:3px;height:18px;bottom:0;transform:translate(-50%,50%)}.v-point,.d-point{background:var(--accent);transition:left .25s}.values{font-size:12px;font-variant-numeric:tabular-nums;color:var(--muted);margin-top:12px;min-height:3.4em}.scale{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}#system-v{position:absolute;top:25px;bottom:25px;border-left:1px dashed var(--accent);pointer-events:none;transition:left .25s}#system-label{position:absolute;top:-25px;transform:translateX(-50%);white-space:nowrap;font-size:12px;color:var(--accent);background:var(--paper)}.winner{display:flex;justify-content:space-between;align-items:center;padding:14px;margin-top:16px;background:var(--accent-soft);border-radius:8px}.winner strong{font-size:26px;color:var(--accent)}","js":"const $=s=>document.querySelector(s),f=n=>viz.number(n,2),x=n=>(n-94)/24*100;\nfunction draw(){const v=[98,99,+$('#position').value],r=[10,+$('#request').value,.25],V=v.reduce((a,b)=>a+b)/3,d=v.map((n,i)=>n+r[i]),eligible=v.map(n=>n<=V+1e-9);let winner=-1;for(let i=0;i<3;i++)if(eligible[i]&&(winner<0||d[i]<d[winner]))winner=i;$('#position-value').textContent=f(v[2]);$('#request-value').textContent=f(r[1]);$('#system-v').style.left=x(V)+'%';$('#system-label').textContent='V = '+f(V);for(let i=0;i<3;i++){const k='ABC'[i],color=winner===i?'var(--second)':eligible[i]?'var(--accent)':'var(--muted)';$('#request-'+k).style.left=x(v[i])+'%';$('#request-'+k).style.width=(d[i]-v[i])/24*100+'%';$('#request-'+k).style.background=color;$('#request-'+k).style.opacity=eligible[i]?'1':'.25';$('#v-'+k).style.left=x(v[i])+'%';$('#v-'+k).style.background=color;$('#d-'+k).style.left=x(d[i])+'%';$('#d-'+k).style.background=color;$('#values-'+k).textContent='v '+f(v[i])+' → d '+f(d[i])+' · lag '+f(V-v[i]);$('#status-'+k).textContent=viz.t(eligible[i]?'eligible':'ineligible');$('#status-'+k).style.color=color;}$('#formula').textContent='V = ('+v.map(f).join(' + ')+') / 3 = '+f(V);$('#winner').textContent='ABC'[winner];$('#explanation').textContent=viz.t('why'+'ABC'[winner]);}\n$('#position').addEventListener('input',draw);$('#request').addEventListener('input',draw);draw();\n","audio":false,"strings":{"eligible":"有资格","eyebrow":"02 · EEVDF","heading":"期限最早,为什么仍然轮不到它?","ineligible":"暂不合格","intro":"拖动 C 的服务位置,或缩短 B 的请求。观察资格门槛 V 和虚拟截止期怎样分别影响选择。","markers":"圆点是 v,短竖线是 d,中间的线段是请求长度;虚线是系统虚拟时间 V。金色标出选中任务,灰色表示不合格。","note":"三个等权任务的新请求快照,基准权重为 1;A 的 v=98、r=10,B 的 v=99,C 的 r=0.25。每次调参重建快照,不是运行时不断重算已有 deadline。同期限按字母顺序仅为演示约定。","position":"C 的虚拟运行位置 vC","request":"B 的请求量 rB","scope":"模型范围与约定","whyA":"A 合格,且它的截止期不晚于其他合格请求。已获得过多服务的任务不能仅凭更早的期限越过资格门槛。","whyB":"B 在合格集合中有最早的截止期。缩短请求改变期限,但不会直接消除任务已经超前的服务差额。","whyC":"C 现在位于 V 左侧或线上,具备资格;它的短请求也有最早的截止期,因此被选中。","winner":"本快照选中"}}
可以直接运行这个小模型检查结果;它不模拟 Linux 的唤醒、层级和迁移逻辑:
from fractions import Fraction as F
tasks = {
" A" : { " w" : F ( 1 ) , " v" : F ( 98 ) , " r" : F ( 10 ) } ,
" B" : { " w" : F ( 1 ) , " v" : F ( 99 ) , " r" : F ( 5 ) } ,
" C" : { " w" : F ( 1 ) , " v" : F ( 103 ) , " r" : F ( 1 , 4 ) } ,
}
V = sum ( t [ " w" ] * t [ " v" ] for t in tasks . values ( ) ) / sum (
t [ " w" ] for t in tasks . values ( )
)
eligible = { }
for name , t in tasks . items ( ) :
lag = t [ " w" ] * ( V - t [ " v" ] )
deadline = t [ " v" ] + t [ " r" ] / t [ " w" ]
print ( name , " lag=" , lag , " deadline=" , deadline )
if lag >= 0 :
eligible [ name ] = deadline
assert V == 100
assert min ( eligible , key = eligible . get ) == " B"
从教学模型到内核实现
缩短请求量能影响响应顺序,但不会自动增大任务的长期权重。真实内核还要防止任务通过“运行到超前就短暂睡眠,醒来重新领信用”的方式破坏公平;睡眠时的 lag 保存、衰减和延迟出队记账,都是维护这个约束的手段。延迟出队中的记账实体不等于一个仍在等待事件的任务可以执行用户代码。
真实实现还要解决查找成本。本机 kernel/sched/fair.c 的相关树按 deadline 组织,用子树 vruntime 信息帮助剪枝查找合格实体;pick_eevdf() 还处理当前请求保护。
分层组实体、入队/出队修正和整数精度,让 V 的维护比固定集合平均值复杂。阅读源码时,可以先核对“资格与期限”两个判断,再追踪这些实现细节。EEVDF 文档 、CFS 设计
4. 调度策略与调度类:不要混用三种 deadline
普通任务通常使用 SCHED_OTHER,内核也常称 SCHED_NORMAL;公平类内部采用 EEVDF,不存在一个要求应用改成 SCHED_EEVDF 才能启用的通用策略。实时策略和可扩展调度则是另外的选择:
策略或机制 分配规则 关键边界
SCHED_OTHER / BATCH 公平份额;BATCH 表达批处理倾向 普通优先级与 nice 权重不能覆盖更高调度类
SCHED_IDLE 公平类中的极低权重策略 与 CPU 无事可做时运行的 idle task 不同
SCHED_FIFO 同优先级按 FIFO;可一直运行到阻塞、主动让出或被抢占 没有普通 RR 时间片,不代表不受系统带宽约束
SCHED_RR 实时优先级加同优先级轮转 时间片轮转不构成应用完成时限保证
SCHED_DEADLINE EDF 选择与 CBS 运行预算管理 参数、准入控制、执行负荷共同决定可行性
sched_ext 启用并装载相应 BPF 调度器后,由扩展策略管理相关任务 是否接管普通任务取决于模式;错误可触发回退
通常 deadline 类先于实时类,实时类先于公平类;内核还有 stop 等内部类,启用 sched_ext 时应结合运行模式阅读类次序,不能把一张三行表当成全部实现。
对照:调度期限与业务超时
以 runtime=2 ms、deadline=10 ms、period=10 ms 为例:任务声明每周期最多需要 2 ms CPU 服务,并要求相应期限。参数需要满足 runtime ≤ deadline ≤ period 等约束,还要通过准入检查。
它不会替应用每隔 10 ms 自动调用函数。应用仍要释放工作、控制执行预算并分析阻塞,才能讨论时限是否可行。
名字里都带“期限” 实际约束什么
EEVDF 虚拟 deadline 公平服务坐标中的请求先后
SCHED_DEADLINE 的 deadline 实时任务的调度期限
RPC 超时 排队、执行、网络等共同构成的端到端等待
这三种期限分属不同层次,不能相互替代。SCHED_DEADLINE 、sched_ext
5. cgroup:权重决定竞争份额,quota 决定可用上限
组权重:先分给组,再分给任务
公平调度可以分层。假设根下有 A、B 两组,权重 2:1,A 中两个等权持续可运行任务,B 中一个;仅考虑一颗 CPU、没有 quota 时,A 先获得约 2/3,再在组内各分 1/2,所以三个任务各约 1/3。若 A 再增加一个任务,A 仍约 2/3,内部各约 2/9;B 中唯一任务仍约 1/3。组隔离让“多创建几个线程”不必直接侵占另一个组的份额。
quota:多核共同消耗一份预算
cpu.weight 描述相对份额,不是预留资源,也不是严格上限。cpu.max 则限制周期内组的总 CPU 时间。例如 50000 100000 是 100 ms 周期最多使用 50 ms CPU 时间。若两线程在两颗 CPU 上同时运行,它们可以约 25 ms 就耗尽合计 50 ms 的预算,然后被节流到后续补充;不能按“每颗 CPU 各允许用 50 ms”理解。
同样的 quota,两个 CPU 可以更早耗尽预算
cpu.max = 50000 100000:每周期总预算 50 ms,周期 100 ms
CPU 0
执行 25 ms
预算耗尽,等待补充
CPU 1
执行 25 ms
预算耗尽,等待补充
0
25 ms
100 ms
简化为无 burst、周期起点预算完整、两线程持续并行;实际还受分发粒度等影响。
调节并行数,再把墙钟时间拖过周期边界。各条 CPU 运行带都从同一份预算扣除时间。
正在呈现知识画面 重新加载
CPU 越多,为什么越早被节流? 多颗 CPU 消耗同一份组预算。50 ms CPU 时间由两颗 CPU 并行消耗,约 25 ms 墙钟时间就会用尽,随后等待周期补充。
{"id":"scheduler-quota","title":"CPU 越多,为什么越早被节流?","summary":"多颗 CPU 消耗同一份组预算。50 ms CPU 时间由两颗 CPU 并行消耗,约 25 ms 墙钟时间就会用尽,随后等待周期补充。","height":1000,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"controls\"><div class=\"control\"><label for=\"quota\" data-i18n=\"quota\"></label><output id=\"quota-value\" for=\"quota\"></output><input id=\"quota\" type=\"range\" min=\"10\" max=\"200\" step=\"5\" value=\"50\"></div><div class=\"control\"><label for=\"cpus\" data-i18n=\"cpus\"></label><output id=\"cpus-value\" for=\"cpus\"></output><input id=\"cpus\" type=\"range\" min=\"1\" max=\"4\" step=\"1\" value=\"2\"></div><div class=\"control\"><label for=\"time\" data-i18n=\"time\"></label><output id=\"time-value\" for=\"time\"></output><input id=\"time\" type=\"range\" min=\"0\" max=\"200\" step=\"1\" value=\"25\"></div></div><div class=\"budget\"><div><span data-i18n=\"remaining\"></span><strong id=\"remaining\"></strong></div><div class=\"budget-track\"><i id=\"budget-fill\"></i></div><p id=\"period\"></p></div><div class=\"timeline\"><div id=\"probe\"></div><div class=\"lane\" id=\"lane-0\"><code>CPU 0</code><div class=\"lane-track\"><i class=\"run first\"></i><i class=\"run second\"></i><span class=\"inactive\" data-i18n=\"inactive\"></span></div></div><div class=\"lane\" id=\"lane-1\"><code>CPU 1</code><div class=\"lane-track\"><i class=\"run first\"></i><i class=\"run second\"></i><span class=\"inactive\" data-i18n=\"inactive\"></span></div></div><div class=\"lane\" id=\"lane-2\"><code>CPU 2</code><div class=\"lane-track\"><i class=\"run first\"></i><i class=\"run second\"></i><span class=\"inactive\" data-i18n=\"inactive\"></span></div></div><div class=\"lane\" id=\"lane-3\"><code>CPU 3</code><div class=\"lane-track\"><i class=\"run first\"></i><i class=\"run second\"></i><span class=\"inactive\" data-i18n=\"inactive\"></span></div></div><div class=\"ticks\"><span>0</span><span>100</span><span>200 ms</span></div></div><div class=\"legend\"><span data-i18n=\"running\"></span><span class=\"gold\" data-i18n=\"throttled\"></span></div><div class=\"formula\" id=\"formula\"></div><div class=\"actions\"><button id=\"play\" aria-pressed=\"false\"><span id=\"play-label\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"play\"></span><span class=\"reserve\" aria-hidden=\"true\" data-i18n=\"pause\"></span></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"available\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"exhausted\"></p></div><details class=\"scope\"><summary data-i18n=\"scope\"></summary><p class=\"note\" data-i18n=\"note\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.note,.caption{font-size:12px;color:var(--muted);margin-top:14px}.controls{display:grid;gap:12px;margin:18px 0}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center;font-size:13px}.control input{grid-column:1/3;width:100%;margin:0}.control output{color:var(--accent);font-variant-numeric:tabular-nums}.presets{display:flex;flex-wrap:wrap;gap:4px}.presets button{flex:1 1 auto;min-width:min-content}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.actions button{flex:1;min-width:0;min-height:42px;font-size:13px}.copy{display:grid;margin-top:16px;font-size:14px}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0}.metrics>div{min-width:0;border-left:2px solid var(--accent);padding-left:12px}.metrics span{display:block;color:var(--muted);font-size:12px;min-height:2.8em}.metrics strong{display:block;font-size:22px;font-weight:500;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.formula{font-size:13px;overflow-wrap:anywhere;border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-variant-numeric:tabular-nums}.formula div+div{margin-top:6px}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.card{min-width:0;border:1px solid var(--rule);border-radius:9px;padding:14px}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card code{font-size:12px;overflow-wrap:anywhere}.card .value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums}.card small{display:block;color:var(--muted);font-size:12px}.card button{width:100%;min-width:0;font-size:13px;min-height:42px}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{accent-color:var(--accent);width:16px;height:16px;flex-shrink:0}.pill{display:inline-block;padding:3px 8px;border-radius:5px;background:var(--accent-soft);color:var(--accent);font-size:12px}.plot{position:relative;margin:18px 0}.plot svg{width:100%;height:auto;overflow:visible}.plot text{font-size:12px;fill:var(--muted)}.plot .ink{fill:var(--ink)}.plot .blue{fill:var(--accent);stroke:var(--accent)}.plot .gold{fill:var(--second);stroke:var(--second)}.plot .area{fill:var(--accent-soft);stroke:var(--accent);transition:all .25s}.plot .other{fill:var(--second-soft);stroke:var(--second)}.plot line,.plot path,.plot rect,.plot circle{transition:all .25s}.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:12px;color:var(--muted);margin-top:10px}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}.legend .gold:before{background:var(--second)}details{margin-top:14px;font-size:12px;color:var(--muted)}summary{cursor:pointer;padding:8px 0}button:disabled{cursor:default}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.card button{font-size:12px}.metrics{gap:10px}.metrics strong{font-size:20px}.actions button{font-size:12px}.plot text{font-size:13px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n\n.budget{padding:14px;background:var(--accent-soft);border-radius:8px}.budget>div:first-child{display:flex;justify-content:space-between;gap:12px;font-size:13px}.budget strong{font-size:22px}.budget p{margin:8px 0 0;font-size:12px;color:var(--muted)}.budget-track{height:10px;background:var(--rule);border-radius:5px;overflow:hidden;margin-top:10px}.budget-track i{display:block;height:100%;background:var(--accent);transition:width .2s}.timeline{position:relative;margin:20px 0 0;padding-top:5px}.lane{display:grid;grid-template-columns:52px 1fr;gap:8px;align-items:center;margin:10px 0;font-size:12px}.lane-track{height:26px;position:relative;background:var(--second-soft);border-radius:4px;overflow:hidden}.run{position:absolute;top:0;height:100%;background:var(--accent);transition:width .25s}.run.first{left:0}.run.second{left:50%}.lane.off .lane-track{background:var(--surface)}.lane.off .run{visibility:hidden}.inactive{display:none;font-size:11px;text-align:center;color:var(--muted);line-height:26px}.lane.off .inactive{display:block}#probe{position:absolute;top:0;bottom:28px;width:2px;background:var(--ink);z-index:1;pointer-events:none}.ticks{margin-left:60px;display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}#play{display:grid}#play>span{grid-area:1/1}#quota-value{width:8.4em;white-space:nowrap;text-align:right}#time-value{width:5em;text-align:right}.formula{min-height:calc(3.2em + 13px)}.budget strong{white-space:nowrap}@media(max-width:480px){.budget>div:first-child{display:grid;grid-template-columns:1fr;gap:6px}.budget strong{justify-self:end}}","js":"const $=s=>document.querySelector(s);function playing(on){$('#play').setAttribute('aria-pressed',String(on));$('#play-label').textContent=viz.t(on?'pause':'play');}\nfunction draw(){const q=+$('#quota').value,n=+$('#cpus').value,t=+$('#time').value,local=t%100,used=Math.min(q,n*local),remaining=q-used,run=Math.min(100,q/n),blocked=q<n*100&&local>=q/n;$('#quota-value').textContent=q+' ms / 100 ms';$('#cpus-value').textContent=n;$('#time-value').textContent=t+' ms';$('#remaining').textContent=viz.number(remaining,1)+' ms';$('#budget-fill').style.width=100*remaining/q+'%';$('#period').textContent=viz.t('period')+' '+(Math.floor(t/100)+1)+' · '+local+' / 100 ms';for(let i=0;i<4;i++){const e=$('#lane-'+i);e.classList.toggle('off',i>=n);e.querySelectorAll('.run').forEach(b=>b.style.width=run/2+'%');}$('#probe').style.left='calc(60px + (100% - 60px) * '+t/200+')';$('#formula').textContent=''+q+' ms / '+n+' CPU = '+viz.number(q/n,1)+' ms';$('#explanation').textContent=viz.t(blocked?'exhausted':'available');}\nconst player=viz.playback({delay:100,canAdvance:()=>+$('#time').value<200,advance(){$('#time').value=Math.min(200,+$('#time').value+2);draw();},onChange:playing});for(const id of ['quota','cpus','time'])$('#'+id).addEventListener('input',()=>{player.pause();draw();});$('#play').addEventListener('click',()=>{if(+$('#time').value===200){$('#time').value=0;draw();}player.toggle();});$('#reset').addEventListener('click',()=>{player.pause();$('#time').value=0;draw();});playing(false);draw();\n","audio":false,"strings":{"available":"这一时刻还有预算,参与的 CPU 可以继续执行。100 ms 边界会开启新周期;图中的每条蓝带都从同一份预算扣费。","cpus":"同时运行的 CPU 数 N","exhausted":"预算已经耗尽,即使机器还有其他空闲 CPU,本组也需要等待补充。增加并行数会更快花完同样多的 CPU 时间。","eyebrow":"03 · 共享配额","heading":"CPU 越多,为什么越早被节流?","inactive":"未参与","intro":"多颗 CPU 同时从一份预算中扣除运行时间。调整配额和并行数,再拖动时间,看预算何时耗尽、何时补充。","note":"固定周期 P=100 ms,任务始终需要 CPU,无竞争、无祖先限额、无突发额度。图按连续预算计算,省略真实带宽分片和少量结转;Q/N 超过一个周期时,本周期不会耗尽。200 ms 是第三周期起点。","pause":"暂停","period":"周期","play":"播放时间","quota":"每周期 CPU 时间预算 Q","remaining":"当前周期剩余预算","reset":"回到起点","running":"运行","scope":"模型范围与约定","throttled":"预算耗尽后的节流区间","time":"墙钟时间"}}
回到开头的慢请求:即使全机还有空闲 CPU,这个 cgroup 的本周期预算也可能已经耗尽。供给还有剩余,不代表该组仍有使用额度。
检查时,把 cpu.stat 的节流次数/时间、cpu.max 和请求延迟放在同一时间窗口比较。节流增量提供排查线索,具体慢请求仍需结合应用锁、I/O 和上层队列分析。cgroup v2 CPU 控制器
6. 多核放置:均匀不一定最快
两颗 CPU 各运行一项任务,看起来比一颗拥挤、一颗空闲更合理,但迁移有代价。任务刚在 CPU 0 填热缓存,迁到 CPU 1 会损失局部性;若数据主要在另一个 NUMA 节点,内存访问还可能变远。SMT 兄弟线程共享执行资源,也不能机械地当成两颗完全独立的核心。调度器因此在空闲容量、缓存、拓扑与能耗之间权衡。
PELT 等历史信号帮助估计任务近期的运行/可运行行为。利用率、可运行负载和带权负载不是同一个数:一个任务等待 CPU 很久可能体现排队压力,而真正执行比例才直接反映已消耗的算力。短暂采样看到某 CPU 100% 并不能预测迁移一定获益,也不能保证它是全部系统瓶颈。
唤醒放置、周期性均衡和空闲 CPU 拉取工作会在不同情境发生。亲和性与 cpuset 缩小候选集合,异构 CPU 的能力与能耗策略进一步改变选择。因此排查时应先确认任务实际允许使用哪些 CPU,再解释负载是否不均;对被绑在一个 CPU 的任务,增加其他 CPU 数量不会自动消除排队。
7. 可重复的只读观察
下面只查询当前 Python 进程的策略、nice、亲和性及部分调度统计,不修改策略或系统配置。/proc/self/sched 的字段受内核配置影响,不应作为固定列数格式解析。
import os
from pathlib import Path
print ( " policy:" , os . sched_getscheduler ( 0 ) )
print ( " nice:" , os . getpriority ( os . PRIO_PROCESS , 0 ) )
print ( " allowed CPUs:" , sorted ( os . sched_getaffinity ( 0 ) ) )
try :
for line in Path ( " /proc/self/sched" ) . read_text ( ) . splitlines ( ) :
if any ( key in line for key in (
" se.sum_exec_runtime" , " nr_switches" , " nr_voluntary_switches" ,
" nr_involuntary_switches" , " se.load.weight" , " policy" , " prio"
) ) :
print ( line )
except OSError as exc :
print ( " sched statistics unavailable:" , exc )
要把一次延迟拆开,分别观测事件发生、sched_wakeup 和任务真正出现在 sched_switch 的 next 位置的时间。唤醒到运行的间隔反映运行队列等待及相关调度延迟;执行时间又包含应用自己的工作。它们都不等于一次寄存器和地址空间切换的纯成本。下一篇上下文切换 沿具体交接说明这些边界。