Linux CPU 性能分析:从平均负载、上下文切换到 iowait 与僵尸进程

Linux CPU 性能分析:从平均负载、上下文切换到 iowait 与僵尸进程

Linux 性能问题真正难的地方,通常不是缺少命令,而是不知道应该观察什么、指标之间是什么关系、下一步为什么要用这个工具

topuptimevmstatpidstatiostatperfstrace 都很常见,但如果只会机械地执行命令,很容易陷入一种尴尬状态:看到 CPU 高就重启进程,看到 Load 高就扩容,看到 iowait 高就怀疑磁盘,结果问题偶尔消失,却没有真正定位瓶颈。

性能分析更像一条证据链:

先从宏观指标确认系统发生了什么,再逐层缩小范围,最后落到具体进程、线程、系统调用或函数。

这篇文章围绕 Linux CPU 性能分析建立一套完整的知识框架,重点讨论:

  • 平均负载到底表示什么,为什么它不等于 CPU 使用率;
  • CPU 使用率各个字段应该如何解释;
  • CPU 上下文切换为什么会消耗性能;
  • 如何区分 CPU 密集、I/O 等待和调度竞争;
  • 为什么系统 CPU 很高,却找不到高 CPU 进程;
  • 如何处理短时进程、不可中断进程和僵尸进程;
  • 如何把 uptimempstatpidstatvmstatperfstracepstree 等工具串成一套排查流程。

1. 性能优化的核心不是工具,而是指标、原理和证据链

从应用视角看,性能最常见的两个核心指标是:

  • 吞吐量(Throughput):单位时间内系统能够完成多少工作,例如 QPS、TPS、PPS;
  • 延迟(Latency):一次请求从开始到完成需要多久。

从系统资源视角看,还要观察:

  • CPU 使用率;
  • 平均负载;
  • 内存使用情况;
  • I/O 使用率、吞吐量和延迟;
  • 网络吞吐和丢包;
  • 上下文切换;
  • 中断;
  • 资源饱和度。

性能问题的本质通常可以描述为:

某类资源接近瓶颈,导致请求处理速度无法继续随着负载增长而增长,最终表现为吞吐下降、延迟升高甚至请求失败。

一个完整的性能优化闭环可以抽象为:

flowchart LR
    A[选择性能指标] --> B[设定性能目标]
    B --> C[建立性能基线]
    C --> D[定位瓶颈]
    D --> E[实施优化]
    E --> F[重新验证]
    F --> G[监控与告警]
    G --> D

这里最容易被忽略的是基线

例如看到每秒上下文切换 8000 次,单独看这个数字没有太大意义;如果这台机器平时稳定在 7000~9000 次,那么它可能完全正常。但如果系统过去长期只有 500 次,突然上升到 8 万次,就应该重点调查。

因此,性能分析不应该是“看到某个数字大就判定异常”,而应该同时考虑:

  1. 指标的物理含义;
  2. 系统的资源规模;
  3. 历史基线;
  4. 指标之间是否相互印证;
  5. 异常发生时业务负载是否变化。

2. 建立 CPU 性能分析的整体视图

一次请求从应用进入操作系统,大致会经过:

1
2
3
4
5
6
7
8
9
Application

Libraries

System Call

Linux Kernel

CPU / Memory / Disk / Network Device

性能分析可以从两个方向观察:

  • 应用负载视角:吞吐、延迟、请求数、并发量;
  • 系统资源视角:CPU、内存、磁盘、网络、中断、调度。

这两个方向最终需要在某个位置汇合。

例如:

1
2
3
4
5
6
7
8
9
10
11
接口 RT 升高

平均负载升高

CPU usr 99%

php-fpm 占用 CPU

perf 发现 sqrt() 热点

源码存在百万次无意义循环

这就是一条完整的性能证据链。

相比之下,下面这种排查方式是不完整的:

1
2
3
4
5
接口慢

CPU 高

重启服务

重启可能让症状消失,却没有解释“为什么”。


3. 平均负载 Load Average 到底是什么

执行:

1
uptime

可能看到:

1
02:34:03 up 2 days, 20:14, 1 user, load average: 0.63, 0.83, 0.88

最后三个数字分别表示过去:

  • 1 分钟;
  • 5 分钟;
  • 15 分钟;

的平均负载。

3.1 Load Average 不是 CPU 使用率

这是最常见的误区。

平均负载不是“CPU 使用率的百分比”,它更接近:

单位时间内处于可运行状态和不可中断状态的平均任务数。

也就是:

  • 正在 CPU 上运行的任务;
  • 等待 CPU 的任务;
  • 处于不可中断睡眠状态的任务。

在进程状态中主要对应:

  • R:Running / Runnable;
  • D:Uninterruptible Sleep。

所以 Load 高,可能是:

  1. CPU 真忙;
  2. 大量任务排队等 CPU;
  3. 大量任务在等待 I/O 或其他不可中断资源。

这也是为什么:

Load Average 高,不代表 CPU 使用率一定高。

3.2 R 状态和 D 状态

R:运行或可运行

R 状态包含:

  • 正在 CPU 上运行;
  • 已经具备运行条件,但正在 Run Queue 中等待 CPU。

