Linux 工具实战:进程、线程与系统调用排障

“程序卡住了”不是一个足够具体的结论。它可能在忙算、等锁、等网络、等磁盘,也可能已经退出,只剩父进程还没有回收的记录。真正有效的排查,是逐步回答:哪个进程?哪个线程?当前在做什么?它在等哪个对象?

这篇以 pstoppstackstracelsofipcs 六个工具建立这条证据链。工具清单的选题线索来自 Linux 工具快速教程的 pstack 入口,以下组织、实验与判断方法独立编写,命令语义以各工具上游手册为准。

系列阅读:学习地图 → 本篇 → 内存与 I/O 监控ELF 与链接传输与任务调度。需要源码级追踪时,接着看 GDB 实战

实验与观察边界

所有动画都是实验实际输出的分帧重排,不是屏幕录像,也不是伪造一段漂亮终端。进程号、地址、线程顺序及采样数值会变化;判断是否跑通,以 验证状态记录会话 为准。

在完整网站源码仓库根目录执行:

bash docker/linux-tools-lab/run.sh build
bash docker/linux-tools-lab/run.sh test
bash docker/linux-tools-lab/run.sh render
bash docker/linux-tools-lab/run.sh snapshot

具体环境、依赖和输出目录见 实验 README。这里不假定宿主 macOS 上的同名命令等同于 Linux 版本,也不把容器内观察自动解释为宿主机全貌。

实验的 tool_demo worker 提供可识别的线程、文件和本地 socket,tool_demo once 提供有限的文件访问和等待。程序由实验脚本在隔离环境构建,后文普通命令则展示迁移到自己工程时的使用方式,不要把示意 PID 当成固定产物。

本文把两类操作分开:pstoplsof 等主要读取状态;GDB 附加和 strace 则会干预调度。即使不修改业务内存,暂停与额外开销也可能影响超时、控制周期和竞态。机器人实机上先确认允许暂停的边界。

命令中的 TARGET_PID=1234 表示你已核对的目标,不是让你直接照抄 1234。检查所有者、启动时间和命令行,避免进程重启后 PID 被复用。

ps:确认到底是谁

它提供快照,不是实时曲线

先用明确的列建立进程清单,比看一屏默认输出更容易回答问题:

ps -eo pid,ppid,user,stat,etime,pcpu,pmem,rss,comm \
--sort=-pcpu

TARGET_PID=1234
ps -p "$TARGET_PID" \
-o pid,ppid,lstart,etime,stat,nlwp,pcpu,rss,args

pid 定位进程,ppid 给出父进程,lstartetime 核对这次启动;nlwp 是线程数量。comm 偏向执行名称,args 包含参数,二者不是同一种身份信息。命令行可能包含令牌或业务路径,分享前先脱敏。procps-ng 的 ps 手册

使用 ps 从进程列表缩小到指定进程并查看线程和状态

查看原尺寸 GIF · 完整会话

读这段操作时,先找目标身份,再比较状态和线程数量;不要先寻找某个“正确的 CPU 百分比”。同一条命令在空闲和繁忙机器上,本来就应该产生不同结果。

STAT 是方向提示

常见状态里,R 是运行或可运行,S 是可中断睡眠,D 是不可中断睡眠,T 是停止,Z 是僵尸。附加字母还会表示多线程、前台进程组等属性。

S 不等于异常,大多数等待工作到来的服务平时就在睡眠。D 常见于某些内核等待路径,但不能只凭一个字母断言磁盘坏了。Z 的进程执行已经结束,保留记录等待父进程回收;它不是仍在大量计算的“顽固进程”。

继续查看线程和父子关系:

ps -L -p "$TARGET_PID" \
-o pid,tid,psr,stat,pcpu,wchan:28,comm

ps -eo pid,ppid,stat,comm --forest

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 &
LAB_PID=$!
ps -p "$LAB_PID" -o pid,ppid,stat,etime,args
ps -L -p "$LAB_PID" -o pid,tid,stat,comm

