CPU 调度与系统负载详解
CPU 调度不只是 FCFS、RR、CFS 这些算法名词。排查线上问题时,还要弄清线程为什么被换下去、上下文切换的成本在哪里,以及 load average 很高但 CPU 使用率不高意味着什么。
这些问题背后是同一组约束:CPU 核心有限,任务需要排队,调度器负责决定谁先运行;系统指标则帮助判断任务是在争抢 CPU、等待 I/O,还是卡在内核、中断或虚拟化层。
比如一台有 8 个逻辑 CPU 的机器,load average 已到 40,但 CPU 还有空闲,%Cpu(s) 里的 wa 长期偏高。此时直接去找 CPU 热点函数可能没有结果,机器上更可能堆了一批等待 I/O 的任务。CPU 使用率和 load average 需要分开判断。
为什么需要 CPU 调度
CPU 核心有限,可运行任务可能很多。
在一个 Java 服务中,业务线程、GC 线程、JIT 线程、Netty 事件循环线程都可能要跑;同机还可能有日志采集、监控 Agent、定时任务和数据库客户端。如果系统可用的是 8 个逻辑 CPU,同一时刻最多只能让大约 8 个可运行任务占用 CPU,其余任务只能排队、睡眠或等待 I/O。容器环境还要看 CPU quota,不能只看宿主机物理核心数。
不同系统对调度对象的叫法不完全一样。后端排查时,可先把 Linux 的调度实体理解成能被内核单独安排到 CPU 上运行的执行单元。
一个进程可包含多个线程。它们共享进程地址空间和文件描述符,但各自有栈、寄存器、程序计数器等执行现场。

在 Linux 内核里,进程和线程都用 task 表示,调度器实际调度的是 task 或调度实体;用户态看到的一条线程,大多对应一个内核可调度任务。
如果没有调度,单核机器上的一个死循环就能一直占着 CPU,其他程序没有机会响应。多核机器只是把同一时刻能运行的任务数从 1 个变成 N 个;任务数超过核心数后,仍然要排队和切换。
调度器要兼顾交互响应、公平性、吞吐量、优先级、实时任务、功耗和缓存局部性。这些目标经常互相牵制:时间片长一些,切换会减少,但交互任务可能等更久;时间片短一些,响应会改善,切换开销又会上升。

