暗色模式

Linux CPU 使用率是怎么算出来的:从 /proc/stat 到 steal 窃取时间

技术教程
2026-09-30
6
0
本文要点
  • 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,user 37.87%、system 62.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) 秒"

读取 /proc/stat 第一行原始数据,并用 awk 把它拆成 8 个字段与各自的开机以来累计占比,最后打印 jiffies 频率、核数与开机时长

实测输出:

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 一致):

#字段含义本机累计
1user用户态时间(不含 nice)1.26%
2nicenice 值大于 0 的用户态时间1.66%
3system内核态时间0.66%
4idle空闲95.46%
5iowait等待 I/O 完成的时间(⚠️ 官方标注不可靠,见下文)0.07%
6irq硬中断处理0.00%
7softirq软中断处理0.01%
8steal被虚拟化层拿走的时间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

先用 timeout 启动一个占满单核的死循环,再用 awk 读取两次 /proc/stat 求差,得到 3 秒窗口内各字段的增量与占比

实测输出:

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%

三个可以立刻读出来的信息:

  1. idle 归零——唯一的那个核被占满了。1 核机器上"CPU 使用率"就是 100% − idle%,这里正好 100%。
  2. 合计 301 jiffies ≈ 3 秒(3 × 100 = 300),说明这个窗口覆盖了完整的 3 秒,采样本身没有丢拍。窗口内总和恒等于 时间 × CLK_TCK × 核数,这是校验脚本有没有写错的最快办法。
  3. system 62.13% 比 user 37.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)"

vmstat 的三行数据(第一行为开机至今平均,后两行为每秒增量),以及 top 一屏显示的 %Cpu 与负载均值

实测输出(节选):

 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,并列了三条理由:

  1. CPU 并不会真的"等 I/O 完成",它进 idle 时如果有别的任务可跑,就会去跑别的任务;
  2. 多核机器上,等 I/O 的任务不在任何 CPU 上运行,每个 CPU 的 iowait 很难算准;
  3. ⚠️ 这个值在某些情况下会变小——也就是说它不是单调递增的计数器。

第三条最要命:上面所有算法都建立在"字段只会变大"这个前提上,iowait 偏偏不保证。所以不要把它放进增量算法里当计数器用,只把它当"最近有没有在等 I/O"的模糊指示。这台机器上它是 0.07%,说明几乎没有磁盘瓶颈——这也和 5.2G 可用空间、discard 挂载的 SSD 云盘状态相符。

一套分诊顺序

把上面的口径落到排查上,顺序大致是:

现象指向先看什么
us 高,id 低用户态在烧 CPUtop -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 这个既不记在你的代码里、也不在你的磁盘上的字段,往往才是"莫名其妙变慢"的答案。

发表评论

暂无评论,快来抢沙发吧!