本文要点
- top / htop 里的 CPU 百分比不是内核算好的,而是工具读
/proc/stat两次、把两次的累计值相减得到的。第一行那串数字是开机以来的累计 jiffies,这台机器上 1 个 jiffy = 10 毫秒(getconf CLK_TCK= 100)。累计值说明不了"现在忙不忙":实测开机 30 小时,user累计 136136 jiffies 也才 1.26%,idle占 95.46% - 正确姿势是两次采样求差。实测在单核跑满时取 3 秒窗口:
idle掉到 0,user37.87%、system62.13%——sys 比 user 还高,因为那个死循环每写一行都要走一次write()系统调用 - ⚠️ 同一时刻三个工具能报出三个数字,因为采样窗口与字段口径都不同:
vmstat第 1 行是"自上次重启以来的平均"(手册原文 averages since the last reboot)、第 2 行起才是每秒增量,且它的us把 nice 并进去了(user time, including nice time,实测us=3正好等于 1.26% + 1.66%);top的%Cpu是近一屏的增量,同一时刻能报到 44.4% - 负载均值和
steal都不是 CPU 使用率:load是可运行 + 不可中断进程数的指数平均(内核每 5 秒采样,1 分钟值的衰减常数 60 秒),实测把唯一的 1 个核跑满 20 秒,它只从 0.20 涨到 0.37;steal(st)是 VPS 上最该看懂的字段——实测这台 NAT 小机开机 30 小时被宿主机偷走 95189 jiffies = 951 秒(0.88%),这段时间不在你的任何进程里,也不在你的磁盘上 - ⚠️
iowait是官方盖章"不可靠"的字段:man 5 proc明确列出三条理由,其中一条是这个值在某些情况下会变小,所以它不能当累计计数器用
Linux CPU 使用率是怎么算出来的:从 /proc/stat 到 steal 窃取时间
在 top 里看到 %Cpu(s): 44.4 us,多数人会默认这是内核算好递上来的一个数字。不是。内核对外的接口只是 /proc/stat 里一行越来越大的整数,所有的百分比都是 top、vmstat、htop、Prometheus 的 node_exporter 自己减出来的。
想通这一点,很多"数字对不上"的困惑就自动解开了:为什么 top 报 44% 而 vmstat 报 3%?为什么刚跑起来的负载 loadavg 还是 0.0?为什么 VPS 上 CPU 明明没跑满,程序就是变慢了?
本文在一台 1 核的 NAT 小机(Ubuntu 22.04、内核 5.15.0-30-generic、开机 30 小时)上把这些数字逐个拆开实测了一遍,包括最有 VPS 特色的 steal 字段。
/proc/stat 的第一行:一串只增不减的数字
head -1 /proc/stat
echo "--- 字段名与开机以来累计占比 ---"
awk '/^cpu /{ split("user nice system idle iowait irq softirq steal", n, " "); tot=0
for (i=2;i<=9;i++) { v[i]=$i; tot+=$i }
for (i=2;i<=9;i++) printf "%-8s %8d jiffies %6.2f%%\n", n[i-1], v[i], v[i]*100/tot }' /proc/stat
echo "jiffies 频率: $(getconf CLK_TCK) Hz CPU 核数: $(nproc) 开机时长: $(cut -d' ' -f1 /proc/uptime) 秒"
实测输出:
cpu 136136 179936 71343 10318430 7176 0 1472 95189 0 0
--- 字段名与开机以来累计占比 ---
user 136136 jiffies 1.26%
nice 179936 jiffies 1.66%
system 71343 jiffies 0.66%
idle 10318430 jiffies 95.46%
iowait 7176 jiffies 0.07%
irq 0 jiffies 0.00%
softirq 1472 jiffies 0.01%
steal 95189 jiffies 0.88%
jiffies 频率: 100 Hz CPU 核数: 1 开机时长: 108320.57 秒那串数字的单位是 jiffy,也就是内核的时钟节拍。它的长度由 CONFIG_HZ 决定,用户态通过 getconf CLK_TCK 读到——这台机器是 100,所以一个 jiffy 是 10 毫秒。换算成秒就是 95189 / 100 = 951.89 秒,这也是后面理解 steal 的关键。
字段含义(编号与 man 5 proc 一致):
| # | 字段 | 含义 | 本机累计 |
|---|---|---|---|
| 1 | user | 用户态时间(不含 nice) | 1.26% |
| 2 | nice | nice 值大于 0 的用户态时间 | 1.66% |
| 3 | system | 内核态时间 | 0.66% |
| 4 | idle | 空闲 | 95.46% |
| 5 | iowait | 等待 I/O 完成的时间(⚠️ 官方标注不可靠,见下文) | 0.07% |
| 6 | irq | 硬中断处理 | 0.00% |
| 7 | softirq | 软中断处理 | 0.01% |
| 8 | steal | 被虚拟化层拿走的时间 | 0.88% |
原始行里其实有 10 个数字,最后两个是guest与guest_nice——在 Linux 控制下跑虚拟机客户机所花的时间,属于虚拟化场景,这台机器上恒为 0。上面的脚本只取了第 2~9 列,也就是user到steal这八个。
nice 占了 1.66% 有点反直觉:这台机器开机以来几乎没干过活,怎么会有 30 分钟的低优先级用户态时间?典型来源是镜像里的后台服务——apt 的自动更新、各种被 niced 的守护进程,它们优先级低、不抢 CPU,但时间会老老实实记在 nice 里,长期运行慢慢累积。看 nice 高不要在应用代码里找原因,先看系统里有哪些 nice 过的定时任务在跑。
累计值不是使用率:两次采样求差
上面那张百分比表是"开机 30 小时的总体画像",它能回答"这台机器平时闲不闲",但回答不了"现在忙不忙"。要看当前状态,必须取两次样本作差。
echo "--- 先让这唯一的 1 个核忙起来,再采样 3 秒 ---"
timeout 8 yes > /dev/null &
sleep 1
awk 'BEGIN{
while ((getline l < "/proc/stat") > 0) if (l ~ /^cpu /) { split(l, a); break }
close("/proc/stat"); system("sleep 3")
while ((getline l < "/proc/stat") > 0) if (l ~ /^cpu /) { split(l, b); break }
close("/proc/stat")
split("user nice system idle iowait irq softirq steal", n, " ")
for (i = 2; i <= 9; i++) { d[i] = b[i] - a[i]; tot += d[i] }
for (i = 2; i <= 9; i++) printf "%-8s %7d jiffies %6.2f%%\n", n[i-1], d[i], d[i]*100/tot
printf "%-8s %7d jiffies %6.2f%%\n", "合计", tot, 100
}'
wait
实测输出:
user 114 jiffies 37.87%
nice 0 jiffies 0.00%
system 187 jiffies 62.13%
idle 0 jiffies 0.00%
iowait 0 jiffies 0.00%
irq 0 jiffies 0.00%
softirq 0 jiffies 0.00%
steal 0 jiffies 0.00%
合计 301 jiffies 100.00%三个可以立刻读出来的信息:
idle归零——唯一的那个核被占满了。1 核机器上"CPU 使用率"就是100% − idle%,这里正好 100%。- 合计 301 jiffies ≈ 3 秒(3 × 100 = 300),说明这个窗口覆盖了完整的 3 秒,采样本身没有丢拍。窗口内总和恒等于
时间 × CLK_TCK × 核数,这是校验脚本有没有写错的最快办法。 system62.13% 比user37.87% 还高。跑的是yes > /dev/null,看着像纯计算,其实它每输出一行都要调一次write();数据虽然被/dev/null丢了,系统调用一次都不能少。看到sy高于us,先想"这进程是不是在做大量系统调用",而不是"内核有问题"。
采样窗口的取舍是个实践问题:窗口越短越灵敏也越抖,3 秒左右适合看"现在",top 那种 1~3 秒的刷新间隔就是同一个量级;要看趋势就用 10 秒以上。但无论窗口多长,永远只能得到"窗口内的平均值",一个 0.1 秒的尖峰在 10 秒窗口里会被稀释成 1%。
同一时刻三个工具、三个数字
这是最容易让人怀疑"监控在骗我"的地方。同一时刻跑 vmstat 和 top,数字经常对不上:
timeout 20 yes > /dev/null &
sleep 2
echo "--- vmstat:第 1 行数据是开机至今平均,之后才是每秒增量 ---"
vmstat 1 3 | tail -4
echo "loadavg: $(cat /proc/loadavg)"
echo "--- top 的 %Cpu 是近一屏的增量口径,不是开机至今 ---"
top -bn1 | head -3
wait
echo "负载进程退出后 loadavg: $(cat /proc/loadavg)"
实测输出(节选):
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 143756 92488 1608120 0 0 6 141 45 70 3 1 95 0 1
1 0 0 143756 92488 1608120 0 0 0 44 276 139 34 66 0 0 0
1 0 0 143756 92488 1608120 0 0 0 0 259 37 35 65 0 0 0
--- top 的 %Cpu 是近一屏的增量口径,不是开机至今 ---
%Cpu(s): 37.5 us, 62.5 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st三行数据里藏着两个坑:
坑一:vmstat 的第 1 行不是"当前"。 看它的 us sy id wa st = 3 1 95 0 1,而第 2、3 行是 34 66 0 0 0 和 35 65 0 0 0。第 1 行是自上次重启以来的平均(man 8 vmstat 原文:The first report produced gives averages since the last reboot),拿它判断"现在忙不忙"会得出"这台机器 95% 空闲"的错误结论。脚本里解析 vmstat 一定要从第 2 行数据开始。
坑二:vmstat 的 us 把 nice 时间并进去了。 手册原文是 user time, including nice time。回头核对第一行:us=3,而开机至今 user 是 1.26%、nice 是 1.66%,两者相加 2.92%,四舍五入正好 3。vmstat 老版本没有独立的 ni 列,所以它和 top 的 us/ni 不是同一个口径,跨工具填表前必须先统一。
再看 top:它报 37.5 us, 62.5 sy,几乎和 vmstat 的增量行(34/66、35/65)对得上,而和开机至今的 1.26%/0.66% 差了三十倍。原因就是 top 的 %Cpu 是"近一屏"的增量——第一屏通常是一个很短的采样窗口,负载一起来它立刻显示高占用。所以:
- 想判断"最近这一会儿":看
top的%Cpu或vmstat的增量行。 - 想判断"这台机器长期闲不闲":看
/proc/stat的累计值,或者vmstat的第 1 行。 - 两者都对,只是窗口不同。 把
top的瞬时值和别人截图里的累计值对比,是没有意义的。
顺带一提,top 的第一屏在负载刚起来时就能报出高 us,而 load average 那一行还是 0.20, 0.10, 0.03——因为它俩是两套完全不同的东西。
负载均值不是 CPU 使用率
/proc/loadavg 的三个数字是 1 分钟、5 分钟、15 分钟的平均负载。它的分子是处于 R(可运行/运行中)和 D(不可中断睡眠,通常在等 I/O)状态的进程数,所以:
- 1 核机器上
load = 1.0就是"正好跑满",load = 4.0意味着平均有 4 个进程在抢 1 个核。 - 它不是百分比,也不能反推 CPU 使用率:4 个进程都在等磁盘(D 状态)时,CPU 可能 95% 空闲,而 load 已经是 4。
- 它不是瞬时值。内核每 5 秒采样一次,然后做指数衰减平均,1 分钟那个值的衰减常数是 60 秒。
最后这条实测起来很直观:上面把唯一的 1 个核跑满,跑了约 20 秒后 1 分钟负载是 0.20,进程退出后又涨到 0.37(延迟效应),再慢慢回落。一个核被 100% 占用的这段时间里,load 始终没到 1.0——如果把它当"CPU 使用率"看,会严重低估当时的繁忙程度。反过来,要看"这台机器现在忙不忙",load 的响应速度远不如 %Cpu;load 的价值在于看趋势和揪出 D 状态的进程(top 里按 b 高亮,或 ps -eo state,pid,cmd | awk '$1=="D"')。
关于用 top / ps 定位负载元凶的完整流程,可以看之前这篇:Linux 进程与性能排查:从 top/ps 到负载与僵尸进程定位。
steal:VPS 上最该看懂的字段
回到第一张图里那个不起眼的数字:steal 95189 jiffies。换算一下——951.89 秒,将近 16 分钟,占开机以来总时间的 0.88%。
steal 的含义是"本该给你的 CPU,被虚拟化层拿去给了别人"。man 5 proc 的定义是:
Stolen time, which is the time spent in other operating systems when running in a virtualized environment
关键在于这段时间的性质:
- 它不在你的任何进程里。你的进程没有被调度,不是在跑、也不是在等锁,而是物理上没拿到 CPU。
- 它也不在你的磁盘、内存或网络上。看
iostat、看free、看ss都找不到原因。 - 它只在你【被虚拟化】的时候存在。物理机上这个字段恒为 0,所以在 VPS / 云主机 / 容器宿主机里才有意义。
所以它对应的真实体验是:程序"卡了一下",但所有指标都正常。这台机器 0.88% 属于很健康的水平(换算成每天大约 12 分钟),邻居不吵。
怎么用它做判断:
- 看长期累计占比,也就是第一张图里那种算法。
steal占开机总时间的比例长期超过 5%,就说明这台机器被超卖得比较厉害;超过 10% 基本可以认为性能不可预期,该考虑换机器了。 - 别只看瞬时值。实测这台机器在负载跑起来时,
top的st和vmstat增量行的st都是0.0——因为这几秒里恰好没被偷。瞬时st = 0不能证明"从来没被偷过",只有累计值能回答这个问题。 - 容器场景要小心:cgroup v1 时代容器里读到的
steal常常是宿主机的值,不同运行时、不同版本表现不一致,别拿它当容器配额的依据。
顺带说说 iowait:官方盖章"不可靠"
iowait 是另一个经常被误读的字段。它看起来像"磁盘等待时间",但 man 5 proc 直接写了 This value is not reliable,并列了三条理由:
- CPU 并不会真的"等 I/O 完成",它进 idle 时如果有别的任务可跑,就会去跑别的任务;
- 多核机器上,等 I/O 的任务不在任何 CPU 上运行,每个 CPU 的 iowait 很难算准;
- ⚠️ 这个值在某些情况下会变小——也就是说它不是单调递增的计数器。
第三条最要命:上面所有算法都建立在"字段只会变大"这个前提上,iowait 偏偏不保证。所以不要把它放进增量算法里当计数器用,只把它当"最近有没有在等 I/O"的模糊指示。这台机器上它是 0.07%,说明几乎没有磁盘瓶颈——这也和 5.2G 可用空间、discard 挂载的 SSD 云盘状态相符。
一套分诊顺序
把上面的口径落到排查上,顺序大致是:
| 现象 | 指向 | 先看什么 | |
|---|---|---|---|
us 高,id 低 | 用户态在烧 CPU | top -H 找出线程,再进应用 profile | |
sy 高 | 系统调用/中断/网络栈 | 结合 %si、ctxt、in(上下文切换与中断速率) | |
wa 高(且 id 也高) | 磁盘/网络存储慢 | iostat -x、pidstat -d,或 Linux 内存排查:从 free 到 /proc/meminfo 与 OOM Killer 定位 | |
st 持续高 | 宿主机超卖/邻居吵闹 | 累计占比换算,对比不同时段 | |
load 高但 %Cpu 低 | 多半是 D 状态在等 I/O | `ps -eo state,pid,cmd \ | awk '$1=="D"'` |
1 核机器上 load ≈ 1 且 %Cpu 满 | 正常满载,不是故障 | 看是不是自己的业务确实需要更多核 |
监控工具本身的口径差异,之前单独整理过一篇:Linux 系统实时监控:从 htop 到 iostat、mpstat 与 ss 的性能观测工具;想把某个进程钉在低优先级或独占某些核,是另一条线:Linux 进程调度优先级:从 nice/renice 到 ionice 与 chrt。
回到最开始的那句话:内核只给你一串单调递增的整数,百分比全是工具自己减出来的。知道工具用的是哪两个采样点、窗口多长、哪些字段被合并了,就不会再被三个不一致的数字弄糊涂——而在 VPS 上,steal 这个既不记在你的代码里、也不在你的磁盘上的字段,往往才是"莫名其妙变慢"的答案。
评论 (0)
暂无评论,快来抢沙发吧!