Theory · OS · CPU Scheduling

CPU 调度

从 FCFS/RR 到 MLFQ,再到 Linux 的 CFS 与 EEVDF —— 公平、延迟与吞吐的三方拉扯

经典算法

每个算法都有一个致命缺陷,而下一个算法正是为修它而生:护航效应 → 短作业优先 → 饥饿 → 老化

Linux 现实

调度类分层:实时类永远压过公平类;fair 类 2007 年用 CFS 的 vruntime 实现公平,6.6 起交给 EEVDF 表达延迟

Go 视角

GMP 是用户态的 MLFQ 变体:work stealing 求公平、sysmon 抢占防独占、netpoller 让 I/O 等待不占线程

定位:OS 系列第三篇。主线是"缺陷驱动进化":每介绍一个算法先讲它解决什么、再讲它暴露什么。结尾把同一套思想映射到 Go runtime。

The Scheduling Problem

调度在解什么问题:指标之间天生打架

为什么需要调度

就绪的进程数 > CPU 核数,CPU 必须在进程间时间复用(配合时钟中断的抢占)。调度器决定"下一个 CPU 给谁、给多久"——这是内核最热路径上的策略代码。

什么时候调度

① 进程阻塞(等 I/O / 锁)或退出;② 时间片耗尽(时钟中断);③ 更高优先级进程就绪(抢占);④ I/O 完成唤醒。前两类是主动可预期的,后两类是事件驱动的。

两类任务

CPU 密集型(编译、科学计算):吃满时间片,在乎总完成时间;I/O 密集型(Web 服务、代理):频繁阻塞让出,在乎单次响应延迟。真实系统永远是混合负载。

指标定义在乎它的是
周转时间完成时刻 − 到达时刻批处理 / 吞吐
响应时间首次被调度 − 到达时刻(延迟)交互式 / 在线服务
吞吐量单位时间完成任务数后端系统容量
公平性资源按权重 / 按需分配多租户 / 防饥饿
跷跷板:让周转最优(SJF 先跑短任务)会牺牲响应;让响应最优(RR 细分时间片)会增加切换开销伤吞吐。没有全能算法,只有场景取舍——这是全篇的母题。
抢占式 vs 协作式:协作式靠进程主动让出(一个死循环拖死系统,Windows 3.x / 早期 Mac);抢占式靠时钟中断强制切换(现代通用 OS 的选择)。实时系统还要考虑可抢占内核(Linux PREEMPT_RT)。
先立母题:"指标打架,算法即取舍"。周转/响应/吞吐/公平四指标对应后面每个算法的偏向。抢占式的存在依赖时钟中断——呼应第一篇的中断机制。

FCFS · SJF · RR · Priority

四个经典算法:每个都是下一个的动机

算法做法优点缺陷(= 下一算法的动机)
FCFS先来先服务的 FIFO 队列,非抢占实现最简单,无饥饿护航效应:一个长任务堵住后面所有短任务,平均周转崩坏
SJF / SRTF优先跑(剩余)时间最短的;SRTF 是其抢占版平均周转时间理论最优需要预知未来(只能估计运行时长);长任务饥饿
RRFIFO + 时间片 q,到点强制轮转响应时间好,公平且无饥饿q 的两难:太大退化成 FCFS,太小切换开销吃掉 CPU
优先级按优先级挑最高者(可抢占)能表达重要性,实时场景必需低优先级饥饿 → 老化 aging(等待越久提权越多);还要防优先级反转

时间片 q 怎么选

经验法则:上下文切换开销控制在 <1%——即切换成本(约 μs 级)相对 q 可忽略。Linux 的思路更进一步:由调度器按就绪任务数动态决定每次跑多久(CFS 的 sched_latency / min_granularity,EEVDF 的 slice),而不是固定 q。

面试金句:"FCFS 教会我们短任务会被护航,SJF 教会我们不能预知未来,RR 教会我们响应与开销互相拉扯——MLFQ 的答案是用历史预测未来。"
表格按"缺陷即动机"的链条读。护航效应给个数字例子更好讲:1 个 100s 任务 + 10 个 1s 任务,FCFS 平均周转 ≈ 50s+,若短任务先跑则 ≈ 6s。时间片经验值 <1% 是标准答案。

Multi-Level Feedback Queue

MLFQ:用历史预测未来,逼近 SJF 而不需要预知

