pstack实战:Linux进程卡死与死锁的线程栈定位技巧

发布时间:2026/10/21 4:33:28
pstack实战:Linux进程卡死与死锁的线程栈定位技巧 凌晨两点线上告警订单处理服务卡住。登录服务器一看进程还在CPU也还正常但业务就是不走。ps只能看到进程活着top也只能看到它占了多少资源谁都说不清它到底卡在哪儿。这种时候我在CentOS 7.6环境里的第一反应就是pstack一行命令把进程所有线程的调用栈全部拉出来三四秒就能判断出它是卡在锁上、等着IO还是陷在某个死循环里。这篇文章就把pstack从安装、原理到实战场景讲透运维、后端开发以及所有跟Linux进程打过交道的人都应该把这手基本功练熟。1. pstack到底能干什么核心价值与适用场景1.1 一个命令就把线程栈全拉出来pstack的名字很好理解print stack打印堆栈。它做的事情是让正在运行的进程暂时停下来把里面每个线程的函数调用链也就是调用栈/堆栈打印出来然后让进程继续跑。所谓“函数调用链”就像你一路敲门走进一栋楼从最外层的大门main()到楼道里的processOrder()再到具体某个房间saveToDB()每一步从头到尾的路径都会被打出来。这个过程在CentOS 7.6上实现得非常轻量不需要预埋探针不需要重启进程也不需要业务代码配合只要目标进程在跑、你有权限attach它就能抓。相比gdb attach那种重量级交互式调试pstack是非交互式的执行完就结束非常适合线上环境快速看一眼。这里要提醒一个容易混淆的点在CentOS 7.6里pstack并不是一个独立维护的小工具它本质上是gdb的包装脚本。理解这一点很重要后面遇到权限报错、依赖缺失、输出格式变化时你才知道该往哪个方向排查。1.2 哪些场景非它不可pstack几乎就是为“进程看起来活着但不动了”这种诡异状态准备的。我日常遇到最多的是下面几类进程卡死或无响应业务方说接口超时进程还在但请求堆积不处理。pstack能看出线程是不是都堵在某个锁上。死锁多个线程互相等对方持有的锁整个进程原地转圈。pstack一抓两个线程都卡在pthread_mutex_lock锁地址直接暴露问题代码。CPU异常飙高配合top -Hp先定位线程号再用pstack看这个线程正在执行哪个函数能快速排除死循环、自旋锁、GC线程等问题。IO或网络等待线程卡在read、poll、epoll_wait这类系统调用上栈能看出它是在读文件、等socket还是等NFS响应。还有一个常见用途是排查Java进程的JVM层问题。pstack虽然看不懂Java业务方法但能看到JVM的native栈比如libjvm.so里GC线程、编译线程在做什么。这种时候配合jstack一起看往往比单独用任何一个都要快。2. 开始动手安装检查、权限与第一次抓栈2.1 检查与安装CentOS 7.6上pstack藏在哪CentOS 7.6最小化安装通常不带pstack所以先确认which pstack如果提示no pstack in ...直接安装gdb包。因为CentOS 7.6软件源里的pstack命令大多数情况是随gdb一起分发的有些发行版会提供独立pstack包但在这个版本下我建议直接装gdb反正后面排查复杂问题时也用得上。yum install -y gdb装完再执行which pstack正常情况下会输出类似/usr/bin/pstack的路径。如果源里没有也可以试试yum search pstack实在不行就退一步直接用gdb的批处理模式替代效果完全一样gdb -batch -p PID -ex thread apply all bt2.2 基础用法抓一次栈需要几步pstack的用法简单到不像生产工具只有一个参数进程PIDpstack PID我们来实际试一次。启动一个在后台睡觉的进程sleep 300 echo $!假设输出的进程号是5678然后执行pstack 5678输出会类似这样Thread 1 (Thread 0x7f1c39720740 (LWP 5678)): #0 0x00007f1c392e8f10 in nanosleep () from /lib64/libc.so.6 #1 0x00007f1c392e8dc0 in sleep () from /lib64/libc.so.6 #2 0x00007f1c39528f92 in main () from /usr/bin/sleep每一行#0、#1、#2代表一层栈帧#0在最底部是当前正在执行的函数后面的编号越往上越接近调用源头。第一行Thread 1 (Thread 0x... (LWP 5678))表示这个进程里第1个线程括号里的十六进制数是线程在内核里的标识。多线程程序会打印多段这样的内容。线上环境栈通常很长动辄几百行我建议先落盘再分析pstack 5678 /tmp/stack_5678_$(date %Y%m%d_%H%M%S).txt不然终端刷屏滚动你连看的机会都没有。2.3 权限限制为什么普通用户有时会失败pstack抓栈依赖ptrace系统调用而Linux对ptrace有权限控制。简单说普通用户只能attach自己启动的进程用root可以attach所有用户进程。如果你不是目标进程的属主或者不是root执行时大概率遇到pstack: 5678: Operation not permitted另外CentOS 7.6的/proc/sys/kernel/yama/ptrace_scope默认值通常是1它限制了普通用户只能attach自己的子进程。即使你俩都是同一个用户如果目标进程不是你的子进程也可能失败。解决方式有三种用sudo执行线上推荐权限可审计。切换到目标进程的属主用户再执行。临时把ptrace_scope改成0echo 0 /proc/sys/kernel/yama/ptrace_scope。这个改动重启失效而且会放宽整个系统的ptrace限制生产环境不建议随便动特别是多租户机器上。我自己的习惯是查看别人账号的进程一律sudo pstack PID查看自己服务则直接执行尽量不做全局sysctl修改。3. 三个高频排查场景实战死锁、CPU飙高与IO卡死3.1 场景一卡死与死锁定位死锁是线上最磨人的问题之一两个线程各自持有一把锁又都在等对方那把锁结果谁都走不下去。业务表现就是进程在、线程在、请求超时。我们用一段极简代码模拟#include pthread.h #include unistd.h #include stdio.h pthread_mutex_t lock_a PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t lock_b PTHREAD_MUTEX_INITIALIZER; void *worker1(void *arg) { for (;;) { pthread_mutex_lock(lock_a); sleep(1); pthread_mutex_lock(lock_b); pthread_mutex_unlock(lock_b); pthread_mutex_unlock(lock_a); } } void *worker2(void *arg) { for (;;) { pthread_mutex_lock(lock_b); sleep(1); pthread_mutex_lock(lock_a); pthread_mutex_unlock(lock_a); pthread_mutex_unlock(lock_b); } } int main() { pthread_t t1, t2; pthread_create(t1, NULL, worker1, NULL); pthread_create(t2, NULL, worker2, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); return 0; }编译加运行gcc -o deadlock_demo deadlock_demo.c -lpthread ./deadlock_demo echo $!假设进程号是9100执行pstack 9100关键输出是这样的Thread 2 (Thread 0x7f2c85e32700 (LWP 9102)): #0 0x00007f2c85a7a32d in __lll_lock_wait () from /lib64/libpthread.so.0 #1 0x00007f2c85a75d80 in pthread_mutex_lock () from /lib64/libpthread.so.0 #2 0x0000000000400708 in worker1 () at deadlock_demo.c:12 #3 0x00007f2c85a75ea5 in start_thread () from /lib64/libpthread.so.0 Thread 1 (Thread 0x7f2c86064740 (LWP 9100)): #0 0x00007f2c85a7a32d in __lll_lock_wait () from /lib64/libpthread.so.0 #1 0x00007f2c85a75d80 in pthread_mutex_lock () from /lib64/libpthread.so.0 #2 0x000000000040067b in worker2 () at deadlock_demo.c:21 #3 0x00007f2c85a75ea5 in start_thread () from /lib64/libpthread.so.0两个线程分别卡死在自己的pthread_mutex_lock调用上worker1在deadlock_demo.c:12等待lock_bworker2在deadlock_demo.c:21等待lock_a。这基本实锤了互等锁的死锁现场。再往上翻worker1里已经持有lock_aworker2已经持有lock_b问题代码就锁定了。真实业务的栈不会这么干净函数层级深、日志夹杂但思路一样先找所有卡在pthread_mutex_lock或futex的线程再用#2以后那些函数名和行号对应到代码仓库搜索。3.2 场景二CPU飙高的线程级定位CPU飙高要先定位到具体的线程再看这个线程在跑什么函数。我一般按这套流程走top先确认是哪个进程的CPU高记下进程PIDtop用top -Hp PID看这个进程内部哪个线程在吃CPU记下线程号TIDtop -Hp 9100把TID转成十六进制pstack输出的线程标识就是十六进制printf 0x%x\n 9102抓整个进程的栈然后根据十六进制线程ID找到对应段落pstack 9100 /tmp/stack_9100.txt grep -A 20 LWP 9102 /tmp/stack_9100.txt如果线程正卡在一个没有阻塞操作的函数里或者在一个循环里反复进出某函数那就是热点所在。比如你看到一个线程频繁出现在某个字符串拼接、日志打印、加解密函数里配合perf top可以进一步看采样热点。这里有个经验pstack只抓瞬时的栈如果CPU高是间歇性的多抓几次隔几秒抓一次对比栈的变化才能区分是稳定热点还是偶发抖动。3.3 场景三IO或网络等待的确认有时候进程没死锁CPU也不高但请求就是慢。这种多半是卡在IO或者网络等待上。pstack的输出里如果出现这些函数名基本可以往对应方向查read、pread64、__read_nocancel可能在读普通文件、管道、socket需要结合打开的fd确认。poll、ppoll、select、epoll_wait处于事件等待状态。注意epoll_wait很多时候是正常空闲等待不代表有问题要结合业务状态判断。recvfrom、sendto、connect、accept网络相关。futex可能是锁等待也可能是条件变量等待需要结合上一层函数判断。只看pstack不够通常我会接着用strace确认系统调用strace -p 9100 -f -e tracenetwork,read,write -tt -T比如栈里看到read但不知道它在读哪个文件描述符strace会告诉你它正在read(17, ...)再用ls -l /proc/9100/fd/17就能看到这个fd指向什么。这种逐层下钻的思路比单看任何一个工具的输出都有效。4. 原理与进阶pstack是怎么实现的为什么它能抓到栈4.1 ptracepstack抓栈背后的系统调用pstack能暂停进程、读取寄存器、遍历线程、读取内存底层靠的是ptrace系统调用。这是Linux调试体系的基石gdb、strace、gdbus等工具都建立在它之上。ptrace最核心的能力是允许一个进程tracer观察和控制另一个进程tracee。整个抓栈过程大致是这样tracer调用ptrace(PTRACE_ATTACH)附加到目标进程。内核会向目标进程发送SIGSTOP信号让目标进程停下来。tracer读取/proc/PID/task/目录找出进程里所有线程ID。对每个线程tracer通过PTRACE_GETREGS拿到寄存器状态其中就包括栈指针rsp和帧指针rbp。结合寄存器里的地址信息tracer读取目标进程内存里的栈数据一步一步回溯出每个栈帧。拿到返回地址后tracer再查符号表把地址翻译成函数名和行号。全部打印完毕tracer调用ptrace(PTRACE_DETACH)让进程继续运行。整个过程对目标进程来说就像被打了个岔暂停了很短的时间然后恢复执行。所以pstack本身对线上进程的影响通常非常小但并不是零。某些对暂停特别敏感的程序比如严格依赖实时性的场景使用前还是要有心理准备。4.2 拆开脚本pstack其实就是gdb的一行批处理在CentOS 7.6上pstack是个shell脚本。你可以直接打开看cat /usr/bin/pstack简化后的核心内容大致是gdb -q -batch -nx -p $1 -ex thread apply all bt拆解一下参数-q安静模式不打印gdb的版本信息。-batch批处理模式执行完命令直接退出不需要交互。-nx不加载初始化文件.gdbinit避免用户自定义命令干扰。-p $1attach到第一个参数指定的进程。-ex thread apply all bt对所有线程执行btbacktrace即打印调用栈。明白这一点之后你就会发现pstack只是简化版gdb命令。如果你希望看到更多细节比如每个栈帧上的局部变量值完全可以直接用gdb实现gdb -batch -p PID -ex thread apply all bt fullbt full会额外打印局部变量和参数但输出量大很多排查深层问题时再用。普通情况下pstack默认的bt已经足够。4.3 符号表与编译优化对栈回溯的影响pstack把地址翻译成函数名靠的是目标进程里的符号表。如果二进制文件被strip过符号表被删掉pstack的输出就会变成#0 0x00007f2c85a7a32d in ?? () #1 0x00007f2c85a75d80 in ?? ()这种输出基本没法用。所以排查问题前先用file看一下file /path/to/binary输出里有not stripped说明符号还在有stripped说明符号被删了。生产环境里很多部署包为了减小体积、防止逆向会strip二进制这在排查问题时非常痛苦。另外编译优化级别也会影响栈回溯质量。gcc开-O2以上优化时可能省略帧指针内联函数也会导致栈上出现或缺少某些帧。最稳妥的做法构建测试或排查用的二进制时保留符号或者至少保留一个带debug info的副本如果条件允许编译时加-fno-omit-frame-pointer能显著提升回溯准确性。4.4 工具对比什么时候用pstack什么时候换别的工具核心能力优点局限典型场景pstack打印全线程栈快、非交互、一条命令依赖gdb看不到变量值快速判断卡顿、死锁、热点gdb全功能调试器可修改变量、断点、内存分析交互操作成本高线上慎用事后详细分析、crash现场jstack打印Java线程栈能看到Java业务方法只能用于Java进程看不到native层细节Java服务卡顿、线程状态分析strace跟踪系统调用看清进程在做什么syscall高频进程下开销较大IO、网络、权限问题追踪perf采样性能分析热点函数采样、火焰图需要足够的权限和一定的学习成本CPU性能瓶颈、火焰图分析eu-stack打印进程栈elfutils自带不依赖gdbCentOS 7.6不一定会装要额外安装gdb不可用时的备选简单归纳一下我的选择逻辑快速看栈首选pstack想看系统调用选strace想看热点分布选perf想深入调试选gdbJava进程配合jstack但不能只依赖jstack因为JVM自身的问题还是要看native栈。5. 常见问题与避坑清单5.1 高频报错的排查思路报错或现象原因处理方式No such file or directory目标进程不存在或者PID写错先ps -efOperation not permitted权限不足非属主用户或ptrace受限用sudo pstack或切换成进程属主执行pstack: 9100: 12345类似奇怪的输出某些低版本gdb对不存在的PID输出不一致检查进程是否已退出重新确认输出全是??二进制被strip缺符号表换带符号的二进制副本或用gdb配合debuginfo包抓完栈后进程状态异常极少数程序对暂停敏感确认程序是否依赖强实时性操作前评估影响有一类比较隐蔽的情况你要抓的进程其实是个内核线程。内核线程在ps输出里名字带中括号比如[kworker]、[ksoftirqd]它们不占用用户态上下文pstackattach不上。判断方法就是看ps -o pid,comm,cmdcmd列带[]的就是内核线程直接跳过。5.2 容易被忽略的坑Java进程、僵尸进程与多实例先说Java进程。pstack能打印JVM的native栈比如你可能会看到#0 0x00007f... in os::PlatformEvent::park () from /usr/lib/jvm/.../lib/server/libjvm.so这说明JVM线程在等待某个条件属于正常状态。但如果你想知道Java业务代码到底执行到哪一行pstack无能为力必须配jstack PID。经验是遇到Java进程卡死两个栈都抓先看jstack里业务线程的Java栈再用pstack看对应的native栈基本能定位是业务逻辑死循环、锁等待还是GC停顿。僵尸进程也抓不了。如果进程状态是Zzombie说明它已经死了但父进程还没收尸没有用户态栈可打。ps看到Z状态直接放弃。还有一个坑是多实例部署。同一个服务启动了多个进程不确认PID就抓很容易抓到无关实例。我遇到过在测试环境抓错进程、来回对比白白耗掉半小时的情况。建议抓之前先做两重确认ps -ef | grep 服务名 | grep -v grep再配合端口确认真实PID比如netstat -lntp | grep 服务端口 ss -lntp | grep 服务端口确认无误再执行pstack。5.3 操作规范建议给线上排障留个后路pstack输出很可能不只一次用到。特别是那种“进程还在但问题偶发”的场景我习惯了每次抓完都落盘文件名按“日期-进程名-事件”组织pstack 5678 /tmp/pstack_20250710_order_service_deadlock.txt这样做有几个好处一是可以在终端里用grep精确过滤某个线程或函数二是如果现场被打断回头还能复盘三是和同事协作时直接把文件发到群里比你口头描述“我看到它卡在某个锁上”要准确得多。另外抓栈要多次对比。单次抓栈可能抓到一个恰好路过的状态比如线程正在做常规等待你误判成卡死。我的做法是间隔5到10秒连续抓三次看线程栈是否在原地不动。如果三次都停在同一个位置基本可以确定线程长时间阻塞如果栈在跳变说明进程还在往前走只是某一次抓到了一个中间状态需要换个思路排查。最后说一个我自己实际使用中的体会pstack的输出价值并不只是那一瞬间的栈内容而是它和top、strace、jstack组合起来之后形成的完整证据链。在CentOS 7.6这类环境下我通常先把pstack当第一板斧快速判断方向再决定要不要上strace跟系统调用要不要上perf看采样热点。有一次线上事故就是靠pstack连续两次抓到的线程栈各差一行一眼看出两个线程在互相等同一把锁代码仓库里搜到对应锁变量的加锁顺序后问题半天内就闭环了。把这套流程练熟你会发现自己排查诡异故障的速度会快一个量级。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询