当 Runnable 任务远多于 CPU 的逻辑处理器数量时,意味着 CPU 竞争严重。

D:不可中断睡眠

D 状态通常意味着任务正在等待内核中的某个关键操作完成,常见场景是硬件 I/O。

一个容易产生误解的地方是“不可中断”。它不是说:

这个进程霸占着 CPU,连硬件中断都不能发生。

更准确地理解是:

任务进入了不可中断睡眠,普通信号不能把它提前唤醒;它仍然已经让出了 CPU。

另外,D 状态并不等价于磁盘 I/O。磁盘 I/O 是常见原因,但不是唯一原因。因此看到 D 进程只能说明“它值得重点调查”,不能直接宣布磁盘故障。

3.3 平均负载应该和多少 CPU 比

最理想的情况,是每个逻辑 CPU 上都有一个可运行任务。

查看逻辑 CPU 数量可以使用:

1
grep 'model name' /proc/cpuinfo | wc -l

也可以用:

1
nproc

假设机器有 2 个逻辑 CPU:

  • Load ≈ 2:基本被充分利用;
  • Load < 2:仍有调度余量;
  • Load > 2:开始出现排队或不可中断任务。

比较平均负载时,一般应与操作系统可见的逻辑 CPU 数进行比较。

3.4 1、5、15 分钟负载怎么看趋势

三个值不是“三选一”,而是共同描述趋势。

情况一:三个值接近

1
1.8 1.7 1.9

说明负载比较平稳。

情况二:1 分钟远小于 15 分钟

1
1.2 3.8 7.0

说明系统此前压力很大,但正在恢复。

情况三:1 分钟远大于 15 分钟

1
8.5 4.0 1.5

说明系统最近出现新的压力,需要继续观察是否会持续。

3.5 不要迷信“70% 阈值”

课程中给出了“Load 超过 CPU 数量的约 70% 就应该关注”的经验值。这个数字只能作为启发式参考,不能当成所有系统的统一告警线。

真正可靠的做法是:

  • 持续采集平均负载;
  • 记录业务高峰和低峰;
  • 建立机器自己的历史基线;
  • 关注趋势变化和数量级变化

4. 平均负载高,怎么判断到底是谁导致的

Load 高后不能停在 uptime,而要进一步回答:

活跃任务到底在干什么?

最常见的三类场景是:

场景 Load CPU 使用率 典型特征
CPU 密集 %usr%sys
I/O 等待 不一定高 %iowait 高、D 进程增加
CPU 竞争 Run Queue 很长、%wait 高、上下文切换多

4.1 CPU 密集场景

模拟一个 CPU 忙任务:

1
stress --cpu 1 --timeout 600

观察平均负载:

1
watch -d uptime

观察每个 CPU:

1
mpstat -P ALL 5

定位进程:

1
pidstat -u 5 1

如果看到:

  • 某个 CPU 接近 100%;
  • iowait 很低;
  • 某个进程 %CPU 接近 100%;

则 Load 上升主要由 CPU 密集任务导致。

4.2 I/O 等待场景

早期案例使用:

1
stress -i 1 --timeout 600

但这一写法在不同系统上的效果并不稳定,因为它主要通过 sync() 制造压力;如果缓存较少,可能只看到系统 CPU 升高,未必看到明显 iowait。

更可靠的实验方式可以使用支持更多 I/O 场景的 stress-ng,但具体参数应根据当前发行版手册确认。

判断 I/O 问题时,不要只看 Load,而应结合:

1
2
mpstat -P ALL 5
pidstat -d 1

4.3 大量 Runnable 任务竞争 CPU

例如在 2 CPU 机器上创建 8 个 CPU 任务:

1
stress -c 8 --timeout 600

此时通常会看到:

  • Load 接近任务数;
  • CPU 接近饱和;
  • 每个任务 %wait 很高;
  • 非自愿上下文切换增加。

这类问题的关键不是“某个进程单独跑得特别慢”,而是:

系统创建了超过 CPU 调度能力的活跃任务。


5. CPU 使用率到底是怎么算出来的

CPU 使用率比 Load 更直观,但要正确解释它,必须理解它的来源。

Linux 会把不同 CPU 状态累计在:

1
cat /proc/stat | grep '^cpu'

示例:

1
2
3
cpu  280580 7407 286084 172900810 83602 0 583 0 0 0
cpu0 144745 4181 176701 86423902 52076 0 301 0 0 0
cpu1 135834 3226 109383 86476907 31525 0 282 0 0 0

这些值是从系统启动以来累计的 CPU 时间。

系统性能工具通常不会直接用开机以来的累计值,而是间隔一段时间取两次采样做差。

可以近似写成:

1
CPU Usage = 1 - Δidle / Δtotal

某一种 CPU 状态的比例则是:

1
Category Usage = Δcategory / Δtotal

因此,不同工具如果采样窗口不同,结果可能不一致。

例如:

  • top 是短时间滚动采样;
  • ps 常反映进程生命周期内的平均情况。

不能拿两个不同时间窗口的数字直接硬比。

5.1 CPU 各字段的含义

常见字段如下:

字段 含义 常见排查方向
us / usr 用户态 CPU 应用计算热点、业务代码
ni nice 调整后的低优先级用户态 CPU 低优先级任务
sy / sys 内核态 CPU 系统调用、上下文切换、内核线程
id CPU 空闲 没有 CPU 工作
wa / iowait CPU 空闲期间存在 I/O 等待任务 存储、块设备、I/O 模式
hi / irq 硬中断 设备中断
si / softirq 软中断 网络、软中断处理
st / steal 虚拟机被宿主机抢走的 CPU 时间 虚拟化资源竞争
guest 运行虚拟机的 CPU 时间 虚拟化工作负载

5.2 iowait 最容易被误解

很多人会把:

1
iowait = 80%

直接解释为:

1
磁盘已经 80% 忙

这并不成立。

从 CPU 记账角度看,iowait 更接近:

CPU 本来处于 idle,但此时系统存在等待 I/O 完成的任务。

所以 iowait 高并不能单独证明磁盘已经达到设备极限。

判断 I/O 是否真的成为瓶颈,还应该继续看:

  • 磁盘吞吐;
  • IOPS;
  • I/O 延迟;
  • 队列长度;
  • 具体进程的 I/O;
  • 设备是否达到饱和。

6. CPU 上下文是什么,为什么切换会变慢

Linux 可以同时运行远多于 CPU 数量的任务,但单个 CPU 在某个瞬间仍然只能执行一个执行流。

CPU 从任务 A 切换到任务 B 前,必须先保存 A 的运行状态,再恢复 B 的运行状态。

最基本的 CPU 上下文包括:

  • CPU 寄存器;
  • Program Counter(程序计数器);
  • 栈指针等执行状态。

上下文切换可以抽象为:

sequenceDiagram
    participant A as Task A
    participant K as Kernel Scheduler
    participant B as Task B

    A->>K: 触发调度
    K->>K: 保存 A 的 CPU 上下文
    K->>K: 选择新的可运行任务
    K->>K: 恢复 B 的上下文
    K->>B: 从 B 上次的位置继续运行

问题在于:

保存和恢复上下文不是免费的。

它要消耗 CPU,并且可能影响:

  • CPU Cache;
  • TLB;
  • 内核栈;
  • 地址空间相关状态。

当切换特别频繁时,CPU 花在“切任务”上的时间越来越多,真正执行应用代码的时间反而越来越少。


7. 三种上下文切换:进程、线程和中断

7.1 进程上下文切换

Linux 中进程既可能运行在用户态,也可能通过系统调用进入内核态。

传统 x86 权限模型中通常可以理解为:

  • Ring 3:用户态;
  • Ring 0:内核态。

进程上下文不仅包含寄存器等 CPU 状态,还关联:

  • 虚拟地址空间;
  • 用户栈;
  • 内核栈;
  • 进程级资源状态。

不同进程之间切换时,代价通常比同进程线程之间切换更高。

7.2 系统调用不是“切换到另一个进程”

例如:

1
open() -> read() -> write() -> close()

系统调用会经历:

1
用户态 -> 内核态 -> 用户态

但它仍然是同一个进程

因此需要区分:

  • 特权级切换 / mode switch:同一进程在用户态与内核态之间切换;
  • 进程上下文切换:从进程 A 切到进程 B。

系统调用同样有 CPU 上下文保存和恢复成本,但不需要完成完整的进程地址空间切换。

7.3 线程上下文切换

线程是内核调度的基本执行单位,进程更像资源容器。

同一进程内的线程共享:

  • 虚拟内存;
  • 全局变量;
  • 打开的资源等进程级对象。

线程自己仍拥有:

  • 栈;
  • 寄存器;
  • 调度状态。

所以:

  • 不同进程的线程之间切换,成本接近进程切换;
  • 同一进程内的线程切换,因为地址空间等资源可以继续复用,通常更轻量。

7.4 中断上下文切换

硬件事件发生时,CPU 会暂停当前执行流,转而执行中断处理程序。

中断上下文通常只需要保存内核执行必要的状态,不需要像进程切换那样完整切换用户地址空间。

但中断过多同样会让 CPU 大量时间消耗在中断处理上。

因此如果:

1
hi / si 明显升高

就应该调查中断来源,而不是只盯着用户进程。


8. 什么情况会触发调度和上下文切换

常见原因包括:

  • 当前任务结束;
  • 当前任务主动睡眠;
  • 当前任务等待 I/O、锁、内存等资源;
  • 调度器决定让另一个任务运行;
  • 更高优先级任务变为可运行;
  • 硬件中断;
  • 多核系统中的重新调度。

需要注意一个历史化的表述:用“固定时间片轮流运行”来解释调度非常直观,但现代 Linux 常用的 CFS 调度器不能简单理解成传统固定时间片轮转。CFS 的核心是通过 vruntime 等机制维护公平性,任务实际可运行时长会动态变化。

因此在性能分析里,应该关注的是:

任务为什么频繁变成可运行、阻塞、唤醒,以及调度器为什么频繁切换执行对象。

而不是死记“一个时间片是多少毫秒”。


9. 如何查看上下文切换

9.1 vmstat:看系统整体

1
vmstat 1

重点关注:

1
2
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b ... in cs us sy id wa st

关键字段:

  • r:正在运行或等待 CPU 的任务数;
  • b:不可中断睡眠任务数;
  • in:每秒中断次数;
  • cs:每秒上下文切换次数;
  • us:用户 CPU;
  • sy:系统 CPU;
  • wa:iowait。

