“程序卡住了”不是一个足够具体的结论。它可能在忙算、等锁、等网络、等磁盘,也可能已经退出,只剩父进程还没有回收的记录。真正有效的排查,是逐步回答:哪个进程?哪个线程?当前在做什么?它在等哪个对象?
这篇以 ps、top、pstack、strace、lsof、ipcs 六个工具建立这条证据链。工具清单的选题线索来自 Linux 工具快速教程的 pstack 入口,以下组织、实验与判断方法独立编写,命令语义以各工具上游手册为准。
系列阅读:学习地图 → 本篇 → 内存与 I/O 监控 → ELF 与链接 → 传输与任务调度。需要源码级追踪时,接着看 GDB 实战。
实验与观察边界
所有动画都是实验实际输出的分帧重排,不是屏幕录像,也不是伪造一段漂亮终端。进程号、地址、线程顺序及采样数值会变化;判断是否跑通,以 验证状态 和 记录会话 为准。
在完整网站源码仓库根目录执行:
bash docker/linux-tools-lab/run.sh build |
具体环境、依赖和输出目录见 实验 README。这里不假定宿主 macOS 上的同名命令等同于 Linux 版本,也不把容器内观察自动解释为宿主机全貌。
实验的 tool_demo worker 提供可识别的线程、文件和本地 socket,tool_demo once 提供有限的文件访问和等待。程序由实验脚本在隔离环境构建,后文普通命令则展示迁移到自己工程时的使用方式,不要把示意 PID 当成固定产物。
本文把两类操作分开:ps、top、lsof 等主要读取状态;GDB 附加和 strace 则会干预调度。即使不修改业务内存,暂停与额外开销也可能影响超时、控制周期和竞态。机器人实机上先确认允许暂停的边界。
命令中的 TARGET_PID=1234 表示你已核对的目标,不是让你直接照抄 1234。检查所有者、启动时间和命令行,避免进程重启后 PID 被复用。
ps:确认到底是谁
它提供快照,不是实时曲线
先用明确的列建立进程清单,比看一屏默认输出更容易回答问题:
ps -eo pid,ppid,user,stat,etime,pcpu,pmem,rss,comm \ |
pid 定位进程,ppid 给出父进程,lstart 和 etime 核对这次启动;nlwp 是线程数量。comm 偏向执行名称,args 包含参数,二者不是同一种身份信息。命令行可能包含令牌或业务路径,分享前先脱敏。procps-ng 的 ps 手册
读这段操作时,先找目标身份,再比较状态和线程数量;不要先寻找某个“正确的 CPU 百分比”。同一条命令在空闲和繁忙机器上,本来就应该产生不同结果。
STAT 是方向提示
常见状态里,R 是运行或可运行,S 是可中断睡眠,D 是不可中断睡眠,T 是停止,Z 是僵尸。附加字母还会表示多线程、前台进程组等属性。
S 不等于异常,大多数等待工作到来的服务平时就在睡眠。D 常见于某些内核等待路径,但不能只凭一个字母断言磁盘坏了。Z 的进程执行已经结束,保留记录等待父进程回收;它不是仍在大量计算的“顽固进程”。
继续查看线程和父子关系:
ps -L -p "$TARGET_PID" \ |
tid 是 Linux 线程标识;GDB 自己的线程编号是另一套编号。wchan 是内核等待位置线索,受权限和内核符号可见性影响,可能显示 - 或 0,也不能替代用户态完整调用栈。
CPU 与内存列的三个坑
ps 的 %CPU 通常是累计 CPU 时间除以进程生存时间,偏向“出生以来平均”,不是最近一秒。一个运行很久、刚刚突然忙起来的服务,ps 数值可能还很低;这时应转到 top 的间隔采样。
RSS 表示驻留页计数,通常以 KiB 显示;VSZ 是虚拟地址空间大小,不等于已经占用同样多的物理内存。多个进程映射同一批共享页,各自 RSS 里都可能计入,直接相加会重复计算。
需要更细的归属时,可在权限允许的情况下看目标的汇总映射:
cat /proc/"$TARGET_PID"/smaps_rollup |
PSS 把共享页按共享者比例分摊,比简单相加 RSS 更接近比例分摊后的占用;它也不是“杀掉这个进程就一定释放的字节数”。读取详细映射有额外开销,不应对大量进程每毫秒扫描。Linux 内核 /proc 内存接口
最小练习
在实验容器里运行一个会自然退出的子进程,不需要对陌生 PID 发信号:
sleep 20 & |
说明它为什么通常低 CPU、处于睡眠,却仍是正常运行中的进程。等它自然结束后再查询,区分“查询不到”“已退出未回收”和“进程还活着”。
top:定位最近谁在忙
先固定时间窗口
top -p "$TARGET_PID" |
-H 切到线程视角。交互界面里可用 P 按 CPU、M 按驻留内存排序,1 查看分 CPU,H 切换线程显示;具体显示和持久化配置以当前 procps-ng 版本为准。采样分析别只保留刚启动的第一屏,优先比较后续固定间隔刷新。procps-ng 的 top 手册
动画教的是“比较刷新样本”,之后可用上面的线程模式继续缩小范围,不是拿演示机器的百分比作为性能基准。真实机器上短任务可能在两个样本之间已经结束,未出现在榜首不代表没有耗时。
100% 到底意味着什么
在常见 Irix 计量模式下,一个逻辑 CPU 的满占用对应约 100%,多线程进程可能超过 100%;Solaris 归一化模式会按 CPU 数量缩放。先确认模式,再比较截图,不能把 250% 判为工具坏了。
举个只为说明口径的计算:采样间隔为 1 秒,进程各线程合计使用 2.5 秒 CPU 时间,则按单 CPU 归一的使用率是 250%。这不是说墙上时间过去了 2.5 秒,而是多个线程并行消耗了 CPU 时间。
进程 CPU 高但整机总 CPU 不高,可能只是少数核心或单线程繁忙。反过来,整机繁忙不证明你的目标进程繁忙;虚拟机 steal、内核线程和其他租户工作都需要分开。
高负载不等于 CPU 满载
Linux load average 涉及可运行任务和特定不可中断等待,不是 CPU 使用率的另一种写法。不能把 8 核机器的 load=8 不加上下文地解释成“刚好 100%”,更不能用宿主 32 核掩盖容器只获 2 核配额的事实。
针对“服务慢”,比较三组问题:CPU 使用是否持续高?可用 CPU 数及配额是什么?线程是在计算还是等待?这些问题分别需要 top、资源边界信息与调用栈,不是单看榜首能完成的。Linux /proc 统计接口
最小练习
继续对前面的 sleep 子进程采样;若它已经退出,就重新创建一个:
top -b -d 1 -n 3 -p "$LAB_PID" |
解释为什么进程存在却几乎不消耗 CPU。再运行实验 README 中的受控负载,比较进程与线程视角。不要为了做图在共享服务器上运行无限忙循环。
如果已经定位到某个计算线程,但不知道哪段函数耗时,接着看 perf CPU 分析笔记。top 负责找对象,采样剖析负责找热点,两者并不重复。
pstack:拍一张线程栈快照
先确认本机是什么实现
pstack 不是 Linux 内核提供的统一接口。同名工具的包、架构支持和实现可能不同,不能假定每个 Ubuntu ARM64 镜像都有相同程序;先检查命令来源:
command -V pstack |
上游 GDB 的 gstack 是附加进程、打印各线程栈再分离的脚本。某些发行版提供兼容名称,但若本机没有它,直接使用明确的 GDB 批处理操作更可复现。GNU gstack 手册
# 只有确认本机实现和目标权限之后,才使用对应工具 |
这里展示的是采栈能力;实际用了哪个可执行文件,以会话记录为准,不能把 GDB 替代命令伪装成系统已经安装原版 pstack。
看等待关系,而不是背函数名
从 #0 向业务调用者读:最内层在哪个库等待?业务上是谁发起的等待?另一个线程能否满足它?同一个 futex 既可能来自互斥锁,也可能来自条件变量或其他同步机制。
两个线程长期停在相同位置,只证明抓到的样本相似;正常 worker 等待任务也会这样。宣称死锁需要进一步建立循环等待,例如 A 持有 L1 等 L2,B 持有 L2 等 L1。缺通知、任务饥饿、外部设备迟迟不返回是不同问题。
只有地址或 ?? 时,先看二进制和库的符号、栈展开信息、架构以及目标版本。调用栈不是完整执行历史;已经返回的函数不会因为你抓栈就重新出现。
附加并非无侵入
GDB 附加会停止目标,detach 后让它继续。加载大体积符号、遍历很多线程会延长暂停,频繁采栈也会改变时序。GDB 附加与分离
容器中权限不足时,需检查用户、ptrace 策略、PID 命名空间、seccomp 和相关能力。不要为了采一张栈全局关闭安全策略,或默认使用 --privileged;实验只对隔离目标提供所需能力。
最小练习是在实验容器中启动一个有等待线程的示例,采一次栈,说明哪个线程负责唤醒它。再对照源码确认等待条件。最后检查工具确已分离,目标继续运行;不能只验证日志文件出现了。
strace:看它向内核请求了什么
系统调用是边界,不是每一个函数
strace 追踪系统调用和信号。它能看到程序打开文件、读写 socket、等待事件,但不会直接告诉你 C++ 中每次矩阵乘法或普通函数调用的耗时。实现依赖 ptrace 等内核机制,目标会承受额外开销。strace 官方说明
启动时追踪通常比事后附加更适合排查初始化失败:
strace -f -tt -T -s 160 \ |
这个最小练习应先确认测试路径不存在;cat 的非零退出是实验要观察的结果,不是宣称命令执行成功。看它实际访问了哪个路径、收到什么 errno,而不是只看最末尾中文报错。
一次系统调用可按“名称—参数—返回值—错误—时间”阅读。出现 ENOENT 通常意味着找不到某个路径元素,但程序也可能正常探测多个候选文件;EAGAIN 在非阻塞 I/O 里不必然是故障。要结合后续调用和业务终止点解释。
根据问题过滤
strace -f -tt -T -s 160 \ |
-f 跟踪相关子进程/线程;-ff -o /work/trace 可按 PID 分文件,适合并发输出。-tt 给时钟时间,-T 给系统调用开始到结束的经过时间;-c 默认汇总的是系统态 CPU 时间,配合 -w 才按经过时间汇总。汇总模式不保留完整顺序,并行线程的时间也不能直接当应用端到端延迟。strace 上游手册
例如一个 poll 持续较久,可能只是事件循环正常等待;一个 read 很久,可能在等远端数据,不能直接怪本地磁盘。反过来,CPU 忙算却几乎没有系统调用时,空荡荡的 trace 正在提示你换用 CPU profiler。
抓得越多未必越好
全量抓取高频 read/write 不仅可能很慢,还可能记录口令、网络报文和个人数据。先限定 PID、调用类别、字符串长度和采样时长;不要把原始 trace 自动上传到公开笔记。
对已经附加的目标,结束 strace 后要确认它已分离、业务正常继续;“停止跟踪”与“停止被跟踪进程”不是一句话。不要用生产 PID 试验不熟悉的信号组合。
最小练习:把不存在文件改成实验目录里确实存在的小文件,分别观察失败和成功路径;随后比较 -c 的摘要与完整逐行记录,说明为什么只看调用次数不能还原因果顺序。
lsof:把文件和 socket 关联回进程
Linux 的“打开文件”不只有普通文件
lsof 能列出普通文件、目录引用、库映射、管道和 socket 等对象。它最适合回答“哪个进程持有这个资源”,但不是持续记录所有文件事件的历史数据库。
lsof -nP -p "$TARGET_PID" |
-n 避免地址解析,-P 避免端口名称转换。-p 和 -i 这类选择条件默认可能组合成并集;希望看“这个 PID 的 TCP”时显式加 -a,否则结果范围可能远大于预期。lsof 官方手册
优先读 COMMAND、PID、FD、TYPE、NAME。FD 可能是数字描述符,也可能是 cwd、txt、mem 等用途;不是每一行都代表一个新的普通文件描述符。SIZE/OFF 也可能是偏移或与对象类型有关的值,不能将所有行直接相加为磁盘占用。
文件删了,空间为什么没释放
lsof -nP +L1 |
这个选择寻找链接数小于 1、但仍被打开的文件。文件名从目录移除,不代表所有打开它的引用同时消失;进程仍持有引用时,空间可能暂时保留。这是 df 与按目录遍历的 du 不一致的一个可能原因,不是唯一原因。lsof 官方 FAQ
找到对象后,先确认服务和日志轮转协议,再决定是否让服务重新打开日志。不要看到 (deleted) 就批量终止进程,更不要向陌生 /proc/PID/fd/N 路径写入来“清空”,那属于修改现场甚至破坏业务数据。
看不到不代表不存在
权限、PID/网络命名空间、挂载边界、快速退出的进程都会影响结果。容器内查不到宿主端口的持有者,可能只是看不到那个进程;容器里的端口映射也不一定对应容器进程直接监听同一个宿主端口。
全局递归 lsof +D /某个大目录 可能很重,应先限定目标 PID 或确切文件。它不是任意规模都适合高频执行的轻量查询。
最小练习:在隔离容器中运行 README 的本地 socket 示例,用 -iTCP 找到端口,再用 -a -p 确认交集结果。退出示例后重复检查,说明为什么一次截图不能证明端口长期无人使用。
ipcs:区分共享资源与遗留对象
先问使用的是哪一套 IPC
System V IPC 包含共享内存段、信号量集合、消息队列,相关接口如 shmget、semget、msgget。它们与 POSIX 的 shm_open、命名信号量、消息队列不是同一套对象。Linux System V IPC 接口
ipcs -m |
-m、-s、-q 分别选择三类 System V 对象;-p、-t 提供支持的进程/时间信息;-l 查看限制。不同对象类型能提供的字段不同,不能把共享内存表的列硬套在信号量上。util-linux ipcs 手册
以共享内存为例,先看对象 ID、所有者、权限、大小、附加数量及时间。nattch == 0 只是当时没有附加者,不证明对象已经无主;服务可能稍后重新使用,或通过其他管理流程保留它。
查看单个对象,别先想着清空
# 用前一步确认过的共享内存 ID 替换示例数值 |
IPC key 与对象 ID 不应混用,PID 也不是 IPC 对象 ID。删除这类对象属于状态变更,须确认精确 ID、所有者、用途以及所有依赖进程。这里不提供“一键清理全部共享内存”的流水线。
看到容器间 ipcs 结果不同,先检查 IPC 命名空间;它们可能本来就不共享对象。把容器改成宿主 IPC 模式是隔离边界变化,不是普通诊断命令,应单独评估。
/dev/shm 不是 ipcs 输出的目录版
Linux POSIX 共享内存通常映射到 /dev/shm 的 tmpfs 对象,System V 对象不是简单按同名普通文件列在那里的另一种显示。ipcs 为空,不能推断进程没有使用共享内存,更不能据此清空 /dev/shm。Linux POSIX 共享内存
机器人中间件可能使用共享内存传输,也可能使用 socket、匿名映射或其他机制。先查实际实现和配置,再选工具;不要因为名字里有 “shared memory” 就认定一定能用 ipcs -m 找到。
最小练习:运行实验创建的唯一 IPC 对象,记录创建前后 ID、所有者和大小,退出后检查实验自己的清理逻辑。只操作本次实验创建的对象,并确认不存在残留;不对同一命名空间内其他应用的对象做清理。
将六个工具连起来
场景一:ROS 2 节点没有继续输出
先用 ps 确认进程身份和启动参数,再用 top -H 看是少数线程忙算还是整体睡眠。若是计算热点,进入 perf;若是等待,采所有线程栈,建立业务唤醒关系。
如果栈显示在系统调用附近等待,可限时用 strace 检查实际文件、socket 或事件操作。用 lsof 将对应描述符关联到端口或文件。只有明确使用 System V IPC 时,再用 ipcs 检查对象、权限和状态。
这是一套按证据分支的流程,不是每次故障都必须把六条命令全部跑一遍。比如 CPU 忙算问题,一开始全量 strace 可能增加很多干扰,却看不到真正的算法热点。
场景二:程序不停重启
不要拿上一次的 PID 继续抓栈。每次记录启动时间、父进程和实际路径,区分程序自退出、外部监督程序重启、内存限制触发终止。资源限制线索接到下一篇 内存与 I/O 监控,动态库错误则接到 ELF 与链接。
最终留下可复核证据
- 目标身份:PID/TID、所有者、启动时间、二进制与参数。
- 观察边界:宿主或容器、命名空间、权限、采样窗口。
- 事实:状态、CPU 样本、具体栈、系统调用、对象 ID。
- 推理:这些事实支持哪个原因,还不能排除什么?
- 影响:是否暂停过目标,是否打开过高开销跟踪?
- 结果:采取什么经过授权的修复,如何复现并确认恢复?
好的工具笔记,不是记录“某条命令看起来很厉害”,而是记录它回答了哪个问题,以及它没有回答什么。