CPU 调度与系统负载详解
CPU 调度回答的是”有限的核心上,下一个让谁跑”的问题,load average 回答的是”此刻有多少任务在等”的问题。这篇从调度目标的取舍讲起,过一遍经典算法与 Linux CFS 的公平思想,最后落到线上最常见的那类怪象:负载很高,CPU 却闲着。
它解决什么问题
一台机器的逻辑 CPU 数量是固定的,想跑的任务却远不止这个数:业务线程、GC 线程、JIT 线程、网络事件循环、日志采集 Agent……8 核机器同一时刻最多只有约 8 个任务真正占用 CPU,其余全部排队。调度器就是决定”下一个谁上”的角色。
难办的是,调度追求的目标互相打架:
- 吞吐量:单位时间完成更多任务,希望少切换、单个任务一次跑久一点;
- 响应延迟:交互任务要尽快被看到,希望切换勤快、能及时抢占长任务;
- 公平性:不能让低优先级任务永远挨饿。
时间片拉长,吞吐上去了,交互卡顿;时间片缩短,响应好了,CPU 却把时间浪费在切换上。不存在三者同时拉满的策略,所有调度算法本质上都是在这个三角里选位置。
核心原理
任务离开 CPU 只有两大类原因。一类是主动让出:调 read 等数据、等锁、等定时器,线程进入睡眠,调度器顺势换人。另一类是被抢占:时钟中断给了内核一个检查机会,发现当前任务跑得够久了,或有更合适的任务就绪,就把它换下去。通用操作系统都是抢占式的,否则一个死循环就能焊死单核机器。
换人的动作叫上下文切换:保存当前任务的寄存器、程序计数器、栈指针,再恢复下一个任务的现场。跨进程切换还伴随地址空间更换,TLB 与 CPU 缓存的局部性一并受损。所以切换从来不是免费的——线程池开得远超核数时,业务代码没跑多少,CPU 时间先消耗在调度和同步上。
深入机制
经典算法:四个简化模型
- FCFS(先来先服务):按到达顺序执行,实现极简。代价是护航效应——一个长任务排前面,后面所有短任务陪着等,平均响应被拖垮。
- SJF(短作业优先):预计运行时间短的先跑,平均等待时间在理论上最优。致命前提是得提前知道每个任务还要跑多久,真实系统没有这种上帝视角;长任务还可能被新来的短任务无限插队,最终饿死。
- RR(时间片轮转):每个任务轮一个时间片,用完回队尾。时间片是那个关键旋钮:太大就退化成 FCFS,太小则上下文切换占比飙升。
- 多级反馈队列:新任务先进入高优先级队列;总是用完整个时间片的逐步降级(判定为 CPU 密集型),经常主动等 I/O 的保持高优先级(判定为交互型)。用运行行为猜测任务类型,是教科书里最接近实战的模型。
CFS:把公平做成记账
Linux 普通任务的调度器 CFS(Completely Fair Scheduler)不分配固定时间片,而是记账。每个任务维护一个 vruntime(虚拟运行时间):实际运行多久,按 nice 值对应的权重折算后累加进去——nice 越低权重越高,vruntime 涨得越慢,分到的 CPU 份额越大。
可运行任务按 vruntime 组织进一棵红黑树,调度器每次取最左节点,也就是在公平时间轴上”最吃亏”的那个任务。任务多了,每人分到的片段自动变短;任务少了,片段自动拉长。一句话:CFS 把公平定义为 vruntime 的均衡,而不是时间片的均分。(Linux 6.6 起 fair 调度类引入 EEVDF,用 lag 和虚拟截止时间做选择,对延迟敏感任务更友好,但公平记账的骨架没变。)
load average 统计的到底是什么
uptime 吐出的三个数字是 1、5、15 分钟尺度上的指数衰减平均,不是简单算术平均。关键在于统计口径:Linux 的 load 同时计入两类任务——
- R 状态:可运行,正在跑或在运行队列里排队;
- D 状态:不可中断睡眠,绝大多数是在等磁盘 I/O、网络存储或 Swap。
所以 load 高绝不等于 CPU 忙。判断时必须除以逻辑核数:8 核机器 load 8 可能只是刚好排满;load 40 而 CPU 有空闲,多半是一批任务卡在 D 状态等 I/O。反过来 load 不高但 CPU 100%,则是少数线程把核心打满,没有排队。
工程实践
排查顺序:先看趋势,再看构成,最后定位到人。
uptime # 1/5/15 分钟负载趋势,尖刺还是持续 |
对照着读:
us高:业务计算热点。Java 进程在 top 里按H切线程视图,线程号转十六进制去jstack里找栈,或用perf top、async-profiler 采样。sy/si高:时间花在内核路径——系统调用、网络协议栈、软中断。mpstat -P ALL 1看是否单核倾斜,警惕软中断集中在一个核上。- load 高、
wa高、CPU 闲:典型的 D 状态堆积。pidstat -d找谁在大量读写,iostat -x看哪块设备延迟高;别忘了 Swap、NFS 这类非本地盘的不可中断等待。 - load 高、
wa不高、vmstat的r长期大于核数:真的在抢 CPU。查线程数是否失控、锁竞争是否激烈,pidstat -w里nvcswch/s飙升就是被抢占过度的信号。
对比与权衡
| 算法 | 选择方式 | 优点 | 代价 |
|---|---|---|---|
| FCFS | 先到先跑 | 实现最简单 | 长任务拖住全部短任务 |
| SJF | 预计最短的先跑 | 平均等待最优 | 无法预知运行时长,长任务饥饿 |
| RR | 轮流一个时间片 | 响应有保证 | 时间片长短两难,切换开销 |
| 多级反馈队列 | 按行为动态升降级 | 兼顾交互与长任务 | 参数多,规则复杂 |
| CFS | vruntime 最小者优先 | 按权重公平分份额,无需估时长 | 记账与树操作有固定成本 |
面试视角
load average 到底是什么?
1、5、15 分钟的指数衰减平均值,统计 R 状态可运行任务加 D 状态不可中断睡眠任务。必须结合逻辑核数判断:load 等于核数大约是排满,远超核数才是排队或 I/O 堆积。
负载高但 CPU 使用率不高,怎么查?
说明大量任务不在抢 CPU,而是卡在 D 状态等 I/O。vmstat 1 看 b 和 wa,pidstat -d 定位读写大户,iostat -x 看设备延迟;同时排查 Swap 换入换出和网络存储。
CFS 怎么做到公平?
不给固定时间片,而是给每个任务记 vruntime:实际运行时间按 nice 权重折算累加,可运行任务按 vruntime 排进红黑树,调度器永远取最左(最吃亏)的任务。公平体现为长期份额与权重成正比。
时间片长点好还是短点好?
看你要什么。长则切换少、吞吐高,但交互任务等得久;短则响应快,但上下文切换的固定成本占比上升。这是一个响应与吞吐的取舍旋钮,没有通吃值。
上下文切换的开销在哪?
显式成本是保存和恢复寄存器、程序计数器、栈指针;隐式成本更贵——跨进程切换换地址空间,TLB 条目失效,CPU 缓存里的热数据被新任务顶掉。
小结
- 调度是在吞吐、延迟、公平之间取舍,FCFS/SJF/RR 是简化模型,CFS 用 vruntime 记账实现按权重的公平。
- 上下文切换有寄存器现场与 TLB、缓存的双重成本,线程不是越多越好。
- load average 统计 R+D 两类任务,必须除以核数读;负载高 CPU 闲,先怀疑不可中断 I/O 等待。
- 排查套路:uptime 看趋势、top 看构成、vmstat 看队列、pidstat/iostat 定位到人。