如果同时看到:

1
2
3
r 很高
cs 暴涨
sy 很高

往往意味着系统在做大量调度工作。

9.2 pidstat:看进程和线程

进程上下文切换:

1
pidstat -w 1

重点字段:

  • cswch/s:Voluntary Context Switch,自愿上下文切换;
  • nvcswch/s:Non-voluntary Context Switch,非自愿上下文切换。

自愿上下文切换高

通常说明任务主动进入等待,例如:

  • I/O;
  • 锁;
  • 内存或其他资源;
  • sleep / condition wait。

非自愿上下文切换高

通常说明:

  • CPU 竞争激烈;
  • 调度器频繁强制切换任务。

如果进程级数据解释不了系统级 cs,要想到:

Linux 调度的是线程。

查看线程级上下文切换:

1
pidstat -wt 1

这一步很关键。多线程程序主进程指标不高,不代表子线程没有产生海量调度。


10. 一个典型的上下文切换故障:线程远多于 CPU

使用 sysbench 可以模拟大量线程调度。

不同版本的 sysbench 参数存在差异,旧资料中的示例类似:

1
sysbench --threads=10 --max-time=300 threads run

先观察:

1
vmstat 1

如果出现:

1
2
3
4
r   = 8
cs = 1,390,000/s
sy = 84%
id = 0%

而机器只有 2 个 CPU,那么证据已经非常明显:

  • Run Queue 远大于 CPU 数;
  • 上下文切换出现数量级增长;
  • 系统 CPU 极高;
  • CPU 大量时间花在内核调度上。

再使用:

1
pidstat -wt 1

可以进一步看到具体线程的切换频率。

10.1 中断也要一起检查

多核调度还可能伴随 Rescheduling Interrupt(重新调度中断)。

可以观察:

1
watch -d cat /proc/interrupts

如果 RES 等重新调度相关中断快速增长,就能和线程调度问题相互印证。

这体现了性能排查的一个重要原则:

不要只相信单个工具,用不同数据源交叉验证。


11. 每秒多少次上下文切换算异常

没有一个适用于所有机器的绝对阈值。

课程中的经验是:

  • 从几百到一万以内,在不少系统上可能属于正常范围;
  • 超过一万或者出现数量级增长时,应提高警惕。

但更重要的是:

1
异常 = 相对历史基线发生显著变化

例如:

  • 平时 6000/s,今天 9000/s:未必异常;
  • 平时 500/s,突然 80000/s:很可疑。

12. CPU 使用率 100% 时,排查思路应该怎么走

看到 CPU 100%,第一步不是马上看源码,而是先回答:

到底是哪一种 CPU 高?

12.1 用户 CPU 高

如果:

1
2
3
us ≈ 100%
sy 很低
wa 很低

优先怀疑:

  • 算法复杂度;
  • 死循环;
  • 大量计算;
  • JSON / 压缩 / 加解密;
  • JVM / 语言运行时热点;
  • 业务函数热点。

12.2 系统 CPU 高

如果:

1
sy 很高

优先调查:

  • 系统调用频繁;
  • 上下文切换;
  • 锁竞争;
  • 内核线程;
  • 网络协议栈;
  • 调度行为。

12.3 iowait 高

继续向 I/O 路径排查,不要直接宣布“磁盘坏了”。

12.4 hi / si 高

调查硬中断和软中断,例如:

1
cat /proc/interrupts

网络类问题还可能需要继续检查软中断和网卡队列。


13. 从进程进一步定位到函数:perf

toppidstat 已经定位到高 CPU 进程后,下一步需要回答:

这个进程内部到底是哪个函数在消耗 CPU?

这时 perf 是非常重要的工具。

13.1 perf top

1
perf top

指定进程并显示调用关系:

1
perf top -g -p <PID>

重点看:

  • Overhead
  • Shared Object;
  • 用户态 [.] / 内核态 [k]
  • Symbol;
  • 调用链。

采样数量太少时,不要对百分比过度解读。

13.2 perf record + perf report

需要留存数据或离线分析时:

1
perf record -g -p <PID>

结束采样后:

1
perf report

系统级采样也可以使用:

1
2
perf record -ag -- sleep 15
perf report

13.3 为什么不建议一开始就 GDB

GDB 很强,但它会主动控制甚至暂停程序。

在线上性能排查初期,更适合优先使用采样式、低侵入的工具缩小范围。

等已经定位到可疑函数,再在线下或可控环境中用调试器深入分析。


14. 高 CPU 案例:从 11 QPS 到 2200+ QPS

一个 Nginx + PHP 的案例中,压测结果只有约:

1
Requests per second: 11.63

使用 top 可以观察到:

  • 两个 CPU 的用户态 CPU 都接近 100%;
  • 多个 php-fpm 进程持续占用 CPU。

进一步对 php-fpm 使用:

1
perf top -g -p <php-fpm-pid>

调用链最终指向 sqrt() 等热点。

查看业务代码后发现类似:

1
2
3
4
5
6
<?php
$x = 0.0001;
for ($i = 0; $i <= 1000000; $i++) {
$x += sqrt($x);
}
echo "It works!";

