CPU 调度
概念
CPU 调度是操作系统内核的核心功能之一:当多个进程(或线程)同时处于就绪状态时,调度器决定下一个获得 CPU 时间片的是谁。
调度器分为两层:
- 长期调度器(作业调度器):控制多少进程进入内存,影响系统并发度。
- 短期调度器(CPU 调度器):从就绪队列中选出下一个要运行的进程,频率极高(毫秒级)。
调度时机:进程从运行态切换到等待态、就绪态,或进程终止时,调度器介入。
核心原理
1. 调度基本概念
抢占式 vs 非抢占式
| 类型 | 描述 | 代表算法 |
|---|---|---|
| 非抢占式 | 进程一旦获得 CPU,只有主动放弃(等待 I/O 或退出)才会让出 | FCFS、非抢占 SJF |
| 抢占式 | 调度器可在任意时刻强制剥夺当前进程的 CPU | RR、SRTF、优先级抢占 |
抢占式调度响应性更好,适合交互式和实时系统;但引入了上下文切换开销和竞态条件。
CPU 密集型 vs I/O 密集型
- CPU 密集型:大部分时间在做计算,I/O 少(如视频编码、矩阵运算)。
- I/O 密集型:频繁发起 I/O 请求,大部分时间在等待(如 Web 服务器、数据库查询)。
好的调度策略会优先调度 I/O 密集型进程,让其尽快发起 I/O,同时让 CPU 密集型进程在后台占满 CPU。
调度指标
| 指标 | 含义 |
|---|---|
| 周转时间(Turnaround Time) | 进程从提交到完成的总时间 |
| 等待时间(Waiting Time) | 进程在就绪队列中等待的累计时间 |
| 响应时间(Response Time) | 提交请求到首次得到响应的时间 |
| 吞吐量(Throughput) | 单位时间内完成的进程数 |
| CPU 利用率(CPU Utilization) | CPU 处于忙碌状态的时间比例 |
不同场景的优化目标不同:批处理系统追求吞吐量和周转时间;交互式系统追求响应时间;实时系统追求截止期满足率。
2. CPU 负载与利用率(高频排查必备)
面试和线上排查最常被问的一组概念:负载(Load)≠ 利用率(Utilization)。前者衡量"有多少任务在排队",后者衡量"CPU 时间花在哪"。
Load Average(平均负载)
uptime / top 第一行的 load average: 1.20, 0.95, 0.80 是过去 1 / 5 / 15 分钟 的平均负载——指处于可运行(R)+ 不可中断(D)状态的任务平均数。
load average: 1.20, 0.95, 0.80
└1min └5min └15min
1min > 5min > 15min → 负载在上升(要警惕)
1min < 5min < 15min → 负载在回落⚠️ Linux 的特殊点:D 状态也计入负载
其他 Unix 只算 R(等 CPU)的任务,Linux 把 D 状态(不可中断睡眠,通常在等磁盘 / NFS I/O)也算进 load。 所以会出现 load 飙到 100+ 但 CPU 利用率很低 的诡异现象 → 大概率是磁盘 / 网络 I/O 卡住,一堆进程卡在 D 状态,不是 CPU 忙。见 进程状态 D。
怎么判断 load 高不高:要除以核数。
load average 8.0 在 1 核机器 = 严重过载(800%)
load average 8.0 在 16 核机器 = 很轻松(50%)
经验阈值: load / 核数 持续 > 1 → 排队,可能要扩容
load / 核数 持续 > 0.7 → 开始关注CPU 利用率的 8 个分量(必背)
top / mpstat 把 CPU 时间拆成 8 块,看哪一块高,直接定位瓶颈方向:
| 分量 | 含义 | 高了意味着 |
|---|---|---|
| us(user) | 用户态代码 | 业务计算重 / 死循环 / 序列化 / 正则回溯 |
| sy(system) | 内核态 | 系统调用频繁 / 上下文切换 / 中断多 |
| ni(nice) | 调过 nice 的用户进程 | 低优先级任务在跑 |
| id(idle) | 空闲 | 越高 CPU 越闲 |
| wa(iowait) | 等待 I/O 完成 | 磁盘 / 网络 I/O 瓶颈(不是 CPU 忙) |
| hi(hardirq) | 硬中断 | 网卡 / 设备中断风暴 |
| si(softirq) | 软中断 | 高并发网络收发包(见 系统调用与中断) |
| st(steal) | 被虚拟化偷走的 CPU | 宿主机超卖,云主机邻居太吵 |
排查口诀:
us高查应用代码;sy高查系统调用 / 上下文切换;wa高查磁盘 I/O;si高查网络;st高换宿主机 / 找云厂商。
Load vs Utilization 的本质区别(经典面试题)
| Load Average | CPU Utilization | |
|---|---|---|
| 衡量 | 排队长度(多少任务在等 + 在跑) | 时间占比(CPU 忙的比例) |
| 上限 | 无上限(可以 100+) | 0~100%(每核) |
| Linux 口径 | R + D 状态 | 只看 CPU 实际执行 |
| 典型背离 | I/O 卡住 → load 高 + util 低 | 单核打满 → util 100% + load≈1 |
一句话答:利用率是"CPU 有多忙",负载是"有多少人在排队"。CPU 100% 利用率但 load=1 说明刚好喂饱;load 远大于核数说明任务在排队、响应变慢。Linux 的 load 还包含 D 状态 I/O 等待,所以 load 高不一定是 CPU 问题。
云原生新指标:PSI(Pressure Stall Information)
传统 load average 有缺陷(含 D 状态、不分资源维度)。Linux 4.20+ 引入 PSI,按 CPU / 内存 / IO 三个维度精确量化"因为资源不足而被卡住的时间占比":
cat /proc/pressure/cpu
# some avg10=2.04 avg60=0.50 ... ← 部分任务因等 CPU 被阻塞的时间比例
cat /proc/pressure/io
cat /proc/pressure/memoryK8s / cgroup v2 用 PSI 做更精准的资源压力感知和驱逐决策,比 load average 更适合容器环境。
3. 调度算法的工作负载适配
调度器的核心矛盾来自两类工作负载(workload):
| CPU 密集型(CPU-bound) | I/O 密集型(I/O-bound) | |
|---|---|---|
| 特征 | 大部分时间在算 | 大部分时间在等 I/O |
| 例子 | 视频编码、矩阵运算、加解密 | Web 服务、数据库、文件上传 |
| 瓶颈 | CPU 核数 | 磁盘 / 网络 / 下游延迟 |
| 调度诉求 | 长时间片、少切换 | 短时间片、快响应 |
| 线程池大小 | ≈ 核数 + 1 | 核数 ×(1 + 等待/计算时间),可远大于核数 |
线程池大小经验公式(Brian Goetz):
其中 = 等待时间 / 计算时间。纯 CPU 型 → 线程数≈核数;I/O 型等待远大于计算 → 线程数可以是核数的几十倍(这也是 虚拟线程 在 I/O 密集场景碾压传统线程池的原因)。
好的调度策略会优先调度 I/O 密集型进程,让它尽快发起 I/O 后去等待,同时让 CPU 密集型进程在后台占满 CPU——这正是下面 MLFQ 多级反馈队列的设计动机。
4. 经典调度算法
FCFS(先来先服务,First-Come First-Served)
按到达顺序排队,非抢占式。
示例:进程 P1(运行 24ms)、P2(3ms)、P3(3ms)按顺序到达。
| 进程 | 到达时间 | 运行时间 | 完成时间 | 周转时间 | 等待时间 |
|---|---|---|---|---|---|
| P1 | 0 | 24 | 24 | 24 | 0 |
| P2 | 0 | 3 | 27 | 27 | 24 |
| P3 | 0 | 3 | 30 | 30 | 27 |
平均等待时间 = (0 + 24 + 27) / 3 = 17ms
护航效应(Convoy Effect):一个长进程阻塞后面所有短进程,导致整体性能下降。
SJF / SRTF(最短作业优先 / 最短剩余时间优先)
- SJF:非抢占,选择预计运行时间最短的进程。
- SRTF:抢占式版本,若新到进程的剩余时间更短,立即抢占当前进程。
理论上 SJF 给出最优平均等待时间,但实际问题在于:
- 无法准确预知运行时间(通常用历史均值指数平均估算)。
- 饥饿(Starvation):长进程可能永远得不到 CPU,因为不断有短进程插入。
RR(时间片轮转,Round Robin)
每个进程分配固定时间片(quantum),时间片用完后强制切换到下一个就绪进程,循环执行。
时间片大小的权衡:
| 时间片过小 | 时间片过大 |
|---|---|
| 响应时间好 | 上下文切换开销小 |
| 上下文切换频繁,CPU 利用率下降 | 退化为 FCFS,响应时间差 |
经验值:时间片通常设为 10~100ms,上下文切换时间约 0.1ms,开销占比控制在 1% 以内。
优先级调度
每个进程附带优先级,调度器始终选择优先级最高的进程(可抢占或非抢占)。
问题:优先级反转(Priority Inversion)
高优先级进程 H 等待低优先级进程 L 持有的锁,而中优先级进程 M 抢占了 L,导致 H 间接被 M 阻塞。
解决方案:
- 优先级继承协议(Priority Inheritance Protocol, PIP):L 持有锁期间临时继承 H 的优先级,阻止 M 插队。
- 优先级天花板协议(Priority Ceiling Protocol):锁的优先级上限等于所有可能持有该锁的进程中最高优先级,获取锁时直接提升至该上限。
- 老化(Aging):随时间推移逐渐提升低优先级进程的优先级,防止饥饿。
MLFQ(多级反馈队列,Multi-Level Feedback Queue)
综合了 RR 与优先级调度,是现代通用操作系统调度的基础思想。
规则:
- 多条队列,优先级从高到低,高优先级队列时间片短。
- 新进程进入最高优先级队列。
- 若一个进程用完时间片仍未完成,降级到下一优先级队列。
- 若一个进程在时间片内主动放弃 CPU(等待 I/O),保持当前优先级。
- 定期将所有进程提升到最高优先级(防止饥饿)。
效果:短进程(I/O 密集型)始终在高优先级队列获得快速响应;长进程(CPU 密集型)自然沉降到低优先级队列。无需提前知道运行时间,自适应性强。
调度算法对比表
| 算法 | 抢占性 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| FCFS | 非抢占 | 实现简单,公平 | 护航效应,平均等待时间长 | 批处理系统 |
| SJF | 非抢占 | 最优平均等待时间 | 需预知运行时间,长进程饥饿 | 批处理系统 |
| SRTF | 抢占 | 最优平均等待时间 | 切换开销大,饥饿 | 理论最优基准 |
| RR | 抢占 | 响应时间均匀,无饥饿 | 时间片设置敏感,上下文切换多 | 交互式系统 |
| 优先级 | 两者皆可 | 支持差异化服务 | 优先级反转,低优先级饥饿 | 实时 / 嵌入式 |
| MLFQ | 抢占 | 自适应,兼顾吞吐与响应 | 实现复杂,参数调优难 | 通用操作系统 |
5. Linux CFS(完全公平调度器)
Linux 内核(2.6.23+)的默认调度器,用于普通(SCHED_NORMAL/SCHED_BATCH)进程。
核心思想:不分配固定时间片,而是让每个进程获得等比例的 CPU 时间——即"完全公平"。
vruntime(虚拟运行时间)
每个进程维护一个 vruntime 值,表示该进程已经消耗的"加权虚拟时间":
- :实际运行时间。
weight:由 nice 值映射而来,nice 值越低(优先级越高),weight 越大,vruntime 增长越慢,因而获得更多 CPU 时间。
红黑树(Red-Black Tree)组织就绪队列
CFS 用红黑树按 vruntime 排序所有可运行进程,最左节点就是 vruntime 最小的进程,即下一个应该运行的进程。选择复杂度为 ,插入/删除同为 。
nice 值与权重映射
nice 值范围 -20(最高优先级)到 +19(最低优先级)。内核维护一张预计算的 prio_to_weight 表,相邻 nice 值之间权重比约为 1.25,使得 nice 值差 1 对应约 10% 的 CPU 时间差异。
为什么叫"完全公平"?
理想情况下,n 个权重相同的进程各获得 的 CPU 时间。CFS 通过不断调度 vruntime 最小的进程,使所有进程的 vruntime 趋于相等,在宏观上实现了按权重比例的公平分配。
6. 实时调度
硬实时 vs 软实时
| 类型 | 描述 | 示例 |
|---|---|---|
| 硬实时 | 必须在截止期内完成,错过即系统失效 | 汽车 ABS、飞控系统 |
| 软实时 | 尽量在截止期内完成,偶尔错过可接受 | 视频播放、音频流 |
Linux 实时调度策略
Linux 提供两种实时调度策略,优先级高于 CFS:
SCHED_FIFO:实时 FCFS,无时间片。进程一旦运行,除非主动放弃或被更高优先级进程抢占,否则一直运行。SCHED_RR:实时 RR,有时间片。时间片用完后,同优先级的实时进程间轮转。
实时优先级范围为 1~99(数字越大优先级越高),均高于所有普通进程(优先级为 0)。
优先级反转与优先级继承协议
Linux POSIX 互斥锁支持 PTHREAD_PRIO_INHERIT 属性,启用后实现优先级继承:当高优先级线程等待锁时,持锁的低优先级线程临时提升至高优先级,确保其尽快释放锁,避免中间优先级线程横插。
7. nice 值与生产调优实战
nice 是普通用户最常用的优先级控制工具,2025-2026 年 Linux 面试常考。
nice 值范围与影响
nice 取值: -20 ~ +19
-20 = 最高优先级(CFS 权重最大)
+19 = 最低优先级(CFS 权重最小)
0 = 默认
权重映射: weight = 1024 / (1.25 ^ nice)
nice = 0 → weight 1024
nice = -20 → weight 88761 (约 86×)
nice = +19 → weight 15 (约 1/68×)实战命令:
# 启动时指定 nice
nice -n 10 ./batch-job.sh # 低优先级跑批
# 修改运行中进程
renice -n -5 -p 12345 # 提升 PID 12345 优先级(需 root)
# 实时调度
chrt -f 50 ./latency-critical.sh # SCHED_FIFO, 优先级 50
chrt -r 30 ./video-server.sh # SCHED_RR, 优先级 30
chrt -p <pid> # 查看进程调度策略⚠️ 普通用户只能降低优先级
非 root 用户只能把 nice 调大(降低优先级),不能调小。这是 Linux 防止用户进程抢系统资源的硬约束。
8. cgroups:容器化时代的 CPU 调度核心
cgroups(Control Groups) 是 K8s / Docker 实现 CPU 限制的底层机制。容器化运维必问——能讲清楚 CPU requests / limits 怎么映射到 cgroups,是 SRE 加分点。
cgroups v1 vs v2
| 版本 | 状态 | 关键差异 |
|---|---|---|
| v1 | 老版本,逐步淘汰 | 每个子系统独立层级(cpu / memory / blkio 分开) |
| v2 | RHEL 9 / Ubuntu 22+ 默认 | 统一层级(一个 cgroup 同时控制 CPU+内存+IO) |
CPU cgroups 三种限制方式
1. cpu.shares (v1) / cpu.weight (v2):
相对权重,CPU 紧张时按权重分配
例: container-A weight=100, container-B weight=200
→ CPU 紧张时 A:B = 1:2 分配
2. cpu.cfs_quota_us / cpu.cfs_period_us (v1)
cpu.max (v2): "200000 100000" ← quota / period
绝对上限: 每 period(默认 100ms) 最多用 quota 微秒
K8s 的 limits.cpu=2 → quota=200000, period=100000
3. cpuset.cpus:
绑定到指定 CPU 核心,避免跨核切换
例: cpuset.cpus="0-3" → 只能在 0-3 号核运行K8s CPU requests / limits 映射
resources:
requests:
cpu: "500m" # 0.5 核
limits:
cpu: "2" # 2 核requests.cpu=500m → cpu.weight = 51 (v2) ← 影响 CPU 紧张时调度优先级
limits.cpu=2 → cpu.max = "200000 100000" ← 硬上限⚠️ CPU Throttling 陷阱
K8s 的 CPU
limits是硬上限——即使节点空闲,超 limits 也会被强制降速(throttle),导致响应延迟突刺。
生产实践:
- ① 不要给延迟敏感服务设 CPU limits(只设 requests)
- ② 或用 Burstable QoS:
requests < limits,但理解会被 throttle - ③ 监控
container_cpu_cfs_throttled_seconds_total,超过 5% 立即扩容
9. EEVDF:CFS 的继承者(Linux 6.6+)
EEVDF(Earliest Eligible Virtual Deadline First) 是 Linux 6.6(2023.10)引入的新默认调度器,正在逐步替代 CFS。能讲到 EEVDF 是了解前沿动态的强信号。
| 维度 | CFS | EEVDF |
|---|---|---|
| 核心思想 | 最小 vruntime 优先 | 最早 virtual deadline 优先 |
| 延迟优化 | 弱 | 强(每任务可配 latency-nice 期望延迟) |
| 公平性 | 长期公平 | 保持公平 + 短期可定制延迟 |
| 状态 | Linux ≤ 6.5 默认 | 6.6+ 默认 |
EEVDF 的关键优势:交互式任务可以声明"我希望 5ms 内被调度",调度器尽量满足;批处理任务声明"我不在乎延迟",给它更长的运行片提升吞吐。
面试常问 & 怎么答
Q1:常见的 CPU 调度算法有哪些?各有什么优缺点?
先列举算法名称,再抓核心特点:FCFS 简单但有护航效应;SJF 理论最优但无法预知运行时间且有饥饿;RR 响应均匀但时间片选择敏感;优先级调度灵活但有优先级反转风险;MLFQ 综合最优,现代 OS 的基础。面试时重点展开 MLFQ 的设计思路(多队列 + 反馈降级 + 定期提升)和 RR 的时间片权衡,体现系统性思维。
Q2:Linux 用的是什么调度算法?CFS 是怎么工作的?
回答分三层:① 背景——Linux 2.6.23 引入 CFS 替代旧的 O(1) 调度器;② 核心机制——vruntime 记录加权虚拟时间,红黑树排序就绪队列,始终调度 vruntime 最小的进程;③ 公平性——nice 值通过 prio_to_weight 表映射权重,权重影响 vruntime 增长速率,从而按比例分配 CPU。能画出红黑树示意图或写出 vruntime 公式会加分。
Q3:什么是优先级反转?怎么解决?
先描述场景:高优先级进程 H 因等待低优先级进程 L 持有的共享资源而阻塞,中优先级进程 M 抢占 L,导致 H 间接等待 M——这就是优先级反转。然后给出解决方案:优先级继承协议(L 临时继承 H 的优先级)和优先级天花板协议(获锁时提升至预设上限)。可以提 Mars Pathfinder 真实案例(1997 年 NASA 火星探路者遭遇优先级反转导致系统重启),展示工程经验。
Q4:load average 和 CPU 利用率有什么区别?load 很高一定是 CPU 忙吗?
利用率衡量"CPU 时间花在哪"(0~100%/核),load 衡量"有多少任务在排队"(无上限)。关键反例:load 很高但 CPU 利用率很低——因为 Linux 把不可中断 D 状态(等磁盘 / 网络 I/O)也计入 load,所以这种情况通常是 I/O 瓶颈而非 CPU 忙。判断 load 高不高要除以核数(load/核数 > 1 才算排队)。再补一句进阶:容器环境更推荐看 PSI(/proc/pressure/*),按 CPU/内存/IO 分维度量化资源压力。
Q5:线程池大小怎么定?
看工作负载类型。CPU 密集型 → 线程数≈核数+1(再多只会增加上下文切换);I/O 密集型 → 按 Goetz 公式 核数 ×(1 + 等待/计算),等待远大于计算时可以是核数的几十倍。本质是让 CPU 在某些线程等 I/O 时还有别的线程可跑。JDK 21+ 的虚拟线程把这个公式推到极致——I/O 密集场景直接每请求一线程、百万并发。
看到什么就先想到这类
| 关键词 / 场景 | 联想到 |
|---|---|
| 进程等待时间长、短进程被阻塞 | FCFS 护航效应 |
| 调度算法理论最优 | SJF/SRTF,但注意饥饿 |
| 交互式系统、响应时间均匀 | RR 时间片轮转 |
| load 高但 CPU 利用率低 | I/O 瓶颈 + D 状态进程,不是 CPU 忙 |
| top 里 wa 高 | 磁盘 / 网络 I/O 等待 |
| top 里 st 高 | 虚拟化超卖,宿主机邻居太吵 |
| top 里 sy 高 | 系统调用 / 上下文切换频繁 |
| 容器精准资源压力监控 | PSI(/proc/pressure)优于 load average |
| 线程池大小怎么定 | CPU 密集≈核数+1;I/O 密集按等待/计算比放大 |
| 高优先级进程莫名阻塞 | 优先级反转,考虑优先级继承 |
| Linux 普通进程调度 | CFS + vruntime + 红黑树 |
| Linux 实时进程 | SCHED_FIFO / SCHED_RR,优先级 1~99 |
| 系统需要同时兼顾响应和吞吐 | MLFQ 多级反馈队列 |
| nice 值 / 进程优先级调整 | CFS 权重映射,影响 vruntime 增长速率 |