任务离开 CPU 的几种情况
一个任务离开 CPU,大致有几类原因。时间片用完只是其中一种情况。
最常见的是任务主动让出 CPU。比如线程调用 read() 读取磁盘,数据还没有准备好,它就会进入等待;线程等待锁、条件变量、定时器时,也会从运行态离开。CPU 不应该陪着它空等,调度器会选择其他可运行任务。
另一类是被抢占。通用操作系统通常选用抢占式调度,任务运行一段时间后,时钟中断会给内核一个检查机会;如果当前任务已经运行够了,或者有更合适的任务变为可运行,内核就可能把当前任务换下去。
很多教材为了讲清楚抢占,常常把这个过程简化成:定时器周期性地产生时钟中断。
这个模型可以说明抢占的大致过程。不过,现代 Linux 支持 NO_HZ / tickless,会在空闲 CPU 或特定配置下减少调度时钟 tick。定时器和调度 tick 是内核获得抢占检查机会的重要机制,线上机器未必一直按固定频率打 tick。
优先级也会影响调度。高优先级任务排得更靠前,低优先级任务就更容易等待。如果系统完全偏向高优先级任务,低优先级任务可能长期拿不到 CPU,这就是饥饿。教材算法常用动态优先级、老化或队列提升来缓解饥饿;真实系统的处理方式取决于调度类和具体实现。
上下文切换发生在换任务的那一刻。内核要保存当前任务的寄存器、程序计数器、栈指针等现场,再恢复下一个任务。跨进程切换还可能带来页表、TLB、缓存局部性的额外成本。线程很多、锁竞争严重、任务频繁睡眠和唤醒时,业务代码没运行多少,CPU 时间可能先花在调度和同步上。
线程数量需要结合任务类型和 CPU 核数设置。线程可掩盖 I/O 等待,也可利用多核;但线程数量远大于 CPU 核数时,运行队列、上下文切换、缓存失效、锁竞争都会随之上升。
经典调度算法
面试里经常会问 FCFS、SJF、RR、优先级、多级反馈队列。
把这些算法放到短任务、长任务和交互任务如何排队的场景中,更容易看出差异。它们主要是教材中的简化模型;真实 Linux 的普通任务调度不会直接照搬某一个算法,还会涉及 CFS/EEVDF、实时调度类、cgroup、CPU affinity、NUMA 等机制。
| 算法 | 选择方式 | 容易被追问的问题 |
|---|---|---|
| FCFS | 先到先服务 | 长任务排在前面,短任务也要等待 |
| SJF | 预计运行时间短的先运行 | 很难提前知道任务还要运行多久,长任务可能饥饿 |
| RR | 每个任务轮流运行一个时间片 | 时间片太短会放大上下文切换,太长又接近 FCFS |
| 优先级调度 | 优先级高的先运行 | 低优先级任务可能长期等不到 CPU |
| 多级反馈队列 | 多个优先级队列,按运行行为调整位置 | 规则和参数较多,实现更复杂 |
举个例子,线程池前面排了几个大文件压缩任务,后面很多只查缓存的请求也要跟着等待,平均响应时间会被长任务拖坏。SJF 可改善这场景,但前提很强:系统得知道每个任务还要运行多久。真实系统没有这种上帝视角,只能根据历史行为、I/O 等待、交互特性来猜。
RR 更像分时系统的入门模型。每个任务运行一个时间片,运行完放回队列。用户敲命令、移动鼠标、发请求时,不必等长任务完全结束才有响应。上下文切换存在固定成本,因此时间片越短,切换开销占比越高;时间片拉长后,切换成本被摊薄,交互延迟又可能变大。
多级反馈队列同时考虑短任务的响应时间和长任务的推进。新任务一般先进入高优先级队列;如果它总是用完整个时间片,更接近 CPU 密集型长任务,可逐步降级;如果它经常主动等待 I/O,更接近交互或 I/O 型任务,可保留较高优先级。队列数量、时间片和升降级规则都会影响调度效果,实际系统还会叠加更多机制。
从 CFS 到 EEVDF
Linux 普通任务调度长期使用 CFS,也就是 Completely Fair Scheduler。
理解 CFS 时,先看 vruntime。
vruntime 记录任务在公平时间轴上已经运行了多少。任务真实运行一段时间后,内核会把这段时间折算进它的虚拟运行时间;nice 值不同,权重不同,折算速度也不同。调度器倾向于选择 vruntime 更小的任务,让各个任务长期按权重分到 CPU。
CFS 没有旧调度器那种固定 timeslice 概念,它更接近在一段时间内按权重分配 CPU 份额。可运行任务少,每个任务可多运行一点;可运行任务多,每个任务分到的片段会变短。CFS 使用红黑树维护按虚拟运行时间排序的可运行任务,通常选择最左边,也就是在公平时间轴上相对获得 CPU 较少的任务。
Linux 6.6 开始在普通任务调度中引入 EEVDF,也就是 Earliest Eligible Virtual Deadline First。
EEVDF 仍然围绕公平分配 CPU 展开,在选择任务时引入了 lag 和虚拟截止时间。lag 为正,表示任务还欠着 CPU 时间;符合条件的任务中,虚拟截止时间更早的任务优先运行。延迟敏感、请求较短时间片的任务,会更早获得调度机会。
在 Linux 的代码和工具输出中,普通任务仍归到 fair 调度类。EEVDF 改变的是 fair class 中的任务选择逻辑,并不意味着所有调度概念都换了一套名字。
线上机器是否已经使用 EEVDF,要看实际内核版本,以及发行版是否回补或调整了相关补丁。生产排查时,不要默认所有机器都是同一套调度实现。
后端面试通常需要说明 CFS 的 vruntime、权重和公平份额,以及 EEVDF 的 lag、虚拟截止时间和延迟敏感任务。更细的内容会涉及内核实现。

load average 和 CPU 使用率不是一回事
uptime 里看到的三个 load average 数字,对应 1、5、15 分钟时间尺度上的平均负载。它们是指数衰减平均值,并不是最近 N 分钟采样值的简单算术平均。
load average 统计 R 状态的可运行任务,以及 D 状态的不可中断睡眠任务。D 状态经常与 I/O 有关,但排查时不能只盯本地磁盘,块设备、网络存储、文件系统、Swap 等不可中断等待路径都要考虑。
因此,load 高不等于 CPU 被打满。它既可能来自可运行任务争抢 CPU,也可能是大量任务卡在不可中断等待中。
判断 load 必须结合逻辑 CPU 数。8 个逻辑 CPU 的机器 load 8 左右,可能只是 CPU 被排满;load 40 通常说明大量任务在排队或处于不可中断睡眠。1 个逻辑 CPU 的机器 load 8 已经很紧张,64 个逻辑 CPU 的机器 load 8 可能还较轻。
/proc/loadavg 第四个字段形如 3/1024。斜杠前面是当前可运行的内核调度实体数量,后面是系统当前存在的调度实体总数。这个字段补充了采样时刻的任务数量,可与前三个平均负载值一起判断。
CPU 使用率看的是 CPU 时间花到了哪里。top 里的 %Cpu(s) 常见字段可这样读:
us:未调整 nice 值的用户态时间。业务计算、JSON 序列化、正则、压缩、加解密常落在这里。ni:调整过 nice 值的用户态时间。常见于被调低优先级的用户进程。sy:内核态时间。系统调用、网络协议栈、文件系统、内核锁竞争会抬高它。wa:I/O wait。它表示 CPU 空闲且系统有未完成 I/O 请求的时间,适合作为排查线索,不能单独用于精确归因。id:空闲时间。CPU 没事做,或者任务堵在别的资源上。hi/si:硬中断 / 软中断时间。网络包量大、网卡中断集中、协议栈处理压力大时要关注。st:虚拟化环境里被宿主机拿走的 CPU 时间。云主机上这值高时,先不要急着改业务代码。