这段代码属于无意义的测试计算,却被带到了请求路径。

删除后再次压测,吞吐从十几 QPS 上升到两千以上。

这个案例最重要的不是“删掉 for 循环”,而是完整的定位过程:

flowchart TD
    A[接口性能很差] --> B[top 看系统 CPU]
    B --> C[确认 usr 接近饱和]
    C --> D[定位 php-fpm]
    D --> E[perf 分析热点函数]
    E --> F[定位 sqrt 调用链]
    F --> G[回到源码确认]
    G --> H[删除无意义计算]
    H --> I[重新压测验证]

这是一个非常典型的“系统指标 → 进程 → 函数 → 源码 → 验证”过程。


15. 为什么系统 CPU 很高,却找不到高 CPU 进程

这是比“某个进程直接 100%”更有迷惑性的场景。

可能看到:

1
%Cpu(s): 80% us, 15% sy, 2% id

但进程列表中:

1
2
3
4
nginx     3%
php-fpm 2%
dockerd 1%
...

所有进程加起来都解释不了系统的 80% 用户 CPU。

这时要警惕:

短时进程。

15.1 top 是快照工具,有盲区

如果一个进程:

1
创建 -> 运行 50ms -> 退出

top 3 秒刷新一次,那么这个进程很可能在两次采样之间已经完成完整生命周期。

这时你看到的是:

1
2
系统 CPU 很高
但没有一个长期存在的高 CPU 进程

15.2 短时进程常见来源

主要有两种:

  1. 应用通过 exec()、Shell 等方式不断调用外部二进制;
  2. 应用不断崩溃,并被 supervisor、systemd、容器平台等机制快速重启。

15.3 从 PID 不断变化发现线索

如果 top 偶尔看到:

1
stress PID=24344

下一次却变成:

1
stress PID=6779

这说明它不是一个稳定长期存在的进程。

此时可以进一步:

1
pstree | grep stress

找到父进程关系。


16. 专门抓短时进程:execsnoop

execsnoop 的价值在于它不是定时轮询进程表,而是监控 exec() 事件。

典型输出会包含:

1
2
3
4
PCOMM   PID    PPID   RET   ARGS
sh 30394 30393 0
stress 30396 30394 0 /usr/local/bin/stress -t 1 -d 1
...

这样可以直接看到:

  • 谁启动了短时进程;
  • 父 PID;
  • 命令行参数;
  • 执行是否成功。

这类工具体现了一个重要区别:

  • top / ps快照型观测
  • perf / execsnoop事件或采样型观测

当问题对象生命周期比工具刷新周期还短时,必须换成事件记录思路。


17. 短时进程高 CPU 案例:真正的问题不是“业务代码在算”

一个 PHP 应用在每次请求中执行:

1
$result = exec("/usr/local/bin/stress -t 1 -d 1 2>&1", $output, $status);

原本意图是模拟 I/O 压力,但 stress 因权限问题无法创建临时文件,快速失败退出。

结果变成:

1
2
3
4
5
6
7
8
9
10
11
请求进入

创建 stress

初始化

权限失败

退出

下一请求再次创建 stress

大量短时进程持续创建和销毁,系统 CPU 被吃掉,但普通 top 很难稳定看到某个 stress 进程占满 CPU。

这时:

1
2
perf record -g
perf report

可以从采样历史中发现 stress 占用了大量 CPU 事件。

这里的核心经验是:

系统级指标和进程列表对不上时,不要立即怀疑工具,先怀疑观察模型是否遗漏了“短生命周期对象”。


18. Linux 进程状态:R、S、D、Z、T、I、X

性能排查里,进程状态往往比 %CPU 更能暴露问题。

状态 含义 说明
R Running / Runnable 正在运行或等待 CPU
S Interruptible Sleep 可中断睡眠,等待事件
D Uninterruptible Sleep 不可中断睡眠,常见于 I/O
Z Zombie 已退出,但父进程未回收
T/t Stopped / Traced 暂停或被调试器跟踪
I Idle 空闲内核线程
X Dead 已消亡,通常不会长期出现在工具中

18.1 D 状态为什么会拉高 Load

Load Average 统计可运行任务和不可中断任务,所以大量 D 状态会直接推高 Load。

注意:

  • D 进程会进入 Load 统计;
  • I 状态内核线程不会因为只是 idle 就推高 Load。

18.2 Z 状态为什么危险

子进程退出后,父进程需要通过:

1
2
wait();
waitpid();

等方式回收退出状态。

如果父进程一直不回收,子进程就保留为 Zombie。

僵尸进程本身不继续执行用户代码,但会占用内核中的进程表相关资源和 PID。

如果数量持续增长,最终可能导致无法创建新进程。


19. D 状态 + iowait 高:如何分析

一个典型现象可能是:

1
2
3
4
load average: 2.00, 1.68, 1.39
Tasks: ... 115 zombie
CPU0 wa: 60.5%
CPU1 wa: 94.6%

同时存在两个问题:

  1. iowait 很高;
  2. Zombie 持续增加。

不要混在一起处理,要分成两条证据链。

19.1 第一步:确认 iowait 是否和磁盘 I/O 同步

可以使用:

1
dstat 1 10