说明它为什么通常低 CPU、处于睡眠,却仍是正常运行中的进程。等它自然结束后再查询,区分“查询不到”“已退出未回收”和“进程还活着”。

top:定位最近谁在忙

先固定时间窗口

top -p "$TARGET_PID"
top -H -p "$TARGET_PID"

# 批量输出,采三个刷新样本
top -b -d 1 -n 3 -p "$TARGET_PID"

-H 切到线程视角。交互界面里可用 P 按 CPU、M 按驻留内存排序,1 查看分 CPU,H 切换线程显示;具体显示和持久化配置以当前 procps-ng 版本为准。采样分析别只保留刚启动的第一屏,优先比较后续固定间隔刷新。procps-ng 的 top 手册

通过 top 的重复采样观察指定进程 CPU 与状态

查看原尺寸 GIF · 完整会话

动画教的是“比较刷新样本”,之后可用上面的线程模式继续缩小范围,不是拿演示机器的百分比作为性能基准。真实机器上短任务可能在两个样本之间已经结束,未出现在榜首不代表没有耗时。

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
command -V gstack

上游 GDB 的 gstack 是附加进程、打印各线程栈再分离的脚本。某些发行版提供兼容名称,但若本机没有它,直接使用明确的 GDB 批处理操作更可复现。GNU gstack 手册

# 只有确认本机实现和目标权限之后,才使用对应工具
pstack "$TARGET_PID"

# 不依赖 pstack 包的等价目标:采集各线程栈后分离
gdb --quiet --nx --batch \
-ex 'set pagination off' \
-ex 'thread apply all bt 12' \
-ex detach \
-p "$TARGET_PID"
采集目标进程全部线程调用栈,识别等待位置,缺少 pstack 时使用明确的 GDB 替代

查看原尺寸 GIF · 完整会话

这里展示的是采栈能力;实际用了哪个可执行文件,以会话记录为准,不能把 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 \
-e trace=%file \
-o /work/open-trace.log \
/usr/bin/cat /work/missing-example.txt

这个最小练习应先确认测试路径不存在;cat 的非零退出是实验要观察的结果,不是宣称命令执行成功。看它实际访问了哪个路径、收到什么 errno,而不是只看最末尾中文报错。

strace 过滤文件系统调用,观察实际访问路径和失败返回值来解释程序错误

查看原尺寸 GIF · 完整会话

一次系统调用可按“名称—参数—返回值—错误—时间”阅读。出现 ENOENT 通常意味着找不到某个路径元素,但程序也可能正常探测多个候选文件;EAGAIN 在非阻塞 I/O 里不必然是故障。要结合后续调用和业务终止点解释。

根据问题过滤

strace -f -tt -T -s 160 \
-e trace=connect,accept,accept4,recvfrom,sendto,poll,ppoll \
-p "$TARGET_PID"

strace -f -c -p "$TARGET_PID"

-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"
lsof -nP -iTCP:4000 -sTCP:LISTEN

# -a 表示把进程条件和网络条件取交集
lsof -nP -a -p "$TARGET_PID" -iTCP

-n 避免地址解析,-P 避免端口名称转换。-p-i 这类选择条件默认可能组合成并集;希望看“这个 PID 的 TCP”时显式加 -a,否则结果范围可能远大于预期。lsof 官方手册

lsof 将打开的文件和监听 socket 关联到所属进程与文件描述符

查看原尺寸 GIF · 完整会话

优先读 COMMANDPIDFDTYPENAMEFD 可能是数字描述符,也可能是 cwdtxtmem 等用途;不是每一行都代表一个新的普通文件描述符。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 包含共享内存段、信号量集合、消息队列,相关接口如 shmgetsemgetmsgget。它们与 POSIX 的 shm_open、命名信号量、消息队列不是同一套对象。Linux System V IPC 接口

