0%

重新认识 top:从内核视角读懂每一个数字

重新认识 top:从内核视角读懂每一个数字

告警群里弹出一条”CPU 使用率超过 90%”;用户反馈接口突然变慢;或者 ssh 登上一台机器,敲什么都迟半拍。这些时刻,多数工程师的第一反应是同一个动作:敲下 top

先别急着在屏幕上找答案。上面这几种场景,恰好对应着 top 使用中最常见的三类误判,不妨先自测一下:

  1. %CPU 显示 200%,是不是显示错了?
  2. load average 高,是不是说明 CPU 忙?
  3. kill -9 能不能杀死僵尸进程?

三个问题的答案都是否定的。常见的 top 教程大多停留在”字段含义加快捷键”这一层,而这三个问题的答案藏在同一个地方:内核。本文想说清一件事:top 本身不生产任何数据,它只是内核统计信息的一个展示层。读懂 top,本质上是读懂内核的统计口径。口径清楚了,误区自然消失,最佳实践也就有了依据。

一、top 从哪里拿数据

top 的数据来源是 /proc 文件系统。/proc 是内核提供的虚拟文件系统:不占磁盘空间,其中的”文件”在被读取的瞬间由内核实时生成,相当于内核把自身运行状态以文本形式暴露出来。

这个结论不随发行版变化。RHEL、Ubuntu、Debian、openEuler,以及国产化场景常见的银河麒麟、统信 UOS,其 top 都来自 procps-ng 项目;嵌入式常用的 BusyBox top 是另一份实现,读的同样是 /proc。原因很简单:/proc 是内核接口,不是发行版组件,发行版能换的只是展示层。严格说,时钟节拍频率(USER_HZ)、页大小这类常量经由 sysconf 等系统调用获取,但所有动态统计都出自 /proc,一台卸载了 /proc 的机器上 top 直接无法运行。

关于数据形态,只需记住一句话:内核记的是累计值,top 算的是差值。内核在 /proc 里记录每个 CPU、每个进程自开机(或启动)以来累计消耗的时钟节拍数;top 每隔一个刷新间隔(默认 3 秒)读一次,用本次减上次、再除以间隔,才得到屏幕上的百分比。这个机制有三个直接推论,每个都对应一条实践规则:

机制事实 推论 对应动作
首屏没有”上一次读数”可做差 首屏 %CPU 是启动以来的历史均值,不是当前值 看第二屏再下判断;脚本采集用 top -b -n 2 取第二帧
百分比是 3 秒窗口的均值 亚秒级毛刺被抹平,短周期任务的数字会跳变 需要秒级粒度改用 pidstat 1
top 每周期遍历全部 /proc/[pid] 目录 进程数以万计时 top 自身 %CPU 明显升高 属观察行为的固有开销,不必当成问题排查

二、一屏两区:先认布局,再读数字

top 的一屏天然分为两个区域,两个区域的数据来源和回答的问题都不同:

  • 上半区(汇总区):五行文字,每行对应一个子系统——负载、任务清点、CPU、物理内存、交换空间。数据来自全局文件 /proc/loadavg、/proc/stat、/proc/meminfo,回答的问题是”这台机器哪类资源紧张“。
  • 下半区(进程区):一行一个进程,数据来自各进程的 /proc/[pid]/ 目录,回答的问题是”是谁造成的“。

推荐的阅读顺序也由此而来:先扫上半区定性,再进下半区定位。下面按这个顺序,把两个区逐一讲清。

三、上半区:五行汇总,逐行读

第 1 行:load average,一个经常被读错的三元组

load average 的定义是:处于可运行状态(R)和不可中断睡眠状态(D)的任务总数,经指数衰减移动平均后的结果,分 1、5、15 分钟三个窗口,内核约每 5 秒采样一次。

注意这个”约 5 秒”是内核更新 load 的节奏,与 top 的 3 秒刷新间隔无关,两者属于不同的层:CPU 百分比是 top 自己拿两次读数做差算出来的,窗口由 top 的刷新间隔决定;而 load 是内核调度器算好放在 /proc/loadavg 里的现成值,top 只负责照抄。把刷新间隔改成 1 秒,load 也不会每秒变化。