或者其他能够同时观察 CPU 与 I/O 的工具。

如果看到:

1
wai 上升时,disk read 同时明显升高

说明 iowait 与磁盘读存在明显相关性。

这仍然只是“相关”,下一步还要定位到进程。

19.2 第二步:找进程 I/O

查看所有进程 I/O:

1
pidstat -d 1 20

常见字段:

  • kB_rd/s:每秒读取 KB;
  • kB_wr/s:每秒写入 KB;
  • kB_ccwr/s:取消写相关统计;
  • iodelay:I/O 延迟相关统计。

如果目标进程很短,指定单个 PID 可能会扑空,因为 PID 很快就消失。

因此:

1
pidstat -d -p <PID> 1 3

没有数据时,不要马上下结论,要先确认这个 PID 是否仍存在。


20. strace:从进程 I/O 继续追到系统调用

应用访问磁盘最终要通过系统调用进入内核。

所以定位到可疑进程后,可以继续:

1
strace -p <PID>

如果看到大量:

1
2
3
4
read(...)
pread64(...)
write(...)
fsync(...)

就能更精确地判断 I/O 模式。

但如果进程已经变成 Zombie:

1
[app] <defunct>

那它已经退出,strace 无法再附加。

此时应该切换到能够记录历史事件的工具,例如:

1
2
perf record -g
perf report

这再次说明:

实时附加工具适合长生命周期对象;事件记录工具更适合瞬时对象。


21. 直接 I/O:为什么 O_DIRECT 会让 iowait 明显升高

案例中的调用栈可以看到类似:

1
2
3
4
sys_read
-> vfs_read
-> new_sync_read
-> blkdev_direct_IO

继续检查源码,发现文件以:

1
open(disk, O_RDONLY | O_DIRECT | O_LARGEFILE, 0755);

打开。

O_DIRECT 会绕过常规 Page Cache 路径,让应用更直接地控制 I/O。

这种方式并不是“错误设计”。

对于数据库等拥有自己的 Buffer Pool、需要精确控制缓存和持久化语义的系统,Direct I/O 很常见。

但对于普通应用,如果没有必要自己管理缓存,直接绕过 Page Cache 可能会让每次读取都更直接地命中块设备,从而显著增加存储压力和等待时间。

因此优化不能简单变成:

1
看到 O_DIRECT -> 删除

而应该先问:

  • 应用为什么选择 Direct I/O;
  • 是否已经有用户态缓存;
  • 是否对一致性和持久化有特殊要求;
  • 使用 Page Cache 会不会出现双缓存;
  • I/O 模式是顺序还是随机。

22. iowait 高不等于磁盘已经饱和

这是整套 CPU/I/O 分析中必须牢记的一点。

假设系统几乎没有 CPU 计算任务,唯一工作就是等一个低速 I/O:

1
2
CPU 大部分时间无事可做
但存在 I/O waiter

此时 iowait 可能很高,但设备并不一定已经达到最大吞吐。

因此完整判断应该是:

flowchart TD
    A[iowait 高] --> B{磁盘吞吐/IOPS/延迟是否异常?}
    B -- 否 --> C[可能只是 CPU 空闲时存在 I/O 等待]
    B -- 是 --> D[继续定位设备与进程]
    D --> E[pidstat / iostat / 设备指标]
    E --> F[系统调用 / perf / 代码]

23. 僵尸进程如何定位和修复

既然 Zombie 是“子进程退出了,但父进程没有回收”,那么解决方向非常明确:

找到父进程。

可以使用:

1
pstree -aps <zombie-pid>

得到父子关系后,回到父进程代码检查:

  • 是否调用 wait()
  • 是否调用 waitpid()
  • 是否正确注册 SIGCHLD
  • wait 是否处在真正可执行到的位置;
  • 是否存在 fork 后异常控制流。

例如一个错误结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
int status = 0;

for (;;) {
for (int i = 0; i < 2; i++) {
if (fork() == 0) {
sub_process();
}
}

sleep(5);
}

while (wait(&status) > 0);

while (wait(...)) 在无限循环后面,实际上永远执行不到。

应该把子进程回收逻辑放到能够正常执行的位置,或者使用可靠的 SIGCHLD 处理策略。

23.1 不要把“kill Zombie”当成修复

Zombie 已经退出,它本身没有正常执行流可以“杀死”。

真正的问题在父进程。

如果只是粗暴杀父进程,可能暂时让 Zombie 被其他机制接管或清理,但线上生产系统更应该修复父进程的回收逻辑。


24. 一套可复用的 Linux CPU 性能排查路径

把前面的知识收拢后,可以得到一条非常实用的排查主线。

flowchart TD
    A[用户反馈慢 / CPU 告警 / Load 告警] --> B[uptime / top]
    B --> C{Load 是否异常?}
    C --> D[看 1/5/15 分钟趋势]
    B --> E[拆分 usr/sys/wa/hi/si/st]

    E --> F{usr 高?}
    F -- 是 --> G[pidstat/top 定位进程]
    G --> H[perf 定位热点函数]

    E --> I{sys 高?}
    I -- 是 --> J[vmstat 看 cs/in/r]
    J --> K[pidstat -w/-wt]
    K --> L[/proc/interrupts 或 perf]

    E --> M{wa 高?}
    M -- 是 --> N[dstat/iostat 验证 I/O]
    N --> O[pidstat -d 定位进程]
    O --> P[strace/perf 看系统调用]

    G --> Q{系统 CPU 高但进程都不高?}
    Q -- 是 --> R[怀疑短时进程/频繁重启]
    R --> S[pstree / execsnoop / perf record]

    B --> T{D/Z 是否异常?}
    T -- D 多 --> N
    T -- Z 多 --> U[pstree 找父进程]
    U --> V[检查 wait/waitpid/SIGCHLD]