多级反馈队列的结构与升降级规则 示意图:四个优先级从高到低排列的队列,高优先级队列优先调度且时间片短;新任务从最高优先级队列进入,用完时间片未被阻塞则降到下一级队列,主动让出则留在原级;周期性提升把所有任务拉回最高队列防止饥饿。 Q0 · 最高优先级 · 时间片最短(交互任务) 新任务从这里进入 Q1 · 时间片加长 Q0 用完时间片降到这里 Q2 · 时间片更长 CPU 密集型任务持续下沉 Q3 · 最低优先级 · 时间片最长 纯后台批处理任务的归宿 降级 降级 降级 规则(OSTEP 口径) 1 · 高优先级队列先调度 2 · 同队列内 RR 轮转 3 · 新任务进最高队列 4 · 用满时间片 → 降一级 5 · 主动让出(I/O)→ 留原级 6 · 周期性 boost:全部拉回 Q0 效果:I/O 型任务浮在高层 (响应好),CPU 型沉底 (吞吐好),boost 防饥饿 防作弊:按总耗时降级, 卡着时间片结束前 sleep 无效 本质:不预知任务长短,用"过去的行为"(是否用满时间片 / 是否主动让出)预测"未来的行为" 这就是"交互任务像 SJF 一样优先、批处理任务依然不会饿死"的机制来源——Windows / macOS 调度器均有其影子
六条规则按序讲,重点在第 4、5 条的差别(用满才降 vs 让出留级)和第 6 条 boost 的必要性。防作弊条款是面试的差异化细节:gaming 攻击(时间片结束前 sleep)。

Scheduling Classes

Linux 现实:调度类分层,实时永远压过公平

Linux 调度类优先级层级 五层调度类从高到低排列:stop 最高,其次 SCHED_DEADLINE 实时截止期限类,然后是 SCHED_FIFO 与 SCHED_RR 实时类(1 到 99 优先级),之后是 SCHED_NORMAL 公平类(CFS 与 EEVDF,nice 值生效的地方),最底是 idle 类。调度器从最高层开始挑选任务。 stop 内核内部强制抢占(CPU 热迁移等),普通系统几乎不可见 SCHED_DEADLINE (dl) EDF 最早截止期限优先 + CBS 带宽预留,任务声明 runtime/deadline/period SCHED_FIFO / SCHED_RR (rt) 实时优先级 1–99 · FIFO 同级不轮转 / RR 同级轮转 · 高于一切普通进程 SCHED_NORMAL / BATCH / IDLE (fair) 99% 进程的家:nice 值生效处 · 2007–2023 CFS → 6.6 起 EEVDF idle CPU 没活干时的占位任务(idle 进程 / 省电) pick_next_task 自上而下 高层有活 低层无份 推论:一个 SCHED_FIFO 90 的死循环就能饿死全部普通进程(内核用 rt_throttled 限 95% 配额兜底)——实时权限是能力也是危险
"nice 值只在同一调度类内比较"是最容易被忽略的事实。rt_throttled 默认 95% 配额(sched_rt_runtime_us=950000 / sched_rt_period_us=1000000),是内核防实时任务吃穿 CPU 的自保机制。

Completely Fair Scheduler · 2.6.23–6.5

CFS:vruntime + 红黑树,公平的算术实现

核心思想

理想多任务:每个任务同时平分 CPU。CFS 追踪每个任务的虚拟运行时间 vruntime——"最亏待的任务"(vruntime 最小)下一个上 CPU。公平 = 让所有任务的 vruntime 尽量同步增长。

vruntime 公式

vruntime += Δt × (NICE_0_LOAD / weight)。nice 0 的任务按真实速度走;nice 更低的任务权重大、vruntime 涨得(同样真实时间"折算"得少),于是能多占 CPU——权重的相对关系是 每差 1 档 nice,份额差约 1.25 倍

数据结构:红黑树

按 vruntime 排序的红黑树:挑最左节点(最小 vruntime)O(log n),缓存 leftmost 指针后近似 O(1);跑完插回树里。就绪队列 = 每核一棵 cfs_rq。

// 教科书版的 CFS 心跳(简写)
pick_next():
    return rb_leftmost(cfs_rq)   // vruntime 最小

// tick / 抢占点:
curr->vruntime += delta × NICE_0_LOAD / curr->weight
if leftmost(cfs_rq)->vruntime < curr->vruntime:
    resched   // 被人亏待得更多 → 换人

// 新任务防捣乱:
vruntime_new = max(min_vruntime,   // 不许比谁都小
                   vruntime_new - sched_latency)