ipcs -m
ipcs -s
ipcs -q
ipcs -m -p
ipcs -m -t
ipcs -l

-m-s-q 分别选择三类 System V 对象;-p-t 提供支持的进程/时间信息;-l 查看限制。不同对象类型能提供的字段不同,不能把共享内存表的列硬套在信号量上。util-linux ipcs 手册

ipcs 分别查看 System V 共享内存、信号量和消息队列并核对对象身份

查看原尺寸 GIF · 完整会话

以共享内存为例,先看对象 ID、所有者、权限、大小、附加数量及时间。nattch == 0 只是当时没有附加者,不证明对象已经无主;服务可能稍后重新使用,或通过其他管理流程保留它。

查看单个对象,别先想着清空

# 用前一步确认过的共享内存 ID 替换示例数值
SHM_ID=32768
ipcs -m -i "$SHM_ID"

IPC key 与对象 ID 不应混用,PID 也不是 IPC 对象 ID。删除这类对象属于状态变更,须确认精确 ID、所有者、用途以及所有依赖进程。这里不提供“一键清理全部共享内存”的流水线。

看到容器间 ipcs 结果不同,先检查 IPC 命名空间;它们可能本来就不共享对象。把容器改成宿主 IPC 模式是隔离边界变化,不是普通诊断命令,应单独评估。

/dev/shm 不是 ipcs 输出的目录版

Linux POSIX 共享内存通常映射到 /dev/shm 的 tmpfs 对象,System V 对象不是简单按同名普通文件列在那里的另一种显示。ipcs 为空,不能推断进程没有使用共享内存,更不能据此清空 /dev/shmLinux 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。
  • 推理:这些事实支持哪个原因,还不能排除什么?
  • 影响:是否暂停过目标,是否打开过高开销跟踪?
  • 结果:采取什么经过授权的修复,如何复现并确认恢复?

好的工具笔记,不是记录“某条命令看起来很厉害”,而是记录它回答了哪个问题,以及它没有回答什么。

3d打印 actor-critic adaptive sampling ai辅助设计 algorithm algorithms anymal apriltag ardupilot atlas attention automation axis-angle bang-bang belief encoder blender bode c++ cadquery calibration camera calibration camera-intrinsics chrome cmake cmakelists cnn colcon computer-vision conan control controller_manager cpp cpu d435i dagger data_struct db depth camera depth-camera design-pattern diagnostics direct collocation dots dtof economics eigen elevation map elf executor factory-pattern fcpx fiducial marker figure finance forge fourier fov freecad gae gazebo gdb geometry git gnu gru guitar hardware humanoid ibus imu interest isaac gym isaac lab isaaclab kdl laplace latent variable latex launch learning-notes legged locomotion legged robotics legged-robot legged_gym life linux linux-kernel linux-tools mac math matlab matrix memory mixture of experts mlp money motion imitation motion-control motor moveit mpc mujoco music-theory network neural mapping ocs2 ode onnx openscad operator optimal algorithm optimal-control perceptive locomotion perf performance personal-finance piano pinhole-camera pinocchio pixhawk pixhawk 6c point-cloud policy distillation ppo privileged learning profiling px4 python qgroundcontrol qos quadrotor realsense reinforcement learning representation learning reward tuning rnn robogauge robot robot parkour robotics ros ros2 ros2_control rsl_rl rtb security sensor-fusion shell signal-processing sim-to-real simulation socket soft dynamics constraints spot ssh stairs stl stm32 tcp-ip teacher policy teacher student teacher-student temporal convolution terrain reconstruction thread tools tron1 twist ubuntu uml uncertainty unitree unitree g1 urdf vae valgrind vcxsrv velocity vim web wifi wiring work workflow wsl z-transform zero-shot transfer 中文输入 交叉编译 人形机器人 依赖管理 分支管理 动力学 四旋翼 四足机器人 实验诊断 强化学习 接触动力学 数值计算 机器人
知识共享许可协议