1. 从一次线上卡顿说起:pstack 到底能帮你看到什么
线上服务跑着跑着突然响应变慢,CPU 不高、内存正常、日志也没报错,重启一下又好了——这种“幽灵问题”大概是每个后端和运维同学都遇到过的噩梦。我第一次碰到这类问题时,排查手段基本就是看日志、看监控、加打印,折腾半天往往还是抓不到现场。后来一位做系统底层的朋友甩给我一条命令:pstack <pid>,几秒钟之后,一个完整的线程调用栈就打印在终端里,问题瞬间现形——某个线程卡在一把锁上,另一个线程在等网络 IO,主线程在等这个子线程返回。那一刻我才真正意识到,pstack 这类栈回溯工具的价值,不在于它能“修”问题,而在于它能让你在问题发生的当下,看清进程内部每个线程到底在干什么。
这篇内容我想系统地把 pstack 讲透。它是什么、能做什么、适合谁用,我先用一句话说清楚:pstack 是一个用来打印指定进程当前所有线程调用栈的命令行工具,主要服务于 C/C++ 程序、系统服务、后台守护进程的线上问题排查,尤其是死锁、卡顿、CPU 飙高、进程假死这类“看不到日志”的场景。它特别适合后端开发、运维工程师、SRE、以及任何需要跟 Linux 进程打交道的技术人员。哪怕你平时写的是 Java、Go、Python,只要你的服务底层跑在 Linux 上、依赖了 native 库,pstack 一样能派上用场。
不过要提前说明一点:pstack 本身并不是一个“独立完整”的工具,它更像是一个封装脚本,底层依赖的是 gdb(GNU 调试器)或者系统自带的栈回溯能力。不同发行版上,pstack这个名字背后指向的实现可能完全不同,这也是很多人第一次用 pstack 会踩坑的根本原因。所以这篇“完整指南(上)”,我会先把原理、环境、基础用法和核心排查场景讲清楚,把那些文档里不会写的坑一次性摊开。
2. 先搞懂原理:pstack 为什么能“看穿”一个进程
2.1 pstack 的本质是一个封装脚本
很多人以为 pstack 是一个编译好的二进制程序,其实不是。在绝大多数 Linux 发行版上,你which pstack看到的往往是一个 shell 脚本,内容大致是去调用 gdb,然后执行thread apply all bt这类命令。也就是说,pstack 的能力上限,取决于它背后调用的调试器能力。
这就解释了一个常见现象:为什么有的机器上 pstack 输出很完整,有的机器上却报错或者输出残缺。因为不同环境下,脚本调用的后端不一样。有的发行版把它指向 gdb,有的指向 eu-stack(elfutils 套件里的工具),还有的干脆没有预装,需要你自己装。理解这一点非常关键,后面所有的排查技巧都建立在这个认知之上。
从原理层面讲,一个进程在运行时,每个线程都有自己的调用栈,栈里保存着函数返回地址、局部变量、参数等信息。pstack 做的事情,就是通过调试接口(比如 ptrace 系统调用)attach 到目标进程,读取每个线程的栈内存,再结合可执行文件和动态库里的符号信息,把一串内存地址还原成人类可读的函数调用链。这就是为什么你看到的输出是一层一层的函数名,而不是一堆十六进制地址。
2.2 符号信息:决定输出可读性的关键
这里必须重点讲一个概念:符号(symbol)。pstack 输出的可读性,几乎完全取决于目标进程有没有带符号信息。如果程序编译时加了-g选项,或者动态库里保留了符号表,那么 pstack 就能把地址翻译成func_a -> func_b -> func_c这样的函数名;反之,你看到的可能就是一堆??或者纯地址。
我见过太多人第一次用 pstack,输出全是问号,然后就说“这工具没用”。其实问题不在工具,而在于发布版本把符号剥掉了(strip)。生产环境为了减小体积、保护代码,通常会 strip 掉符号,这时候 pstack 的输出就会大打折扣。解决办法有两个:一是保留一份带符号的二进制文件,排查时用gdb手动加载符号;二是编译时保留一份独立的符号文件(debuginfo),需要时再关联上。这个细节后面在实操部分会详细展开。
2.3 pstack 与其他栈工具的定位差异
市面上能看调用栈的工具不止 pstack 一个,常见的有 gdb、eu-stack、perf、以及各种语言自带的诊断工具。它们各有侧重,我简单做个对比,方便你选型:
| 工具 | 核心特点 | 适用场景 | 主要限制 |
|---|---|---|---|
| pstack | 命令简单,一键打印所有线程栈 | 快速定位卡顿、死锁 | 依赖后端调试器,符号缺失时输出差 |
| gdb | 功能最全,可交互调试 | 深度分析、单步调试 | 上手门槛高,attach 有停顿 |
| eu-stack | 轻量,对进程侵入小 | 高频采样、生产环境 | 需要额外安装 elfutils |
| perf | 性能采样,开销低 | CPU 热点分析 | 偏性能剖析,非栈快照 |
从这张表能看出来,pstack 的定位是“快速、直观、低门槛”。它不追求功能全面,而是让你在几分钟内对进程状态有个整体判断。真正复杂的问题,还是要靠 gdb 深入。所以我的建议是:pstack 作为第一响应工具,快速判断问题方向;确认方向后再用 gdb 精确定位。
3. 环境准备:别急着敲命令,先把地基打牢
3.1 确认你的 pstack 到底指向什么
前面说了,pstack 是个封装脚本,所以第一步不是急着用,而是先搞清楚它背后是什么。执行下面这条命令:
which pstack file $(which pstack) cat $(which pstack)如果file显示它是 shell script,那就cat出来看看它调用了谁。常见的几种情况:调用gdb、调用eu-stack、或者调用一个叫pstack的二进制。搞清楚这一点,后面出问题才知道往哪个方向查。
如果系统里根本没有 pstack,也不用慌。它本质上就是一条 gdb 命令的封装,你完全可以自己定义一个别名:
alias pstack='gdb -q -nx -batch -ex "thread apply all bt" -p'这样用的时候直接pstack <pid>就行。注意这里的几个参数:-q安静模式、-nx不加载初始化文件、-batch批处理模式执行完就退出、-ex执行指定命令。这套参数组合是我实测下来最稳的,避免了 gdb 加载一堆配置导致的干扰。
3.2 安装必要的依赖
如果机器上连 gdb 都没有,那就得先装。不同发行版的包管理命令不一样,我列一下常见的:
# Debian/Ubuntu 系 apt-get install -y gdb # RHEL/CentOS 系 yum install -y gdb # 如果想用 eu-stack 方案 apt-get install -y elfutils # 或 yum install -y elfutils这里有个经验:生产环境装 gdb 要谨慎。gdb 体积不小,而且 attach 进程时会让目标进程短暂停顿(stop the world)。对于延迟敏感的服务,这个停顿可能是致命的。所以如果只是偶尔排查,装一个没问题;如果要做高频采样,建议用 eu-stack,它对进程的侵入更小。
3.3 权限问题:为什么你 attach 不上
新手最常遇到的报错就是ptrace: Operation not permitted或者Could not attach to process。这几乎都是权限问题。Linux 出于安全考虑,默认限制了普通用户 attach 其他用户的进程。解决办法有几个:
- 用 root 或 sudo 执行(最简单,但生产环境要谨慎)
- 调整
/proc/sys/kernel/yama/ptrace_scope的值(0 表示允许同用户 attach) - 给目标进程和调试工具配置相同的用户权限
我个人的习惯是:排查时用 sudo,但排查完立刻退出,不做长期提权。另外要注意,有些容器环境(比如某些精简的容器镜像)默认禁用了 ptrace,这种情况下 pstack 是没法用的,需要重新配置容器的安全策略。
提示:attach 一个进程时,目标进程会被暂停。对于高并发服务,这个暂停可能导致请求超时。所以尽量在低峰期操作,或者用 eu-stack 这类侵入更小的工具。
4. 基础用法与输出解读:把一堆栈信息读成故事
4.1 最基础的几条命令
pstack 的用法简单到令人发指,核心就一条:
pstack <pid>执行之后,它会打印出该进程所有线程的调用栈。输出格式大致是这样:
Thread 3 (Thread 0x7f8a1c2d3700 (LWP 12345)): #0 0x00007f8a2b3c4e5d in poll () from /lib64/libc.so.6 #1 0x0000000000412a3b in wait_for_event () #2 0x0000000000413c2d in worker_loop () #3 0x00007f8a2b5a1e25 in start_thread () from /lib64/libpthread.so.0 #4 0x00007f8a2b2d3bad in clone () from /lib64/libc.so.6每一段以Thread开头,后面跟着线程 ID 和 LWP(轻量级进程号)。下面的#0 #1 #2...就是调用栈,#0是当前正在执行的函数,越往下越是调用它的上层函数。读栈的顺序是从下往上:最底下是线程入口,最上面是当前卡住的位置。
如果只想看某个特定线程,可以先用ps -T -p <pid>找到线程 ID,然后针对性分析。不过大多数时候,直接看全部线程反而更容易发现规律——比如一堆线程都卡在同一个函数上,那基本就是那里有问题。
4.2 读懂栈顶:问题往往就在 #0
排查问题的核心技巧,就是看每个线程的#0帧。#0代表线程当前正在执行的代码位置,它直接告诉你这个线程此刻在干什么。常见的几种情况:
#0在poll、epoll_wait、select:线程在等 IO,通常是正常的空闲等待#0在pthread_mutex_lock、futex_wait:线程在等锁,如果大量线程都卡在这,很可能有死锁或锁竞争#0在read、write、recv:线程在做 IO 操作,如果长时间卡住,可能是对端慢或网络问题#0在某个业务函数里:线程在跑业务逻辑,如果 CPU 高,这里就是热点
我一般会先扫一遍所有线程的#0,把状态分类统计一下。如果发现 90% 的线程都卡在等锁,那问题方向就很明确了。这种“先看全局再看局部”的思路,比一上来就盯着某个线程看效率高得多。
4.3 一个真实的排查案例
说个我印象深刻的例子。某次一个后台服务响应时间突然从 50ms 涨到 3s,监控显示 CPU 和内存都正常。我上去先pstack了一把,输出里发现有个线程的栈是这样的:
Thread 8 (LWP 23456): #0 __lll_lock_wait () #1 pthread_mutex_lock () #2 update_shared_cache () #3 handle_request () ...而另一个线程的栈是:
Thread 12 (LWP 23460): #0 do_slow_io () #1 refresh_cache () #2 update_shared_cache () #3 handle_request () ...看出来了吗?线程 12 持有锁去做慢 IO,线程 8 在等这把锁。锁的粒度太粗,把 IO 操作包在了临界区里,导致所有请求都被拖慢。这个问题光看日志根本发现不了,因为日志里只有“请求处理中”,没有锁的信息。pstack 一出手,问题直接现形。后来把 IO 操作挪到锁外面,响应时间立刻恢复正常。
这个案例说明一个道理:pstack 最大的价值是让“不可见”的线程状态变得“可见”。日志记录的是业务逻辑的输入输出,而 pstack 记录的是线程在操作系统层面的真实状态,两者互补,缺一不可。
5. 核心排查场景:死锁、卡顿、CPU 飙高怎么破
5.1 死锁排查:找那个“环”
死锁是 pstack 最经典的应用场景。死锁的本质是两个或多个线程互相等待对方持有的锁,形成循环等待。用 pstack 排查死锁,核心就是找这个“环”。
具体做法:连续执行几次pstack <pid>,间隔几秒。如果每次输出都一样,说明进程状态是“冻结”的,基本可以确定是死锁或长时间阻塞。然后重点看那些卡在pthread_mutex_lock、futex_wait上的线程,顺着它们的栈往上找,看它们各自在等什么锁、持有什么锁。
不过要提醒一句:pstack 只能告诉你线程卡在哪,不能直接告诉你锁被谁持有。要精确定位锁的归属,还得靠 gdb 进一步分析,或者结合代码审查。但 pstack 能帮你快速缩小范围——如果发现线程 A 卡在锁 X 上,线程 B 卡在锁 Y 上,而代码里 X 和 Y 有交叉持有的可能,那死锁的嫌疑就很大了。
注意:排查死锁时,建议至少采样 3 次,间隔 5 秒以上。如果栈完全不变,才是真死锁;如果栈在变,那可能只是慢,不是死。
5.2 卡顿排查:区分“等 IO”和“等锁”
服务卡顿但没死,这种情况比死锁更常见,也更难查。pstack 在这里的作用是帮你区分卡顿的类型。我通常按下面的流程走:
- 先
pstack一次,看所有线程的#0分布 - 如果大量线程卡在
poll/epoll_wait,说明在等 IO,去看下游依赖是不是慢了 - 如果大量线程卡在
futex_wait,说明在等锁,去看是不是有热点锁竞争 - 如果线程都在正常跑业务函数,但响应就是慢,那可能是 CPU 被抢占或单次处理逻辑太重
这个分类过程非常关键,因为它直接决定了你下一步往哪个方向查。我见过有人一看到卡顿就去查数据库,结果发现是本地锁竞争,白折腾半天。先用 pstack 定性,再针对性定量,这是效率最高的排查路径。
5.3 CPU 飙高排查:找到那个“忙”线程
CPU 飙高和卡顿正好相反,卡顿是线程在“等”,CPU 高是线程在“忙”。排查 CPU 高,pstack 要配合top -H一起用。流程是这样的:
# 第一步:找到 CPU 最高的线程 top -H -p <pid> # 第二步:把线程 ID 转成十六进制(pstack 输出里是十六进制) printf "%x\n" <tid> # 第三步:pstack 看这个线程在干什么 pstack <pid> | grep -A 20 "<十六进制tid>"这样就能精确定位到是哪个线程在烧 CPU,以及它在执行什么函数。如果发现某个线程一直在某个循环里打转,那基本就是死循环或者算法效率问题。我实测下来,这套组合拳对付 CPU 飙高问题,命中率非常高。
不过有个细节要注意:pstack 采样是瞬时的,可能刚好采到线程在干别的事。所以对于 CPU 高的问题,建议多采几次,看哪个函数反复出现,那才是真正的热点。
5.4 进程假死排查:看主线程在等谁
进程假死(不响应信号、不接受请求)是另一种常见故障。这时候 pstack 的重点是看主线程的栈。主线程通常负责接收请求、分发任务,如果它卡住了,整个进程就“假死”了。
我遇到过一次,主线程卡在waitpid上,等一个子进程退出,但那个子进程自己又卡住了,导致主线程永远等下去。这种问题用 pstack 一看主线程栈就明白了。所以排查假死,第一件事就是找到主线程(通常是 PID 等于进程 ID 的那个线程),看它的#0在哪。
6. 常见问题与避坑指南:那些文档不会告诉你的事
6.1 输出全是问号怎么办
这是最高频的问题。原因前面讲过,就是符号缺失。解决办法分几种情况:
- 如果是自己编译的程序,重新编译时加
-g,并且不要 strip - 如果是第三方库,安装对应的 debuginfo 包(比如
glibc-debuginfo) - 如果生产环境不能带符号,就保留一份带符号的版本,排查时用 gdb 手动
symbol-file加载
我个人的做法是:构建流水线里同时产出两个版本,一个 strip 过的用于发布,一个带符号的存档用于排查。这样既不增加生产体积,又保证了排查能力。这个习惯帮我省了无数次事。
6.2 attach 导致服务停顿怎么破
前面提过,gdb attach 会暂停进程。对于延迟敏感的服务,这个停顿可能造成请求超时。缓解办法有几个:
- 用 eu-stack 替代 gdb,它的侵入性更小
- 在低峰期操作,或者先摘除流量再排查
- 控制采样频率,不要连续高频 attach
如果服务对延迟极其敏感,我建议提前在测试环境复现问题,而不是直接在生产上 attach。生产环境排查,优先用对进程影响小的工具。
6.3 容器环境下的特殊处理
容器里用 pstack 有几个坑。第一,很多精简镜像没装 gdb,需要自己加。第二,容器默认的 ptrace 权限可能被限制,需要调整安全配置。第三,容器里的 PID 和宿主机不一样,排查时要注意区分。
我的经验是:如果要在容器里排查,最好用带调试工具的镜像,或者临时起一个 debug 容器,通过共享进程命名空间的方式去 attach。这样既不污染生产镜像,又能获得完整的排查能力。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方向 |
|---|---|---|
输出全是?? | 符号被 strip | 加载 debuginfo 或带符号版本 |
| attach 报权限错误 | ptrace 限制 | 用 sudo 或调整 ptrace_scope |
| 输出为空 | 后端工具缺失 | 检查 pstack 脚本调用了什么 |
| 进程卡住不动 | 可能死锁 | 多次采样对比栈是否变化 |
| 采样后服务超时 | attach 停顿 | 改用 eu-stack 或低峰操作 |
| 容器内无法使用 | ptrace 被禁 | 调整容器安全策略 |
这张表是我这些年踩坑总结出来的,基本覆盖了 90% 的常见问题。遇到问题先查表,能省不少时间。
7. 几个让我印象深刻的实操心得
先说一个关于采样时机的经验。很多人排查问题喜欢“事后”去 pstack,但那时候进程状态可能已经变了。最好的时机是问题正在发生时,哪怕服务已经卡了,只要进程还在,pstack 就能抓到现场。所以我的建议是:监控告警一响,第一时间上去 pstack,别等重启之后再查,那时候什么都晚了。
再说一个关于栈对比的技巧。单次 pstack 只能看到“此刻”,但多次采样的对比才能看出“趋势”。比如你每隔 3 秒采一次,采 5 次,如果某个线程的栈一直不变,那它就是在阻塞;如果栈在缓慢变化,那可能只是处理慢。这个“动态对比”的思路,比单看一次快照有用得多。
最后分享一个关于符号管理的小技巧。我习惯在构建时把符号文件单独存一份,命名带上版本号和构建时间,放到一个统一的归档目录。排查时只要知道服务版本,就能立刻找到对应的符号文件。这个习惯看起来麻烦,但真到排查紧急问题时,能帮你节省大量时间。毕竟线上问题争分夺秒,谁也不想在找符号文件上浪费时间。
pstack 这个工具,说简单也简单,一条命令的事;说深也深,背后涉及调试器原理、符号管理、进程状态分析一大堆东西。这篇“上篇”先把基础打牢,把原理、环境、用法和常见场景讲清楚。下一篇我会继续深入,聊聊怎么结合 gdb 做更精细的分析、怎么处理多线程复杂场景、以及怎么把 pstack 集成到自动化排查流程里。如果你现在就想上手,建议先在自己的测试环境里跑几个进程,手动 pstack 几次,把输出格式和读栈顺序练熟,这比看再多文档都管用。