延迟细节:目标延迟(sched_latency,经典默认 ~6ms)内尽可能让所有就绪任务都跑一次,单片不小于最小粒度(min_granularity,~0.75ms)防切换开销失控——任务多时自动延长目标延迟。经典默认值,随内核版本有调整。
CFS 的痛点:公平有了,但进程无法表达"我要低延迟"——nice 只管分多少,不管多久内给。社区用一堆脆弱的唤醒抢占启发式打补丁,最终被 EEVDF 整体替换。
三个必背点:vruntime 公式(权重倒数折算)、红黑树 O(log n) 挑最左、1 档 nice ≈ 1.25 倍份额。新任务 vruntime 钳位防"无限占 CPU"是容易被追问的细节。CFS 的延迟表达缺陷自然引出 EEVDF。

Earliest Eligible Virtual Deadline First · 6.6+

EEVDF:公平不变,给延迟一个数学表达

三个核心概念(LWN 口径)

lag(滞后量):应得时间 − 实得时间。正 lag = 被亏待,负 lag = 多吃了。
eligible(合格):lag ≥ 0 才有资格上 CPU——多吃的任务先还债。
虚拟截止期限:eligible_time + 时间片。调度规则一句话:合格者中,虚拟截止期限最早者优先

延迟需求怎么表达

任务的 slice 由 latency-nice 决定:想要低延迟 → 短时间片 → 截止期限更近 → 更先被调度。相同 nice 的任务总份额不变,只是"切得更碎、给得更快"。公平性由 lag 机制天然保证。

版本与动机

算法出自 1995 年论文,Peter Zijlstra 实现,Linux 6.6(2023-10)成为 fair 类默认。动机:删掉 CFS 时代成堆的脆弱启发式,"用更明确的策略替代猜测";初步基准显示延迟一致性更好。

对比CFSEEVDF
公平实现vruntime 同步增长lag ≥ 0 才合格(等价公平)
挑谁vruntime 最小者合格者中虚拟截止期限最早者
延迟表达无(靠启发式补丁)latency-nice → slice → deadline
数据结构红黑树(vruntime 序)红黑树 ×2(按 eligible / deadline 两棵)
地位2.6.23–6.56.6+ 默认
面试口径:"EEVDF 不是推翻公平,而是把'公平'拆成份额(nice)延迟(latency-nice)两个正交旋钮:份额照旧,延迟用虚拟截止期限直接表达,启发式补丁全部退场。"
版本意识:面试报"Linux 6.6 起 fair 类默认 EEVDF"即可显出信息时效;如果面试官只熟悉 CFS,顺势把两个机制都讲清就是加分。
LWN 的三概念是标准出处:lag/eligible/virtual deadline。强调"eligible 是还债机制"这个直觉。对比表给出知识结构,版本号 6.6(2023-10)必须准确。

RT Scheduling · Priority Inversion

实时调度与优先级反转:Mars Pathfinder 之课

实时调度策略

SCHED_FIFO:同级不轮转,跑到阻塞或被更高优先级抢占;SCHED_RR:同级时间片轮转;SCHED_DEADLINE:任务声明 (runtime, deadline, period) 三元组,EDF + CBS 带宽预留,理论上可证明按时完成。

优先级反转(面试名场面)

高优先级 H 等 L 持有的锁;L 又被中优先级 M 抢占 → H 间接被 M 卡住(优先级序完全颠倒)。1997 年火星车 Mars Pathfinder 在火星上反复重启,就是这种三体反转:最终靠远程启用互斥锁的优先级继承修复。

两种解法

优先级继承:H 等 L 的锁时,L 临时继承 H 的优先级,尽快跑完临界区;② 优先级天花板:锁的优先级 = 所有潜在持有者的最高级,持锁即提权。Linux futex 的 FUTEX_LOCK_PI 与 Go 的 sync 均内置类似思路。

优先级反转时序示意 时间轴示意:低优先级任务 L 持有锁,高优先级任务 H 到来后因锁被阻塞,中优先级任务 M 趁机抢走 CPU 让 L 无法释放锁,形成 H 被 M 间接阻塞的反转;启用优先级继承后 L 以 H 的优先级快速释放锁,H 及时恢复。 H 高 等锁(被 M 间接卡住) M 中 趁虚而入,白嫖 CPU L 低 持锁 · 被抢 → 锁不释放 时间 优先级继承解法: H 阻塞在锁上时,L 临时继承 H 的优先级 → M 抢不动 → L 快速出临界区释放锁
Go 关联:Go 无实时调度概念(GOMAXPROCS 内任务地位平等),但"持锁者被饿死导致全员卡住"在 M:N 调度里同样要防——这就是 mutex 饥饿模式存在的原因(sync 原语 deck)。
Pathfinder 是必讲故事:1997 反复重启 → 远程启用优先级继承。FIFO/RR/DEADLINE 三策略 + 两种解法。时序图讲"H-M-L 三体关系"。

