你有没有遇到过这种情况线上服务进程明明还活着端口也通请求却全部卡住不返回。CPU 不高日志没有异常内存也不见涨就像一个人睁着眼睛但已经没有反应了。我第一次处理这种“假死”问题时试遍了常规手段最后靠 pstack 一条命令把现场拉了回来。pstack 是 Linux 上打印进程线程调用栈的命令它能在不重启、不打断服务的前提下把目标进程所有线程的栈快照一次性输出排查死锁、定位长时间阻塞非常靠谱。这篇指南先讲清楚 pstack 是什么、底层怎么工作、怎么安装、怎么看输出再用一个实际死锁案例带你走一遍完整排查流程。1. pstack 是什么为什么调试时离不开它1.1 一次线上假死引发的工具选型那是一个用 C 写的网关服务某个版本上线后用户反馈请求大面积超时。我登录服务器一看进程还在PID 也正常监听端口没断但是业务线程似乎全部卡在一个地方。当时的第一反应是上 GDB可生产环境的 GDB 交互式操作风险不小而且每次 attach 之后都要手动输入一堆命令面对几百个线程效率很低。就是在那个场景下同组的同事提醒我先别折腾 GDB试试 pstack。我执行了pstack PID几秒钟内在终端刷出了当前所有线程的调用栈很快就看到两个线程分别停在pthread_mutex_lock上而且互相拿着对方下一步要获取的锁。一瞬间就明白了这是典型的死锁。从那之后pstack 就成了我排查线上服务“假死”问题的默认第一动作。它的定位很朴素只做“线程栈快照”这一件事不修改变量、不打断流程、不搞复杂交互所以上手成本极低。更关键的是它把“现场抓取”这个动作变得足够轻轻到可以放心在生产环境用。不过pstack 也不是银弹。它适合回答“当前线程停在哪、调用链是什么”但回答不了“为什么 CPU 这么高、系统调用哪一步慢”。所以这篇指南会把 pstack 的边界也讲清楚让你知道什么时候该用它什么时候该换别的工具。1.2 pstack 的底层工作原理pstack 的核心机制是 Linux 下的 ptrace 系统调用。ptrace 是调试器的基础能力GDB 也是基于它工作的。pstack 执行的动作可以拆成下面几步遍历/proc/PID/task目录拿到进程内所有线程的 TID对每个 TID 执行ptrace(PTRACE_ATTACH)让目标线程暂时停下来读取线程的寄存器和栈内存结合二进制文件的符号表把地址还原成函数名和行号输出完成后执行PTRACE_DETACH让线程继续运行。整个过程非常快通常只有几十毫秒到几百毫秒取决于线程数量和符号表大小。这也是它敢用于生产环境的原因虽然会让进程产生一个极短的停顿但相比 GDB 手工 attach、detach 的流程pstack 的侵入性已经控制到很小。我用一个生活里的类比进程像是一个正在赶路的人pstack 是让他突然站住几秒拍一张 X 光片拍完立刻继续跑。你损失的是几秒时间拿到的是内部骨骼结构图。对于死锁、阻塞这类“静态问题”一张快照往往就足够下结论。还要强调的是权限问题。ptrace 不是随便谁都能用的普通用户只能 attach 自己启动的进程或者被明确授权的子进程要 attach 其他用户的进程必须用 root 或具备CAP_SYS_PTRACE能力。后面我会专门讲权限相关的报错怎么处理。1.3 什么时候会想到用 pstackpstack 适合的场景我整理成一张表场景为什么用 pstack更好的替代方案进程假死、请求卡住快速看线程卡在哪判断是等锁还是等 IOGDB 交互式分析但成本更高死锁直接看到两个线程互相等待锁配合 GDB 看锁持有者更精确长时间阻塞在下游调用栈顶停在 read/poll/recv 等调用strace 看系统调用耗时多线程服务偶发性卡顿连续采样对比两次栈变化配合 coredump 做离线分析Java 服务线程卡死pstack 只能打 JVM 的 C 栈不方便jstack 直接打 Java 方法栈反过来pstack 不适合做 CPU 热点分析。热点函数需要采样多次用 perf、perf top 这类工具更对口。pstack 更适合回答“现在这一刻程序内部的执行状态是什么”而不是“过去一段时间哪个函数最烧 CPU”。2. 安装与基本使用不同发行版的快速上手2.1 安装 pstack 的几种方式在主流 Linux 发行版里pstack 不一定默认安装。常见的安装方式如下# RHEL / CentOS 7需要先启用 EPEL yum install -y epel-release yum install -y pstack # CentOS 8 / Rocky Linux / AlmaLinux dnf install -y epel-release dnf install -y pstack # Ubuntu / Debian apt update apt install -y pstack如果你所在的系统源里没有 pstack 包还有一个非常实用的替代方案直接用 GDB 的批处理模式。pstack 本质上就是“打印所有线程的 backtrace”而 GDB 恰好能做这件事命令如下gdb -p PID -batch -ex thread apply all bt这条命令不需要交互执行完自动退出输出格式和 pstack 非常接近。我甚至见过某些发行版的 pstack 就是一个包装脚本内部就是调这条 GDB 命令。所以就算没有现成的 pstack你也不会卡住。也有源码编译安装的方式但没必要展开因为大多数情况下包管理器已经足够。真要编译无非是拉源码、make make install但生产环境里我一般倾向少折腾编译依赖能装二进制就用二进制。2.2 命令格式与核心参数pstack 的命令行参数少得可怜pstack PID pstack -V核心用法就是跟一个进程号。有的版本支持-v或者-a但不同发行版实现不同不需要记太多。真正要记住的是先确认 PID 正确再执行命令。执行后的输出大致长这样Thread 1 (Thread 0x7f1c5ffd6700 (LWP 14251)): #0 0x00007f1c5edf3c00 in __GI___lll_lock_wait () #1 0x00007f1c5edf56b7 in __GI___pthread_mutex_lock () #2 0x000055b3e26d0524 in thread_b () #3 0x000055b3e26d06a8 in std::thread::_Implstd::thread::_Invokerstd::tuplevoid (*)() ::_M_run () #4 0x00007f1c5ed94c8e in start_thread () #5 0x00007f1c5edc2a1c in clone ()我来拆解一下输出里的信息Thread 1是 pstack 对线程的编号不一定是 PID括号里的LWP 14251才是真正的轻量级进程 ID也就是线程在系统里的标识#0是当前执行点#1是调用者逐步往上就是整条调用链行尾如果是空括号说明没有行号信息通常是因为编译时没加调试符号函数名后面跟着的start_thread、clone这类底层函数表示线程入口和内核创建线程的调用。不要只看第一行栈的上层函数往往才是业务代码真正所在的位置。比如#0停在__lll_lock_wait代表等锁但你要看清楚#2是哪个业务函数触发的才能真正定位到问题代码。2.3 权限与安全限制提前确认实际使用 pstack很多新手第一次都卡在权限上。Linux 内核里有一个安全模块叫 Yama它通过 ptrace_scope 控制 ptrace 的使用范围。查看当前值cat /proc/sys/kernel/yama/ptrace_scope常见取值含义值含义对 pstack 的影响0无限制任何进程都能 attach 任何进程方便但安全风险高1只能 attach 子进程或者具备 CAP_SYS_PTRACE 的进程默认值root 和同用户子进程可用2仅 root 且具备 CAP_SYS_PTRACE 才可用普通用户大概率报错3内核层限制不允许任何外部修改比较少见一般不用考虑如果遇到Operation not permitted先别急按这个顺序排查确认是不是同一个用户启动的进程确认是否 root 执行查看 ptrace_scope 的值是否容器环境内缺少SYS_PTRACE能力。临时把 ptrace_scope 改成 0 可以解决权限问题但不建议在没有安全评估的情况下长期开着尤其是多用户服务器。正确做法是尽量用目标进程的用户身份去执行 pstack或者放容器时显式添加对应能力。3. 核心实操用 pstack 定位死锁与线程卡死3.1 构造一个可复现的多线程死锁 Demo理论说一百遍不如实际看一遍。这里我写一个非常简单的 C 死锁程序方便你在自己环境里验证 pstack 的威力。#include iostream #include thread #include mutex #include chrono std::mutex m1, m2; void thread_a() { std::lock_guardstd::mutex lock1(m1); std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guardstd::mutex lock2(m2); } void thread_b() { std::lock_guardstd::mutex lock2(m2); std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guardstd::mutex lock1(m1); } int main() { std::thread a(thread_a); std::thread b(thread_b); a.join(); b.join(); return 0; }thread_a 先拿 m1再等 m2thread_b 先拿 m2再等 m1。两个线程各自跑 100 毫秒后就会形成交叉等待这正是死锁最常见的形态。编译时一定要加-g否则 pstack 只能看到地址看不到函数名g -stdc11 -g demo.cpp -o demo -pthread运行后用pgrep demo找到 PID./demo pgrep demo这时程序已经死锁两个线程永远停在锁等待中非常适合拿来练手。3.2 从 pstack 输出解读死锁现场对上面这个 Demo 执行pstack PID输出类似这样Thread 2 (Thread 0x7f1c5ffd6700 (LWP 14251)): #0 0x00007f1c5edf3c00 in __GI___lll_lock_wait () #1 0x00007f1c5edf56b7 in __GI___pthread_mutex_lock () #2 0x000055b3e26d0524 in thread_b () #3 0x000055b3e26d06a8 in std::thread::_Implstd::thread::_Invokerstd::tuplevoid (*)() ::_M_run () #4 0x00007f1c5ed94c8e in start_thread () #5 0x00007f1c5edc2a1c in clone () Thread 1 (Thread 0x7f1c5cbe6640 (LWP 14250)): #0 0x00007f1c5edf3c00 in __GI___lll_lock_wait () #1 0x00007f1c5edf56b7 in __GI___pthread_mutex_lock () #2 0x000055b3e26d04f6 in thread_a () #3 0x000055b3e26d06a8 in std::thread::_Implstd::thread::_Invokerstd::tuplevoid (*)() ::_M_run () #4 0x00007f1c5ed94c8e in start_thread () #5 0x00007f1c5edc2a1c in clone ()看到这里判断死锁只需要三个步骤#0位置相同都是__lll_lock_wait说明两个线程都停在锁等待上往上看#2一个是 thread_a一个是 thread_b说明业务入口不同结合代码逻辑thread_a 持有 m1 等 m2thread_b 持有 m2 等 m1交叉等待成立。这个例子告诉我们两条经验第一pstack 输出不需要所有行都看懂关键是看栈顶和业务函数层第二死锁现场往往有对称性两个线程的栈会呈现出“互相等”的结构多线程越多越明显。3.3 除了死锁还能看出哪些问题pstack 的价值不只在死锁。我把常见的线程卡点整理成这样几种类型栈顶停在read、recv、poll、select这类调用说明线程在等网络 IO通常是下游响应慢或 socket 阻塞栈顶停在pthread_cond_wait或者cv.wait说明在等条件变量要检查是不是其他线程忘了 notify栈顶停在malloc或free可能是内存分配器锁竞争甚至多个线程在分配器内部互相等待栈顶停在nanosleep或定时等待说明线程主动休眠需要结合业务逻辑判断是否正常。区分“长时间阻塞”和“瞬间调度”有一个很实用的方法连续做两次 pstack 采样间隔 0.1 到 0.5 秒。如果两次栈顶位置一模一样那基本可以断定线程长时间停留如果每次都不一样说明线程还在运行只是采样时恰好停在某个地方。4. 实战技巧排查线上问题的完整流程4.1 快速定位问题进程的方法拿到一台“假死”服务器我的标准流程是这样的第一步用top看整体负载找到 CPU 不高的嫌疑进程。注意死锁和 IO 阻塞往往 CPU 不高所以不能只盯高 CPU。第二步用ps -T -p PID或top -H -p PID看进程内线程状态。在top -H里你会看到线程的 PID 和 CPU 使用率如果所有线程 CPU 都是 0而进程又不响应多半是卡死。第三步执行 pstack 拿到线程栈快照。如果线程很多建议先输出到文件再分析避免终端被刷屏。我常用的连续采样命令是for i in 1 2 3; do pstack $PID /tmp/stack.$i.txt sleep 0.1 done三份快照之间的差异非常关键。如果三份的栈都一样说明进程已经硬卡住如果栈在变化但业务无进展可能是某个循环在空转。对比采样文件时我用 diff 或直接看调用链中的函数名变化很快能判断问题的“动态”还是“静态”。4.2 pstack 与其它工具组合使用pstack 是很好用的点工具但排查复杂问题经常需要和其他工具配合才能把一张静态快照串成完整故事。工具定位问题与 pstack 配合方式GDB变量值、锁持有者、内存布局pstack 确定函数范围后GDB 深入检查gstack类似 pstack 的实现输出更依赖 GDB可以互相验证strace系统调用耗时和错误码pstack 看到阻塞在 readstrace 看卡在哪个 fdperf / perf topCPU 热点和采样分析pstack 看静态阻塞perf 看性能热点jstackJava 线程堆栈Java 服务优先用 jstackpstack 仅作补充举一个组合排查的例子某个服务卡住但 CPU 不高。pstack 输出显示大量线程停在recvfrom说明在等网络数据。此时再用strace -p PID -f -e tracenetwork跟踪几秒就能看到是不是某个 socket 一直没有消息返回进而联想到下游依赖超时。如果 pstack 显示栈顶停在malloc可以配合perf record做分配器采样看锁竞争来源如果是 CPU 飙高但进程偶尔不响应则不要用 pstack直接用perf top找热点函数。工具之间不是竞争关系而是互补关系。4.3 容器与高并发场景下的注意事项现在很多服务都跑在容器里pstack 的使用环境比裸机更复杂有几个坑需要注意。第一个坑是能力限制。容器默认不一定允许 ptrace即使你在容器里以 root 身份运行也可能遇到Operation not permitted。此时需要在容器定义时添加SYS_PTRACE能力或者临时提升权限否则 pstack 无法 attach。第二个坑是 PID namespace。在容器内看到的 PID和宿主机上的 PID 不一样。如果你在宿主机执行 pstack要使用宿主机的 PID如果在容器内执行要用容器内看到的 PID。不要换算错否则 attach 到的不是你想要的进程。第三个坑是 attach 的短暂停顿。高并发服务对延迟很敏感pstack 每次 attach 都会让目标进程所有线程停一下。虽然通常只有几十毫秒但在极高 QPS 下也会产生影响。我的建议是不要在流量高峰期频繁采样最好一次拿够信息或者先录几分钟数据再做离线分析。第四个坑是 Java 服务。很多人看到 Java 线程卡住下意识用 pstack结果得到一堆 JVM 底层 C 栈如GCTaskThread、VMThread根本看不到业务方法。Java 应用请优先使用 jstackpstack 只能作为 JVM 自身状态排查的补充。4.4 如何判断是否需要重启拿到 pstack 快照后最需要考虑的问题是到底要不要重启很多团队一到线上假死就直接重启省事但重启等于销毁了事故现场。后续复盘没有证据问题第二次出现还是要靠猜。我的建议是在任何重启之前至少留一份 pstack 输出。哪怕当时看不懂等恢复后也能慢慢分析。实际经验里很多看起来“像死锁”的问题并不是真的死锁而是连接池耗尽、线程池拒绝任务、下游超时导致阻塞。这些情况重启可能暂时恢复但根因没解决过几天又犯。如果 pstack 明确看到两个或多个线程在锁上交叉等待那大概率是真死锁重启是合理的止损手段但重启后必须找出加锁顺序不一致的代码。如果栈顶停在网络调用重启前最好把上下游依赖检查一遍否则重启只是治标不治本。5. 常见问题与避坑指南5.1 pstack: Operation not permitted这是最常遇到的报错。原因通常是权限不足。排查步骤# 1. 确认当前用户 whoami # 2. 确认目标进程用户 ps -o user,pid,cmd -p PID # 3. 查看 ptrace 限制 cat /proc/sys/kernel/yama/ptrace_scope如果 ptrace_scope 是 1普通用户只能 attach 自己的子进程。如果你不是该进程用户必须用 sudo 或切换用户。临时放宽限制echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope这句命令只在当前内核会话有效重启后恢复。不要图省事长期改成 0尤其多人共用的服务器安全风险很大。5.2 输出是空的或者看不到函数名pstack 返回了内容但只有地址没有函数名这种情况也很常见。根本原因是目标二进制没有符号表。编译时加上-g可以保留调试符号。发布版本通常还会加-fno-omit-frame-pointer保留帧指针否则优化后栈回溯信息会丢失。如果线上二进制已经 strip 过你还可以用同版本的带符号二进制手动将 pstack 得到的地址映射到符号addr2line -e /path/to/binary -f 0x000055b3e26d0524addr2line可以把地址解析成函数名和文件行号。前提是你手头有相同编译产物的带符号文件否则价值有限。看到大量??的时候还有一个可能是 PIE位置无关可执行文件加载基地址变化导致地址无法直接解析。这种情况建议换 GDB 连上去用 GDB 自动处理重定位比手动换算靠谱。5.3 为什么 pstack 在一些新版系统上是脚本有些发行版没有提供独立的 pstack 二进制而是用一个脚本调用 GDB 来实现。这解释了为什么有些机器装了 pstack 包执行时却报错说找不到 GDB。遇到这种情况顺带安装 GDB 就能解决或者直接用我前面写的 GDB 批处理命令gdb -p PID -batch -ex thread apply all bt没有必要纠结到底是脚本还是二进制输出结果和排查思路完全一致。但你要知道这层关系不然在精简镜像里很容易被 “No such file or directory” 这类诡异报错卡住。5.4 现场已经变了怎么保存证据链排查事故的时候尤其是处理完问题准备复盘最怕的是现场信息不全。我希望你把 pstack 输出当成事故现场的第一份证据而不仅仅是临时看一眼的东西。推荐用这个组合命令保存现场date ps -ef | grep $PID cat /proc/$PID/status pstack $PID $(date %Y%m%d_%H%M%S)_stack.txt采样时间、进程状态、线程数量、完整栈输出都在一个时间点记录下来后续分析问题、写事故报告都够用。如果服务是多线程重灾区pstack 输出可能上万行直接丢终端完全没有可读性一定要重定向到文件再处理。另外连续采样时文件名里的时间戳要精确到秒这样对比多次采样时才能区分先后顺序避免把不同时刻的栈当成同一时刻来推断。这篇先写到这里。我自己的使用习惯是不管什么服务假死先别急着重启先连续抓两份 pstack哪怕暂时看不懂也先把证据拿住。很多线上事故其实并不需要重启可能只是下游超时、线程池打满、锁顺序写错盲目重启只会把最关键的现场毁掉。pstack 的成本低、风险可控是每个后端工程师都应该熟练掌握的工具。下一篇我会接着聊如何结合 addr2line 和 GDB 做更深入的栈分析以及在大型多线程服务里怎么过滤噪音快速从几千个线程里找到真正被卡住的那一个。