25. 常用工具速查

25.1 系统整体

1
uptime

看 Load Average。

1
top

看:

  • Load;
  • Task 状态;
  • CPU 类型;
  • 进程排序。

top 中按 1 可以查看每个 CPU。

25.2 CPU

1
mpstat -P ALL 5

观察每个 CPU 的:

  • usr;
  • sys;
  • iowait;
  • irq / softirq 等。

25.3 进程 CPU

1
pidstat -u 1

25.4 上下文切换

1
vmstat 1
1
pidstat -w 1

线程级:

1
pidstat -wt 1

25.5 进程 I/O

1
pidstat -d 1

25.6 系统调用

1
strace -p <PID>

25.7 函数热点

1
perf top -g -p <PID>

或者:

1
2
perf record -g -p <PID>
perf report

25.8 中断

1
watch -d cat /proc/interrupts

25.9 父子进程

1
pstree -aps <PID>

25.10 短时进程

1
execsnoop

在不同发行版上,execsnoop 的来源、安装方式以及实现技术可能不同,实际使用时应以当前系统工具链为准。


26. 工具之间不是替代关系,而是不同观察尺度

可以把工具按观察层级理解:

层级 工具 回答的问题
系统趋势 uptime Load 是否异常,趋势如何
系统 CPU top / mpstat 哪类 CPU 时间高
调度 vmstat Run Queue、上下文切换、中断是否异常
进程 pidstat 哪个进程的 CPU / I/O / 切换高
线程 pidstat -t 是否是线程级问题
调用栈 perf CPU 时间花在哪些函数
系统调用 strace 进程正在怎样访问内核
进程关系 pstree 谁创建了谁
短时执行 execsnoop 哪些 exec 在快速发生
内核中断 /proc/interrupts 哪类中断增长

真正熟练之后,你不会再问:

“Linux 性能分析最强的命令是什么?”

而会问:

“我现在缺的证据在哪一层?”


27. 常见误区

27.1 Load 高 = CPU 高

错误。

Load 还包含不可中断任务。

27.2 iowait 高 = 磁盘已满负载

错误。

iowait 只是 CPU 记账维度之一,需要继续检查设备吞吐、IOPS 和延迟。

27.3 CPU 高就找 %CPU 最高的进程

大多数时候有效,但不是全部。

短时进程、频繁崩溃重启、中断和内核工作都可能让进程列表解释不了系统 CPU。

27.4 上下文切换越少越好

错误。

上下文切换是多任务系统正常工作的基础。

真正异常的是:

  • 频率显著高于基线;
  • 与 Run Queue 增长同时出现;
  • sys CPU 被调度开销吃掉;
  • 吞吐下降、延迟上升。

27.5 D 状态就是磁盘 I/O

不严谨。

D 是不可中断睡眠,I/O 很常见,但不是唯一原因。

27.6 Zombie 进程直接 kill 掉就行

不对。

Zombie 已经退出。需要修复父进程的回收逻辑。

27.7 一上来就 perf / GDB

工具越底层,并不代表越高效。

应该先从宏观指标缩小范围,否则很容易在海量调用栈里迷路。


28. 生产环境中需要特别注意的细节

28.1 采样窗口必须一致

对比多个工具时,尽量让采样间隔一致。

否则可能出现:

1
2
top 看到瞬时 90%
ps 看到生命周期平均 20%

两者并不冲突,只是观察窗口不同。

28.2 线上机器不一定允许安装工具

生产服务器可能:

  • 没有 root;
  • 不能联网;
  • 不能安装 BCC / perf-tools;
  • 只能经过堡垒机;
  • 容器里没有调试工具。

因此最好同时熟练:

  • /proc
  • top
  • ps
  • vmstat
  • pidstat
  • strace

这些更基础的手段。

高级追踪工具是加速器,不应该成为唯一依赖。

28.3 容器环境里的 perf 符号问题

分析容器进程时,宿主机上的 perf 可能找不到容器内部的动态库和符号,于是调用栈只显示十六进制地址。

这类问题的本质是:

采样发生在宿主机,但符号和依赖库位于容器文件系统。

常见思路包括:

  • 保存 perf.data 后,在能够访问正确符号路径的环境中解析;
  • perf report 提供正确的符号文件系统路径;
  • 在受控环境内完成符号化。

不要因为“只看到地址”就误判为 perf 没采到数据。

28.4 教学案例的 Ubuntu 18.04 已是历史环境

资料中的命令主要基于 Ubuntu 18.04、当时版本的 sysstat、sysbench、stress 等工具。