Go Runtime as a Scheduler

Go 调度器:MLFQ 思想的用户态实现

GMP 与本篇概念的映射

就绪队列:P 本地队列(无锁快路径)+ 全局队列(256 批量转移);work stealing:本地空了就从别的 P 偷一半——分布式版本的负载均衡;抢占:sysmon 后台线程发现 G 运行超时(>10ms 量级)打抢占标志,1.14 起配合异步抢占信号,死循环不再拖死 P;I/O 让出:netpoller 把网络等待变成调度事件,不占 M。

与内核 CFS/EEVDF 的本质差异

无 nice/权重:Go 假设所有 goroutine 同等重要,公平靠队列顺序 + 抢占;② 协作优先:切换点在函数调用插入的检查点,代价远小于内核抢占;③ 调度对象超轻:gobuf 只有 PC/SP 等几个字段,vs task_struct 的庞大现场。

为什么 runtime 要自己造调度器

内核调度的切换成本(μs)对"百万并发"不可接受;且内核不知道"G 在等 channel"这类语言级语义。把调度搬进 runtime,语言才有 channel/defer/panic 的原语级配合——与第 7 页 EEVDF 的理念一致:调度策略要让需求方可表达

调度要素内核(CFS/EEVDF)Go runtime
调度对象task(进程/线程)goroutine(G)
公平度量vruntime / lag队列位置 + 抢占时限
抢占时钟中断 + 抢占点函数调用检查点 + sysmon 信号
负载均衡per-CPU 队列 + 周期均衡work stealing
I/O 等待内核挂起任务netpoller 事件化
面试金句:"Go 调度器是把 MLFQ 的直觉装进用户态:本地队列像多级队列的快路径,stealing 补公平,sysmon 补抢占,netpoller 补 I/O——内核只负责 M 层的真并行。"
深挖链接:抢占的源码路径(sysmon retake → preemptone → asyncPreempt)、全局队列批量转移、netpoll 超时复用,见 GMP 调度模型 deck
映射表逐行对应内核概念与 Go 实现。核心论点:runtime 自建调度器 = 切换成本 + 语言语义两重动机。sysmon 10ms 抢占时限是 Go 岗高频追问。

Interview QA · Part 1

高频追问:算法与权衡

1 · 常见调度算法有哪些?各自优缺点?

FCFS/SJF/RR/优先级/MLFQ

按"缺陷链"答:FCFS 简单但有护航效应 → SJF 周转最优但要预知且饥饿 → RR 响应好在时间片两难 → 优先级能表达重要性但需 aging 防饥饿 → MLFQ 用历史预测未来 + boost 防饿。Linux 用调度类分层把公平与实时分开实现。

2 · 什么是护航效应?

convoy effect

FCFS 下一个长任务把后面的短任务全堵住,像大货车护送车队。数字例子:100s 长任务先到,10 个 1s 短任务随后,短任务平均等 50s+;反过来先跑短任务平均只等约 6s。这就是 SJF/MLFQ 的动机。

3 · 时间片怎么定?太大会怎样、太小会怎样?

响应 vs 开销

太大退化成 FCFS(响应差);太小则上下文切换开销占比过高(吞吐差)。经验:切换开销 <1%,量级在毫秒到几十毫秒。Linux 不用固定时间片:按就绪任务数动态切分目标延迟(CFS),或按 latency-nice 决定 slice(EEVDF)。

4 · 抢占式和非抢占式调度的区别?

时钟中断

非抢占(协作式):任务跑到主动让出才切换,一个死循环能拖死系统;抢占式:内核靠时钟中断随时收回 CPU。现代 OS 都是抢占式,代价是切换必须发生在内核态(中断 → schedule → context_switch)。

5 · 什么是优先级反转?怎么解决?

Pathfinder

高优先级等低优先级持有的锁、低优先级又被中优先级抢占,形成倒挂。解法:优先级继承(持锁者临时继承等待者优先级)或优先级天花板(持锁即提权)。火星车 Pathfinder 是标准案例。

6 · 饥饿怎么产生、怎么避免?

aging

静态优先级/短作业优先策略下,低优先级或长任务永远排不上。避免:aging 老化(等待越久优先级越高)、MLFQ 的周期 boost、公平调度(CFS/EEVDF 的 vruntime/lag 机制天然防饿——亏待者终会被优先补偿)。