关键在于 Linux 把 D 状态计入了负载。这是 Linux 有别于其他 Unix 系统的设计:早年的负载只统计可运行任务,反映对 CPU 的需求;Linux 在修改后把等待磁盘 I/O、NFS 响应乃至部分内核锁的任务也计了进来,让 load 反映”系统的整体需求”。这个口径差异是真实存在过的历史决定,Brendan Gregg 曾专门考证过 1993 年引入该改动的内核补丁。

由这个口径可以推出排障时最有价值的一条规律:load 很高、CPU 却很闲,说明大量任务卡在 D 状态。Red Hat 官方案例库里有一个典型工单:一台 8 核机器 load average 达到 124,619 个任务中仅 1 个在运行,CPU 空闲率 96.9%,连 wa 都只有 0.2%,经内核转储分析确认原因是大量进程(多为 tcsh)陷入 D 状态(约 120 个 D 状态任务加 1 个运行任务,与 load 数值刚好对得上)。遇到这种形态,排查方向是磁盘、NFS 挂载点、内核锁,而不是 CPU。

再说 load 的数值怎么读。先明确一点:load 没有固定的告警阈值,任何”超过某个数就有问题”的判断,只要不带上核数,都不成立。早年单核机器上拿绝对值当阈值是可行的,因为核数恒为 1,load 本身就等于每核的任务数;多核时代沿用同样的绝对值,只会制造误报。load 是任务数,它的参照系是核数:用 load 除以核数,得到的是平均每个核上”正在运行加正在排队”的任务数,这才是可比较的量。由此建立读数标准(以纯 CPU 负载为前提):

  • load 明显小于核数:有余量;
  • load 约等于核数:刚好排满,无排队也无余量;
  • load 持续大于核数:任务在排队,超出越多,排队越长。

同一个 load 为 8,含义完全取决于核数:8 核机器上是刚好排满,2 核机器上是平均每核 4 个任务挤在队列里,属于严重过载,32 核机器上则相当空闲。

在这个标准之上还有两条修正。第一,Linux 的 load 计入 D 状态,所以”load 大于核数”也不能直接等同于 CPU 不够,要结合 CPU 行分流:id 低,说明任务确实在争抢 CPU;id 高,就回到前面讲的 D 状态排查。第二,单点读数不如趋势:1 分钟值明显高于 15 分钟值,说明压力正在上升;反过来,说明高峰已过,三个窗口对照着看比盯任何一个绝对值都可靠。

第 2 行:Tasks 与进程状态,一个字母一种命运

Tasks 行清点全部进程并按状态分类,这也是提前认识状态字母的最佳位置,因为下半区 S 列用的是同一套字母:

字母 状态 说明
R 运行/就绪 正在 CPU 上执行,或在运行队列中等待 CPU
S 可中断睡眠 等待事件(网络、定时器等),可被信号唤醒。绝大多数进程绝大多数时间处于此状态,属正常
D 不可中断睡眠 在内核态等待必须完成的操作(典型为磁盘 I/O),不响应任何信号
T 停止/被跟踪 收到 SIGSTOP,或被调试器暂停
Z 僵尸 已退出,等待父进程回收

值得展开的是两种”杀不掉”的状态。

Z(僵尸):进程已经退出,代码、数据、打开的文件全部释放,只剩进程表里一条记录,保存着退出码,等待父进程调用 wait() 系列函数回收。由此可以推出僵尸的全部性质:它不占内存、不占 CPU,只占一个 PID;kill -9 对它无效,因为进程本体已经不存在,没有东西可杀。正确的处理方向是父进程:让父进程执行回收逻辑,或者杀掉父进程,僵尸成为孤儿后由 init/systemd 接管并自动回收。少量短暂的僵尸无害;持续增多的僵尸指示父进程存在代码缺陷,通常是没有正确处理 SIGCHLD 信号。

D(不可中断睡眠):任务在内核态等待某个必须完成的操作,期间不响应包括 SIGKILL 在内的任何信号,kill 同样无效。设计初衷是这类等待应当在极短时间内结束,不值得为响应信号增加复杂度。所以 D 状态通常一闪而过;一旦有进程长期停在 D 状态,几乎必然指向底层异常,排查方向是存储设备故障或 NFS 服务端无响应。回看第 1 行:这些 D 状态进程同时会把 load 推高,两个现象常常一起出现,根因是同一个。

第 3 行:%Cpu(s),CPU 时间的八种去向

这一行把全部 CPU 时间划分成八份,相加为 100%:

字段 含义
us 用户态时间(nice 值为 0 及负值的普通任务)
sy 内核态时间(系统调用、内核线程)
ni nice 值为正的低优先级任务的用户态时间
id 空闲
wa 空闲且有任务在等待块设备 I/O(详见下文)
hi 硬中断处理
si 软中断处理
st 虚拟化环境中被宿主机拿走的时间(详见下文)

hi 和 si 两个字段单独说清。中断是硬件打断 CPU 当前工作、要求内核立即处理的机制:网卡收到数据包、磁盘完成一次读写,都靠中断通知内核。Linux 把中断处理拆成两段:必须立即执行的一小段计入 hi(硬中断),可以推迟执行的大头计入 si(软中断),网络收发包的协议栈处理就落在 si 里。读法上,两者平时都接近 0。hi 明显偏高很少见,方向查硬件与驱动;si 持续偏高的典型场景是高流量网络收发,且常伴随负载集中在少数核的现象,处置方向是中断亲和性与多队列分流(irqbalance、RSS/RPS)。

日常读法:us 高查应用逻辑;sy 异常高查系统调用和内核行为;si 高先看网络流量。另外要记住这一行是所有核的平均值,而均值会抹平偏斜:单线程程序打满一个核、中断集中在少数核,在汇总里都看不出来。按 1 展开到每个核观察,是发现这类”忙得不均匀”问题的第一动作。需要专门讲清的是 wa 和 st 两个误区。

误区:wa 高就是磁盘瓶颈,wa 低就没有 I/O 问题。 iowait 的定义是:CPU 处于空闲、且至少有一个任务在等待块设备 I/O 完成的时间。注意两个关键词:空闲、任务。CPU 本身从不”等待 I/O”,它要么在执行指令要么空闲;等待 I/O 的是任务,而任务在等待期间不在任何 CPU 上运行。所以 iowait 本质上是 idle 的一个子集:这段时间 CPU 无事可做,只是”恰好有人在等磁盘”。

这个口径带来两个反直觉的性质。其一,一旦有别的任务把 CPU 用起来,wa 就会下降甚至归零,但磁盘的拥堵并没有消失,只是 CPU 不再空闲、没有时间可记到 iowait 名下了。其二,多核会稀释它:64 核机器上一个线程干等磁盘,wa 最多贡献约 1.6%(1/64)。内核文档自己就明确提示过 /proc/stat 中的 iowait 并不可靠,理由正是上述几点,还包括该值在特定条件下甚至会回退。

由此得出使用规则:wa 高提示存在 I/O 等待,值得追查;wa 低不能排除 I/O 问题。确认磁盘瓶颈要看 iostat -x 的 await 与 %util,较新的内核还可以看压力失速信息 /proc/pressure/io,它直接统计”有任务因等 I/O 而停顿”的时间占比,不受上述两个性质的干扰。

误区:忽略 st。 st(steal)自内核 2.6.11 引入,官方定义为:在虚拟化环境中运行时,被其他操作系统占用的时间。落到云主机场景,它就是 vCPU 已经就绪、但宿主机没有把物理核调度给这台虚拟机的时间。

要明确观察位置:st 是站在虚拟机(云主机)内部看的字段。登录到虚拟机里运行 top,st 才有意义;物理宿主机自己的 top 里 st 恒为 0,因为没有人能”偷”物理机的时间。宿主机视角要回答的是”哪台虚拟机在挨饿”,观察对象是各 vCPU 线程的调度延迟,那是另一套工具的事。应用没有任何变化、监控也没有异常、但整体就是变慢了,这类问题在云上出现时,st 是首先要看的字段之一。经验上 st 持续超过 5%~10%(估计值,随业务对延迟的敏感程度浮动)就说明宿主机超卖或同宿主机的邻居负载过重,这类问题在虚拟机内部无法解决,处置手段是提工单或迁移实例。

第 4、5 行:内存与交换,别看 free,看 avail Mem

Mem 行的 buff/cache 是块设备缓冲、页缓存与可回收 slab 之和。Linux 的策略是把空闲内存尽量用作页缓存来加速文件读写,需要时再回收,所以free 很小不代表内存紧张,这是内存部分的第一误区。