放到新的发行版或容器环境时,可能遇到:

  • 包名变化;
  • 参数变化;
  • 内核调度行为差异;
  • pidstat 字段差异;
  • sysbench CLI 变化;
  • 容器权限限制;
  • 块设备命名变化。

所以实际执行时,man--help 和当前发行版官方文档应该作为最终依据。


29. 性能排查的几个工程化原则

29.1 先现象,后源码

不要因为“我知道这段代码看起来很可疑”就跳过系统观测。

正确流程是:

1
2
3
4
5
6
7
8
9
10
11
现象

系统指标

资源类型

进程 / 线程

调用栈 / 系统调用

源码

这样得到的是可验证的结论,而不是靠经验猜。

29.2 一次只缩小一层范围

例如:

1
Load 高

不要直接跑几十个命令。

应该先确定:

1
2
3
CPU 竞争?
I/O 等待?
还是别的不可中断资源?

然后再选下一层工具。

29.3 指标之间必须互相解释

好的结论应该能解释多个现象。

例如“线程过多导致调度压力”应该同时解释:

  • r 高;
  • cs 高;
  • nvcswch 高;
  • sys 高;
  • RES 中断增长;
  • 延迟上升。

如果一个解释只能解释某一个指标,却和其他指标矛盾,就需要继续调查。

29.4 优化后一定重新测

性能优化没有“看起来应该变快”。

必须重新测:

  • 吞吐量;
  • 延迟;
  • CPU;
  • Load;
  • I/O;
  • 资源饱和度。

只有指标证明发生改善,优化才算完成。


30. 一份适合线上故障的快速检查清单

遇到 Linux 服务“突然变慢”,可以按下面顺序快速扫一遍。

30.1 看总体

1
2
uptime
top

回答:

  • Load 是否异常?
  • 1/5/15 分钟趋势如何?
  • CPU 是 ussywahisi 还是 st 高?
  • R、D、Z 任务是否异常?

30.2 看每个 CPU

1
mpstat -P ALL 1

回答:

  • 是所有 CPU 都忙?
  • 还是单核热点?

30.3 看调度

1
vmstat 1

重点看:

1
r b in cs us sy id wa

30.4 看进程

1
2
3
pidstat -u 1
pidstat -w 1
pidstat -d 1

30.5 进程解释不了系统指标

考虑:

  • 线程;
  • 短时进程;
  • 中断;
  • 内核线程;
  • 频繁重启。

继续:

1
2
3
4
pidstat -wt 1
pstree -ap
execsnoop
cat /proc/interrupts

30.6 已经定位进程

1
perf top -g -p <PID>

或者:

1
2
perf record -g -p <PID>
perf report

涉及 I/O 或系统调用:

1
strace -p <PID>

31. 最终应该建立的思维模型

Linux 性能分析不是“背命令”,而是建立下面这套映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
现象

指标

资源

任务状态

进程 / 线程

系统调用 / 调度 / 中断

函数

源码和配置

当你看到:

1
Load 高

应该自动想到:

1
R 多?D 多?CPU 数是多少?趋势是什么?

看到:

1
sys 高

应该想到:

1
系统调用?上下文切换?中断?内核线程?

看到:

1
iowait 高

应该想到:

1
真实 I/O 量有多大?哪个设备?哪个进程?什么 I/O 模式?

看到:

1
系统 CPU 高,但进程都不高

应该想到:

1
短时进程?频繁重启?线程?中断?

看到:

1
大量 Zombie

应该想到:

1
父进程是谁?wait/waitpid/SIGCHLD 是否正确?

真正的 Linux 性能能力,就是把这些问题变成条件反射。

总结

CPU 性能分析最容易掉进两个坑:一个是把所有问题都归结成“CPU 不够”,另一个是把工具输出当答案。

更可靠的方法是建立一条层层收敛的证据链:

1
2
3
4
5
6
7
8
业务慢
→ 看负载与 CPU 类型
→ 区分 CPU、I/O、调度、中断
→ 找进程和线程
→ 看上下文切换、进程状态和系统调用
→ 用 perf 定位热点
→ 回到代码或配置
→ 修改后重新压测

其中最值得长期记住的几个结论是:

  • Load Average 不是 CPU 使用率,而是活跃任务与不可中断任务的综合信号;
  • CPU 100% 之前先看清楚到底是哪一种 CPU 时间高;
  • 上下文切换是必要机制,但数量级增长可能意味着严重的调度浪费;
  • 系统 CPU 很高却找不到高 CPU 进程时,要想到短时进程和线程;
  • D 状态常与 I/O 有关,但不能简单等价;
  • iowait 高不等于磁盘已经达到性能上限;
  • Zombie 的根因在父进程的资源回收逻辑;
  • 性能工具只是观察手段,真正重要的是知道下一条证据应该去哪里找。

当这些概念串成一个整体后,top 就不再只是一个“看 CPU 百分比”的命令,vmstat 也不再是一堆难记的列,perf 更不是遇到问题就随手跑一下的神器。它们会成为同一套性能分析方法在不同层级上的观察窗口。


Linux CPU 性能分析:从平均负载、上下文切换到 iowait 与僵尸进程
https://allendericdalexander.github.io/2026/08/11/devops/linux/performanceOptimization/01linux-cpu-performance-analysis/
作者
AtLuoFu
发布于
2026年8月11日
许可协议