前六题覆盖经典算法主线。第 1 题的"缺陷链"答法是差异化亮点;第 3 题给出固定 q 与动态 slice 两代方案。

Interview QA · Part 2

高频追问:Linux 现实与 Go

7 · CFS 是怎么实现"完全公平"的?

vruntime红黑树

vruntime = 真实运行时间 × (NICE_0_LOAD / 权重),nice 越低权重越大、vruntime 涨越慢;调度永远挑 vruntime 最小(最亏待)者,红黑树 O(log n) 定位最左节点。1 档 nice 份额差约 1.25 倍。新任务 vruntime 钳位到 min_vruntime 防捣乱。

8 · nice 值是什么?-20 和 19 差多少?

权重相对值

nice ∈ [-20, 19] 只在 fair 调度类内生效,映射到权重(0 → 1024)。它决定的是份额比例而非绝对时间;每差 1 档约 1.25 倍,两端差约 6000 倍。实时任务的优先级与 nice 无关(不同调度类)。

9 · EEVDF 相比 CFS 改了什么?为什么换?

lag / deadline6.6

公平性等价(lag ≥ 0 才合格 vs vruntime 最小者),新增延迟表达:latency-nice 决定 slice,虚拟截止期限 = eligible + slice,最早 deadline 先跑。换它的动机是删掉 CFS 的脆弱启发式补丁、延迟表现更一致。Linux 6.6(2023-10)默认启用。

10 · Linux 怎么同时支持实时和普通进程?

调度类分层

调度类是优先级链:stop → deadline → RT(FIFO/RR) → fair(CFS/EEVDF) → idle,pick_next_task 自上而下。实时优先级 1–99 永远压过 nice;内核用 rt_throttled(默认 95% 配额)防实时任务把 CPU 吃穿。

11 · Go 的调度器像哪种算法?和内核调度的区别?

MLFQ 变体用户态

MLFQ 直觉的用户态实现:本地队列快路径 + work stealing 负载均衡 + sysmon 10ms 量级抢占防独占 + netpoller 让 I/O 等待事件化。区别:无 nice/权重(所有 G 平等)、切换在用户态只存 PC/SP(几十 ns)、调度对象与内核线程解耦(M:N)。

12 · 为什么实时进程能"饿死"普通进程?内核怎么办?

调度类隔离

调度类严格分层,fair 类只有实时类全空才轮得到——一个 SCHED_FIFO 90 死循环理论能独占 CPU。兜底:rt_throttled 限制实时总配额(默认 95%)、SCHED_DEADLINE 的 CBS 带宽预留强制任务不超过声明的 runtime。

后六题覆盖 Linux/Go 主线:CFS 机制、nice 的相对性、EEVDF、调度类、Go 映射、实时饥饿兜底。第 11 题是 Go 岗与本 deck 的交汇点。

Related & References

相关知识点与参考

OS 系列(本分类)

操作系统总览 →(时钟中断与上下文切换的机制基础)
进程线程协程 →(调度对象的状态与切换成本)
同步与互斥 →(优先级反转的另一面:锁)
I/O 模型与 epoll →(阻塞唤醒与 netpoller 的内核侧)

跨领域联动

GMP 调度模型 →(用户态调度的完整源码版)
sync 并发原语 →(mutex 饥饿模式与 futex)
Redis 线程模型 →(单线程事件循环为什么不需要调度器)

参考来源(本 deck 结论可溯源至下列一手材料)

OSTEP ch.9–10(Scheduling)FCFS/SJF/RR/MLFQ 的指标分析与规则口径
LWN: The EEVDF CPU scheduler(lwn.net/Articles/925371)lag / eligible / virtual deadline 定义与替换 CFS 的动机
kernel Documentation/scheduler(sched-design-CFS / sched-eevdf)vruntime 公式、nice 权重表、调度类层级
Mars Pathfinder 事后报告(Mike Jones 整理)优先级反转真实案例与优先级继承修复
Go src/runtime/proc.go / sysmon(本机 Go 版本)work stealing、retake 抢占、netpoll 与调度的配合
xiaolincoding.com《图解系统》schedule.html调度/页面置换/磁盘调度算法的中文综述
收尾:OSTEP 与 LWN 是两大理论支柱,Go 源码给映射关系。总页数 12。注意:第 5 页笔记中 rt_throttled 默认值以 sched_rt_runtime_us=950000(95%)为准。