真正该看的是行尾的 avail Mem,它对应 /proc/meminfo 的 MemAvailable 字段:内核 3.14 引入,定义为”在不发生交换的前提下,可供新应用使用的内存估计值”,计算时已扣除不可回收的缓存和内核水位线预留。这个字段的引入本身就是为了纠正一个流传已久的错误算法:提交该补丁的内核开发者 Rik van Riel 在提交说明里写道,很多负载调度程序用 free 加 cached 估算可用内存,这种算法十年前还行,如今几乎注定是错的,因为 cached 里含有共享内存段、tmpfs 等根本不可释放的部分,同时又漏掉了可回收的 slab。换句话说,这个误区流传得太广,以至于内核专门加了一个字段来终结它。

Swap 行的相邻误区:swap 有使用量不等于内存不足。内核会在内存并不紧张时把长期不活跃的匿名页换出,为页缓存腾出空间,这个倾向由 vm.swappiness 参数调节。判断内存是否真的紧张,看的是换入换出是否持续发生(vmstat 的 si/so 列),而不是 swap 的存量。

四、下半区:进程列表的十二列

默认配置下进程区有十二列,先给全量含义,再讲其中的三个误区:

含义 备注
PID 进程号 线程视图下为线程号
USER 有效用户名
PR 内核调度优先级 普通任务为 20+NI;显示 rt 的是实时任务
NI nice 值 范围 -20~19,越小优先级越高
VIRT 虚拟地址空间总量 见误区二
RES 驻留物理内存 含共享页,见误区二
SHR RES 中可共享的部分 主要是共享库、共享内存段
S 进程状态 字母含义见第 2 行的状态表
%CPU 上次刷新以来的 CPU 占比 默认 Irix 口径,见误区一
%MEM RES 占物理内存的百分比 继承 RES 的口径问题
TIME+ 累计消耗的 CPU 时间 见误区三
COMMAND 命令名 按 c 切换完整命令行

误区一:%CPU 超过 100% 是异常

top 默认工作在 Irix 模式:%CPU 以单个核为基准,一个多线程进程在 4 核机器上最高可以显示 400%。procps 官方手册对此说得很清楚:按 I 键切换到 Solaris 模式后,任务的 CPU 使用率会除以 CPU 总数,上限回到 100%。

两种模式没有对错,只有口径之分:Irix 模式回答”这个进程占了几个核”,Solaris 模式回答”这个进程占了整机算力的百分之几”。所以判断进程是否吃满 CPU,正确做法是拿 %CPU 和 nproc 的输出对照,而不是和 100% 对照。容量评估里常说的”CPU 使用了 3.5 核”,就是 Irix 口径除以 100 的结果。

误区二:一笔算不平的账——RES 相加超过总内存

把所有进程的 RES(或 %MEM)加起来,会明显超过系统实际使用的内存,很多人第一次发现时以为统计出错了。原因是共享页被重复计算:一份 glibc 驻留在物理内存中,映射它的每个进程的 RES 都把这一份算了进去。同理,VIRT 大也不代表占用内存多,它包含 malloc 后尚未写入的内存、mmap 映射的文件和共享库,JVM、Go 程序 VIRT 达到几十 GB 属正常现象。

公平的分摊指标是 PSS(Proportional Set Size,比例集大小)。内核文档给出的定义与算例都很直白:进程在物理内存中的每一页,除以共享这一页的进程数后求和;一个进程独占 1000 页、另与一个进程共享 1000 页时,PSS 为 1000 + 1000/2 = 1500 页。top 不显示 PSS,需要读 /proc/[pid]/smaps_rollup(内核 4.14 起提供,是对逐段 smaps 的汇总,读取代价低得多),或使用 smem 工具。评估”这个服务的多个进程一共占多少内存”,PSS 是唯一加得平的口径。

误区三:TIME+ 不是运行时长

TIME+ 是进程启动以来累计消耗的 CPU 时间,精确到百分之一秒。一个挂了三个月、几乎不干活的守护进程,TIME+ 可能只有几分钟;一个刚启动十分钟的计算任务,TIME+ 可以逼近十分钟乘以核数。把它读成”运行了多久”,会在追查”谁长期消耗资源”时得出相反的结论。要看进程启动时刻,用 ps -o pid,lstart,etime -p PID

五、容器里的 top:数字属于宿主机

这是容器时代最实用、也最少被讲透的一条:**/proc 中的全局统计文件不感知 cgroup**。/proc/meminfo、/proc/stat 反映的永远是宿主机整体的状态,容器的资源限制(cgroup)并不会改写它们。于是在一个被限制为 2 核 4 GB 的容器里运行 top,上半区显示的却是宿主机的 64 核 256 GB,CPU 利用率和 load 也都是宿主机的。这不是 top 的缺陷,是它的数据源决定的。

