你打开终端,敲下 htop。屏幕上跳出一堆数字:CPU 50%、内存 8G/16G、负载 2.5。它们一直在变。你觉得你大概看得懂——数字高了就紧张,低了就放心。
笔者也这么以为,直到看到 Peteris Rocks 那篇 2019 年的经典文章再次登上 Hacker News 榜首。
那篇文章做了件很少人做的事:逐行、逐列、逐字段地解释 htop 屏幕上每一个数字背后的内核数据来源。读完之后,笔者的感受只有四个字:原来如此。
本文基于那篇文章的核心内容,结合 HN 社区的讨论和技术演进,尝试把这场”重新理解监控”的旅程讲清楚。
反派登场:你的直觉 vs 内核的真相
这里存在一对明确的张力关系:用户以为自己读懂了监控面板 vs. Linux 内核实际在汇报的数据。
这两个东西之间的关系,很多时候并非”简化版 vs. 完整版”,而是”字面意思 vs. 你想歪了的意思”。下面逐个拆解。
负载均值:最被误读的数字
htop 右上角有三个数字,比如 0.00 0.01 0.05,分别代表 1 分钟、5 分钟、15 分钟的负载均值。
大多数人的直觉反应:双核机器上负载 1.0 = CPU 用了 50%。
这是一个可以理解的误解,但它错了两层。
第一层:负载不是 CPU 使用率
负载数字统计的是正在运行(running)+ 等待运行(runnable)+ 不可中断睡眠(uninterruptible sleep,通常是在等磁盘 I/O)的进程数。换句话说,它衡量的是”有多少个任务在排队等资源”,而不只计算正在占用 CPU 的任务。一个进程卡在磁盘 I/O 上,CPU 空闲,但负载依然会上升。
第二层:它不是简单的滑动平均
大多数人直觉里,“1 分钟负载均值”就是”过去 60 秒的平均值”。但内核实际上用的是指数衰减移动平均(exponentially damped moving average)。
从 Wikipedia 引述原文:
数学上讲,这三个值平均的是自系统启动以来的全部负载。它们都以指数方式衰减,只是衰减速度不同。1 分钟负载均值包含了上一分钟 63% 的负载,加上系统启动以来(不含最近一分钟)37% 的负载。
所以它不是”过去 60 秒的平均值”。它永远带着历史包袱,只是越近的数据权重越大。
工程判断:负载均值适合用来感知趋势变化——1 分钟值比 15 分钟值高很多,说明压力在上升。但它不适合做精确的性能建模。要精确数据,请直接看 /proc/loadavg 里的运行进程数和 CPU 时间片分配。
顺便一提,Linux 内核源码 kernel/sched/loadavg.c 文件头部的注释写道:“这个文件包含了计算全局负载均值的魔法位。它是一个愚蠢的数字,但人们觉得它很重要。“连内核开发者自己都这么写。
内存:VIRT 是谎言,RES 是真相,但也不全是
htop 的进程列表里有四列关于内存:VIRT、RES、SHR、MEM%。大多数人只瞥一眼 RES(常驻内存),觉得那就是”进程吃了多少内存”。这个直觉大致对,但细节偏差可以很大。
VIRT(虚拟内存)——最大的谎言制造机
VIRT 包含了进程地址空间里的全部映射:代码段、数据段、堆、共享库、mmap 映射的文件、以及已申请但从未实际使用过的虚拟地址空间。
这意味着:如果一个进程 mmap 了一个 2GB 的日志文件,VIRT 就会显示 +2GB。但这 2GB 只有在实际被读取时才会触发缺页中断、分配物理页。日常使用中这可能只占了几 MB 物理内存。
HN 上 AnotherGoodName 的评论精确描述了这个问题:
“Chrome 曾经因为使用 mmap 映射文件而遇到这个麻烦——mmap 本身没问题,但用户看到虚拟内存数字之后会疯掉。实际上根本没占那么多物理内存。”
工程判断:VIRT 几乎不值得看。它既不能告诉你进程实际消耗了哪些物理资源,也不反映内核的内存压力。它唯一的价值在于让你知道某个进程的地址空间是否在异常膨胀(可能暗示内存泄漏),但即使这个判断也建议通过 RES 的增长趋势来做。
RES(常驻内存)——最接近真相
RES 是进程当前实际占用的物理内存页数量(未换出到 swap 的)。这是你在”内存用了多少”这个问题上最该关注的数字。
但它也有坑:RES 包含了与其他进程共享的页面(比如 libc 库),所以把所有进程的 RES 加起来会重复计算共享部分。
SHR 和 MEM%
SHR 是共享内存页的大小。如果一个库被 100 个进程加载,每个进程的 SHR 字段都会包含这个库的大小,但内核只分配了一份物理页。
MEM% = RES / 总物理内存。这个值简单直观,但(a)共享部分被重复计入,(b)它不考虑内核缓存(page cache)占用的内存——而这往往才是”内存去哪了”的真正答案。
HN 上 d3Xt3r 指出:比 RES 更精确的指标是 PSS(Proportional Set Size,按比例分配的常驻内存)——共享页面按进程数量均分。但 htop 默认不显示它,因为读取 PSS 需要特殊权限,且对内核性能有微小影响。
工程判断:日常看 RES 和 MEM% 就够了,但做容量规划时请记住:所有进程 RES 之和 > 实际物理内存使用量 是正常的。想知道真实缺口,看 free -h 里的 available 字段。
进程状态:S 不是”Sleep”,D 才是真正危险的
htop 的 S 列显示进程状态。大多数人看到 S 就放心了(“在睡觉,不占 CPU”),看到 R 就觉得正常(“在跑”)。真正值得警觉的状态反而是最少被注意的那个:D。
各状态的真相
- R(Running/Runnable):正在 CPU 上执行,或在运行队列里排队。这是唯一”活跃”的状态。
- S(Interruptible Sleep):等待某个事件完成(网络数据、定时器、用户输入)。事件到来后可以被立即唤醒。绝大多数后台进程都处于 S 状态,这是正常的。
- D(Uninterruptible Sleep):也在等待,但不能被信号打断。通常是在等磁盘 I/O 完成。如果一个进程长期卡在 D 状态,你的磁盘可能出问题了。更关键的是:D 状态的进程会被计入负载均值——这也是为什么磁盘 I/O 瓶颈会让负载飙升但 CPU 看起来不忙。
- Z(Zombie):进程已终止,但父进程还没调用
wait()来回收它。僵尸进程不占用内存和 CPU,但会消耗进程表项(PID)。大量僵尸进程意味着父进程有 bug。 - T(Stopped):被作业控制信号(如 Ctrl+Z)暂停。
t小写表示被调试器(如 gdb)暂停。
工程判断:如果你看到大量 D 状态进程 + 高负载 + 低 CPU 使用率,排查方向应该从”谁在吃 CPU”转向”磁盘是不是快挂了”。用 iostat -x 1 看看磁盘的 await 和 util%。
两个”升维”操作
HN 上 cogman10 的评论(20 小时内获得大量赞同)提到了两个设置,笔者认为是这篇文章最有价值的实践建议:
1. 禁用用户线程视图
htop 默认显示所有线程(包括用户态线程)。在 Setup → Display Options → Hide userland process threads 中可以关闭。
关闭之后,视图像是从一堆噪音中抽出了信号。你会看到真正在工作的进程,而不是被线程池的几十个 worker 线程淹没。
2. 启用进程树视图
按 F5 或 t 切换进程树视图。你会看到进程之间的父子关系——哪个 shell 启动了哪个脚本,脚本又 fork 出了什么子进程。
一个经典场景:编译器(如 gcc)会 fork 出多个子进程并行编译。进程树让你一眼看出这些进程的关系和层级,这比按 CPU 排序更有诊断价值。
注意:进程树模式下动态排序会被禁用(如 HN 上 zekrioca 所指出的),这是取舍——结构 vs. 排序,不能同时拥有。
btop 和其他选择
HN 讨论中大量用户推荐了 btop 作为 htop 的现代化替代品。
优势:更现代的 TUI 界面、GPU 监控、网络和磁盘吞吐率图表、主题支持、鼠标交互。
劣势(综合 HN 讨论):需要至少 80×24 的终端窗口(而 htop 可以在 40×8 下运行);在串口线(57600 bps)上几乎不可用,因为它重绘整个屏幕而非增量更新;不支持 ZFS/ZRAM 统计;部分平台上存在 32 位整数溢出 bug(FreeBSD/OpenBSD)。
笔者的看法:btop 适合日常桌面监控——漂亮、信息密度高。但如果你通过 SSH 连到一台生产服务器排查问题,htop 的轻量性和兼容性仍然是更务实的选择。工具没有绝对的好与坏,只有场景适配。
另有一些用户提到了 procs(基于 Nim 语言,支持 PSS 指标)、nmon(IBM 出品,磁盘 I/O 监控很强)、bottom(Rust 实现)等替代品。选择取决于你需要看到什么维度的数据。
结语
htop 是一面镜子,但镜子里的倒影有时和你以为的不一样。
负载均值不是 CPU 使用率,不是简单平均,甚至内核开发者都管它叫”愚蠢的数字但人们觉得重要”。VIRT 是一个工程上几乎无用的字段,却占据了宝贵的屏幕列宽。D 状态进程比你想象的危险得多——它们不仅卡住了自己,还会污染负载均值。
下次打开 htop 的时候,不妨停一秒,问自己:这个数字到底是从哪个 /proc 文件读出来的?内核是怎么算的?
懂了这一点,屏幕上的数字就不再只是跳动的符号——它们开始讲故事了。
参考链接:
本文的分析基于 Peteris Rocks 的原博文、Linux 内核文档和 HN 社区讨论。对某个指标的解读如果有更精确的理解,欢迎指出。