wa 尤其容易被误读。iowait 不是可靠的归因:CPU 不会真的等待 I/O 完成;在多核系统里,等待 I/O 的任务也不运行在某个 CPU 上。因此,wa 高只能说明系统存在 I/O 等待线索,不能直接说明 CPU 忙于 I/O。
下一步,应该看 vmstat 的 b、bi/bo、si/so,再用 pidstat -d 和 iostat -x 找具体进程和块设备。
从 CPU 报警开始排查
排查时不要一上来就钻进 Java 栈。先把压力类型分出来,再下钻到进程、线程、CPU 核和热点函数。

uptime 先看负载趋势。1 分钟高、5 分钟和 15 分钟不高,可能是短时尖刺;三个值都高,说明压力已持续了一段时间。再打开 top,看 %Cpu(s) 里是 us、ni、sy、wa、si 还是 st 抬头,同时看进程排序和任务状态。
如果 us 很高,先找业务热点。Java 进程可在 top 里按 H 切到线程视图,找到高 CPU 线程,把线程 ID 转成十六进制,再去 jstack 或 jcmd Thread.print 里找对应栈。单次 jstack 只是一瞬间,最好连续抓 2~3 次;如果同一个线程多次停在同一段业务栈,可信度会更高。也可以使用:
pidstat -u -t -p <pid> 1它可按线程查看 CPU 使用率。定位到线程后,再看它是在业务循环、序列化、正则、加解密,还是在 GC/JIT。jstack 适合看线程当下栈帧;要找 CPU 热点,perf top、async-profiler 这类采样工具更可靠。
如果 sy 或 si 很高,先不要只盯 Java 栈。大量短连接、网络收包、文件 I/O、系统调用、软中断都可能把 CPU 时间抬到内核态。可以使用:
mpstat -P ALL 1
sudo perf topmpstat 看是不是某几个 CPU 核特别忙,perf top 看热点符号落在用户态函数、内核网络栈、软中断,还是锁相关路径。某个核心 100%、其他核心很空时,要留意单线程瓶颈、软中断集中在单核、绑核配置或队列倾斜。
如果 load 高、wa 也高,先跑:
vmstat 1重点看 r、b、wa、bi、bo、si、so。r 是可运行任务数量,b 不是所有睡眠线程数量,而是阻塞等待 I/O 的任务数量;bi/bo 是块设备读写吞吐,单位通常是 KiB/s,不是 I/O 请求次数;si/so 是 Swap 换入换出。wa 高同时 b、bi/bo 高,继续查磁盘;wa 高同时 si/so 高,内存压力和 Swap 可能已经把服务拖慢。
再用:
pidstat -d -p ALL 1
iostat -x 1看哪个进程在读写、哪个块设备延迟高。iostat -x 重点看 await、aqu-sz、读写吞吐和请求数,%util 对机械盘有参考价值;RAID、SSD、NVMe 能并行处理请求,不能只靠它判断打满。iostat 第一行通常是自启动以来的平均值,排查当前问题时更应该看后续采样;必要时可用 iostat -x -y 1 跳过第一行。
业务进程 I/O 不高但系统 wa 高,也要看日志压缩、备份、数据库、镜像拉取、同节点其他容器。
如果系统支持 PSI,也可看:
cat /proc/pressure/cpu
cat /proc/pressure/io
cat /proc/pressure/memoryPSI 看的是任务因为 CPU、内存、I/O 压力停住了多久。some 表示至少有任务因为对应资源不足而停顿;对内存和 I/O,full 表示所有非 idle 任务同时停顿。系统级 /proc/pressure/cpu 的 full 没有诊断意义,排查 CPU 压力时主要看 some。PSI 能直接反映业务是否因资源压力而停顿,单看 CPU 使用率无法得到这个信息。
容器环境里还要看 cgroup 限制。一个容器只分到 2 核 quota,即使宿主机有 64 核,容器内的任务也可能已经在排队。排查时要结合 cpu.max、cpu.stat、cpu.pressure、memory.current、memory.events、memory.pressure、io.stat、io.pressure 等 cgroup 指标,而不是只看宿主机总体 CPU。这里列的是 cgroup v2 常见文件名;如果系统还在使用 cgroup v1,路径和文件名会分散在不同 controller 目录下。
如果 load 高、wa 不高,vmstat 1 里的 r 长期明显大于 CPU 核数,说明可运行任务在排队。接着看线程数、线程池、锁竞争和上下文切换:
vmstat 1
pidstat -w -p ALL 1
ps -eo pid,ppid,stat,ni,pri,psr,pcpu,comm --sort=-pcpu | headvmstat 的 cs 可看到系统上下文切换频率,pidstat -w 里重点看 cswch/s 和 nvcswch/s。前者是自愿上下文切换,常见于等待 I/O、锁、条件变量;后者是非自愿上下文切换,常见于时间片用完后被抢占。线程池太大时,r、cs、CPU 使用率一起上升,请求延迟反而变差,继续加线程只会更堵。
time 命令适合看一个单次命令把时间花在哪里:
/usr/bin/time -p <command>real 是墙钟时间,user 是用户态 CPU 时间,sys 是内核态 CPU 时间。real 很长但 user + sys 不高,常见于等待 I/O、网络或锁;user 很高,说明计算本身消耗 CPU;sys 高,则要看系统调用和内核路径。
常用排查命令
下面这些命令可以先把大多数 CPU/load 问题分出方向:
| 命令 | 常用写法 | 主要看什么 |
|---|---|---|
uptime | uptime | 1、5、15 分钟 load average |
top | top,进入后按 H | 总 CPU、进程/线程 CPU、任务状态、load |
vmstat | vmstat 1 | r、b、us/sy/wa/id/st、cs、bi/bo、si/so |
pidstat | pidstat -u -d -w -t -p <pid> 1 | 单进程/线程 CPU、I/O、上下文切换 |
iostat | iostat -x 1 | 块设备吞吐、队列长度、平均等待时间、设备利用率 |
mpstat | mpstat -P ALL 1 | 每个 CPU 核的使用率、iowait、softirq、steal |
ps | ps -eo pid,stat,ni,pri,psr,pcpu,comm --sort=-pcpu | 进程状态、优先级、CPU 核、CPU 占用 |
perf top | sudo perf top | 实时 CPU 热点函数,区分用户态和内核态热点 |
PSI | cat /proc/pressure/{cpu,io,memory} | CPU、I/O、内存压力导致的任务停顿比例 |
perf top 的结果取决于 perf 权限以及内核符号、用户态符号和 JIT 符号能否解析;Java 场景下必要时还要结合 async-profiler。
面试回答要点
回答 CPU 调度时,需要交代任务为什么排队、内核何时切换任务以及切换会产生哪些成本。这比只背算法名称更完整。
CPU 调度
CPU 核心数有限,可运行的进程和线程可能很多。调度器从可运行队列里挑任务上 CPU;任务阻塞、时间片用完、优先级变化,或有更合适任务出现时,内核会调度和上下文切换。调度要在响应时间、吞吐量、公平性和切换开销之间做取舍,线程开太多反而可能把时间花在排队和切换上。
经典调度算法怎么回答
FCFS 简单,但长任务会拖住短任务;SJF 平均周转时间好,但很难知道任务长度,也可能让长任务饥饿;RR 借助时间片改善响应时间,时间片太短会放大切换开销;优先级调度能表达任务紧急程度,但要处理低优先级饥饿;多级反馈队列会根据任务运行行为调整队列位置,尽量照顾交互任务,同时让长任务继续推进。
Linux 调度
Linux 普通任务调度不能直接套某个教材算法。CFS 用虚拟运行时间和权重分配 CPU,倾向选择已经获得 CPU 较少的任务;EEVDF 继续围绕公平份额做选择,用 lag 判断任务是否欠 CPU,再按虚拟截止时间选择任务。普通后端岗位讲到这层面即可。
load average 和 CPU 使用率
load average 统计 R 状态的可运行任务和 D 状态的不可中断睡眠任务,要结合 CPU 核数看。CPU 使用率描述 CPU 时间去向,
us/ni/sy/wa/id/hi/si/st分别对应普通用户态、nice 用户态、内核态、I/O wait、空闲、中断、软中断和虚拟化 steal。load 高但 CPU 不高,常见原因是大量任务处于不可中断睡眠;CPU 高但 load 不夸张,可能是少数线程把 CPU 打满。
如果继续追问排查方法,可以回答:用 uptime 和 top 定位现象,用 vmstat 判断是运行队列、I/O 还是上下文切换问题,再用 pidstat、mpstat 定位到进程和 CPU 核,必要时通过 perf top 查找热点函数。