这个口径问题的影响远不止”看着别扭”。任何按”看到的资源”自动调参的程序都会中招:最著名的案例是早年的 JVM,按宿主机核数设置 GC 线程数、按宿主机内存设置默认堆上限,在小容器里频繁触发 OOM,直到 JDK 10 引入容器感知(UseContainerSupport,后向移植到 JDK 8u191)才系统性解决。

要在容器里获得真实配额,有两条路。一是直接读 cgroup 接口文件:cgroup v2 下看 /sys/fs/cgroup/ 中的 cpu.max 和 memory.max,配合 memory.current 可算出真实的内存水位。二是在宿主机部署 lxcfs:按 Ubuntu 官方的介绍,它是一个 FUSE 文件系统,把按 cgroup 过滤后的 cpuinfo、meminfo、stat、uptime 绑定挂载到容器的 /proc 上,让 top、free 这类工具直接显示容器视角的数据。Kubernetes 环境下更常见的做法则是绕开容器内观察,直接用 kubectl top 或 cAdvisor 体系读 cgroup 统计。

六、把 top 用顺手:分场景速查

场景 A:定位 CPU 问题

按键/命令 作用 何时用
P 按 %CPU 排序 找出 CPU 消耗大户
1 展开每个核的利用率 识别单核瓶颈:单线程程序打满一个核时,汇总口径可能只有百分之几
I 切换 Irix/Solaris 口径 需要”占整机百分之几”的视角时
H 切换线程视图 Java、Go 服务定位热点线程;线程号转十六进制可对上 jstack 的 nid
T 按 TIME+ 排序 找长期累计消耗大的进程

场景 B:定位内存问题

按键/命令 作用 何时用
M 按 %MEM 排序 找出内存占用大户(注意 RES 口径含共享页)
E / e 切换头部/进程区的内存单位 大内存机器上先调单位再读数,避免数量级看错
f 字段管理,增删列、指定排序列 需要 SWAP、CODE、DATA 等非默认列时

场景 C:视野与效率

按键/命令 作用
c 显示完整命令行,区分同名进程
V 树形视图,看清父子关系
u 按用户过滤
x 高亮排序列,避免看错列
L 在进程列表中搜索字符串
W 保存当前布局到配置文件,下次打开保持一致

场景 D:脚本与采集

命令 说明
top -b -n 2 -d 1 批处理模式输出两帧、取第二帧(首屏是历史均值,见第一节)
top -b -o %CPU -n 2 按指定列排序输出
top -b -p PID1,PID2 -n 2 只采集指定进程

场景 E:top 回答不了的问题,交给什么工具

需求 更合适的工具
回看历史(昨晚发生了什么) atop、sar
秒级甚至亚秒级粒度 pidstat 1
容器口径的资源观察 kubectl top、cgroup 接口文件
确认磁盘瓶颈 iostat -x、/proc/pressure/io
按 PSS 口径算内存 smem、/proc/[pid]/smaps_rollup
更好的交互体验 htop、btop

上表按问题横向速查。如果按深度纵向看,还有一条从 top 出发的进阶路线,按观察深度分五层,每一层回答的问题都比上一层更具体:

  1. 全局快照:top、vmstat 1,回答”哪类资源紧张”;
  2. 分子系统定量:mpstat -P ALL(CPU 按核)、iostat -x(磁盘)、pidstat(按进程)、ss(网络连接状态),回答”紧张到什么程度、谁造成的”,sar 为这一层补上历史维度;
  3. 系统调用与报文:strace 看进程在做什么系统调用,tcpdump 看网络上实际发生了什么,一个管进程边界,一个管网络边界;
  4. 性能剖析:perf 采样加火焰图呈现,回答”CPU 时间具体烧在哪个函数上”;
  5. 动态追踪:eBPF 体系(bpftrace 即席追踪、BCC 工具集),在不重启、不插桩的前提下观测内核任意路径,这一层的尽头就是 Linux 内核本身。

每往下一层看得更深,但起点始终是 top 这一屏。top 的不可替代性只有一条,但足够硬:它几乎存在于每一台 Linux 机器上,包括那台出了问题、装不了任何新工具的机器。