
1. 一次真实的卡死现场Claude Code 不动了我却靠 pstack 打开了新思路先说结论如果你也在 Linux 或 WSL2 里面跑 Claude Code遇到过“界面没崩、终端里最后一行还在闪烁、但怎么按回车都没反应”的情况那 pstack 这套工具值得你花十分钟了解。它是 Linux 上最老的进程堆栈查看工具之一作用就一句话把一个进程当前所有线程的执行现场直接打印出来。配合 Claude Code 这种以 Node.js 为底座、日常又经常挂着一堆 MCP 子进程的 AI 编码工具它尤其能救急。1.1 那个下午的现场没有报错、没有崩溃就是卡住一整晚我自己遇到过一次很典型的故障。下午三点多Claude Code 正在改一个多文件的 TypeScript 项目我让它重构某个模块命令发出去之后终端里出现了一条黄色的编辑中提示然后就再也没有下文了。我试过按 Esc、按 CtrlC、切到别的终端全部无效。ps 杀掉进程、重新进对话目录再恢复会话倒是能回到旧状态但刚才这次任务已经没救了。最让人崩溃的是系统日志里什么都没有Claude Code 自己的日志目录里也没有任何 error只有一条正常的请求记录和一个正常结束的 token。进程没崩溃说明它还活着但它卡在某个地方不做任何事。这个时候如果直接 kill 掉等于放弃了一次现场调查的机会。后来我养成了一个习惯任何“没崩但不动”的进程先别急着杀掉先拿 pstack 照一眼。所谓 pstack就是打印线程栈的工具。Linux 下很多发行版自带也可以单独装。你可以把它理解成给进程拍一张“内部合照”每个线程此刻正在执行什么函数、卡在哪个系统调用上全部一目了然。它不会暂停进程也不影响正在运行的任务非常适合用来收集卡死现场。1.2 为什么是老牌工具 pstack而不是直接重启遇到卡死大部分人的第一反应是“重启”。但重启解决的只是表面症状它没法回答“为什么会卡”。如果你用的是 Claude Code 这种真金白银按 token 计费的工具一次卡死就意味着那段任务的输出可能已经永久丢失下次同样的问题还会再犯。pstack 的定位非常准它只解决“进程到底在等什么”这一个问题。比如你的进程卡住了你能不能回答出来它是在等网络包、等子进程、等磁盘 IO、等锁还是在无限重试知道了这一个问题的答案后面 80% 的修复方案都能顺着往下推。它对 Claude Code 尤其友好因为 Claude Code 本质上是一个跑在 Node.js 上的进程而 Node 进程的绝大部分时间都在事件循环里等回调。它不像那些纯计算型的程序一样能把 CPU 跑满更多时候就是“正在 sleep / 正在 poll / 正在 read”。pstack 打印出来的栈能直接定位到它到底在哪一个系统调用上睡过去了。1.3 为什么这套东西值得单独起一个项目名叫 pstack-claude“pstack-claude”这个项目名我当时起得很随意就是把我日常排查 Claude Code 卡死问题的脚本、常看的栈特征、对比记录都收到一个目录里。后来用着用着发现它已经不只是几条 pstack 命令而是一套可以复用的排查流程第一步抓堆栈快照第二步对照符号特征判断等待类型第三步结合 Node 日志、网络连接、子进程状态交叉验证第四步把结论沉淀成一个特征库下次再遇到类似卡死一眼就能识别。这篇文章就把这套流程完整拆开从 pstack 的原理到我在 Claude Code 场景里踩过的坑、总结的特征全部讲清楚。内容偏实操你跟着步骤做一遍至少能学会“拿到一份 pstack 输出后到底该怎么看”。2. 先搞清楚 pstack 到底看到了什么原生栈和 JS 栈之间的距离很多人在用 pstack 的第一分钟就会受挫拿到输出的确是一大串函数名但全都是v8::internal::...、uv__...、read()、poll()这种底层符号完全找不到任何和 TypeScript、Prompt、模型调用相关的字眼。这不是工具坏了而是你还没理解 Node 进程的机制。2.1 底层原理很简单它拿到的是内核视角的执行现场pstack 的工作方式本质上依赖 Linux 的进程调试接口它会短暂地 attach 到目标进程让每个线程停下来记录当前正在执行的指令位置然后沿着栈帧往回回溯打印出一串调用链。这个调用链不会包含你的 JS 业务逻辑它只反映“此刻线程真正在跑的机器指令属于哪个函数”。这有点像你去医院给设备拍片子。片子能显示的是骨头的轮廓和密度不会直接告诉你“这个人的工作压力来自老板”但骨头上的疲劳性裂纹能反过来证明他最近确实很累。pstack 就是那台拍片机它不解释原因只提供证据。所以 pstack 输出的第一行通常不是main而是某个系统调用比如read、epoll_wait、futex_wait。这些符号才是真正能说明问题的部分。2.2 V8 的双层巴士结构你在栈上看到的其实只是巴士底盘Claude Code 跑在 Node.js 上而 Node.js 内部由两部分组成一部分是 V8 引擎负责解释和编译 JavaScript另一部分是 libuv负责提供事件循环、线程池和文件 IO。你的业务代码会先被 V8 编译成机器码然后由 libuv 摆在合适的线程上运行。当进程阻塞时V8 本身并不会一直占着 CPU它会把执行权交回给 libuv然后 libuv 再把控制权交给内核。于是 pstack 打印出来的栈大概分三种系统调用栈比如read()、write()、epoll_wait()说明进程正在等 IOlibuv 相关栈比如uv__io_poll()、uv__work_submit()说明事件循环在等任务V8 内部栈比如v8::internal::...说明有一批 JavaScript 代码正在被解释或执行但大多数卡死场景里这种栈反而是少数。记住这个分层你就能明白为什么 pstack 里看不到generateDoc()这样的名字。如果你的确需要看 JS 层调用栈pstack 做不到应该用node --inspect配合调试器或者用 llnode 加载 Node 的调试符号来解析这部分我在第 3 章的进阶玩法里会讲。2.3 什么时候不该用 pstack换别的工具更合适再好的工具也有边界。以下情况我不建议用 pstack你是 macOS 用户那 pstack 在原生 macOS 上并不存在需要装 gdb 或用sample命令你想看的是 JavaScript 层的函数调用关系而不是原生线程栈你怀疑的是内存泄漏而不是阻塞那应该优先看堆快照你对目标进程没有 ptrace 权限跑 pstack 会直接报错。这里我给你一张我自己常用的工具选择表免得每次排查都从零开始工具适用场景我主要用它看什么pstack / gstack进程卡死、无响应快速看一眼各线程卡在哪个系统调用/底层函数gdb需要深入调查变量、内存、寄存器附加到进程后交互式调试llnodeNode 进程的深度排障JS 调用栈、堆对象、可疑引用node --inspectJS 层逻辑可疑但不卡死CDP 协议抓 JS 函数调用栈perf性能瓶颈、CPU 时间分布采样热点函数、火焰图数据pstack-claude 这个项目在最早期其实只维护了一张表和一串命令。后来我发现大多数 Claude Code 卡死都属于“系统调用层面的等待”所以 pstack 的出镜率最高其他工具只是偶尔补位。3. 核心实操怎么装、怎么抓、怎么把一份栈读懂到了动手环节。你不需要对 Linux 内核有多深的理解按下面的步骤来基本能完成一次完整的现场取证。3.1 安装 pstack不同发行版还是有区别的Debian / Ubuntu 系最省事sudo apt-get update sudo apt-get install -y pstack如果你所在的环境里没有 pstack 这个包装 gdb 也能得到等价工具 gstack它是一个由 gdb 实现的脚本用法几乎一模一样sudo apt-get install -y gdb gstack pidRHEL / CentOS 系一般通过 gdb 提供sudo yum install -y gdbFedora 用 dnf 同理。装完后先验一下工具能不能用挑一个你知道肯定活着的进程测试sudo pstack 1如果能打出一串栈帧说明工具本身没问题。如果提示没有权限再确认你是不是真的加了 sudo以及目标进程是不是你的用户启动的。3.2 抓取堆栈的三种姿势快照、连续采样、多进程比对第一步是找到 Claude Code 的主进程号。注意别用pgrep claude因为命令行的进程名可能是 node真正的入口脚本可能是 cli.js。我建议用下面这条命令来找ps -ef | grep -i claude | grep -v grep这时候会看到若干进程Claude Code 的主进程一般是一个 node 进程命令行里带着类似cli.js的路径或者带着当前工作目录的参数。把它的 PID 摘出来接着做快照sudo pstack pid /tmp/claude_stack_1.txt一份快照只有一瞬间的证据很多时候不够。因为阻塞可能不是永久的而是每隔几十秒触发一次。这时候我建议连续采样for i in 1 2 3 4 5; do sudo pstack pid /tmp/claude_stack_$i.txt sleep 2 done连续抓五份之后对比一下第一份和最后一份。如果栈完全一致说明进程真的死等在同一位置如果栈在变化说明它其实还是有动作的只是某个回调的重试逻辑走入了死循环或异常分支。还有一种情况值得注意Claude Code 会拉起来 MCP 子进程比如用 npx 启动某个 MCP server。这时候光看主进程不够你也得查看子进程的栈。子进程和父进程的 PID 关系可以用以下命令梳理ps -ef --forest | grep -A 20 -B 2 cli.js看到有父子关系的 node 进程后挨个都抓一份 pstack再对照着看父进程是等子进程输出还是子进程自己在等什么资源。3.3 读懂输出我整理的一张关键符号对照表这是整个 pstack-claude 项目里最值钱的部分。刚接触 pstack 的时候我面对几十行符号完全懵后来把常见现象整理成表再碰见类似栈基本扫一眼就能判断方向栈中反复出现的符号或系统调用含义下一步诊断动作read/recvfrom/tcp_recvmsg正在等待网络数据用 tcpdump 或 ss 看连接状态epoll_wait/uv__io_poll事件循环空闲说明当前没有待处理回调多对比两次采样futex_wait/sys_futex在等待锁可能死锁看是否有多个线程卡在同一个锁地址clock_nanosleep/__sleep/sleep在主动退避重试多半是自动升级或请求重试逻辑read(pipe)/uv__read在等子进程输出优先检查 MCP 子进程是否存活tty_write/ioctl/write往终端写数据被阻塞检查终端缓冲换非终端运行方式试验SSL_*/OPENSSL相关符号在 TLS 握手或加解密阶段抓包看是否卡在 TLS 协商拿到这份表之后你就把 pstack 从“一串看不懂的名字”变成了“一套可以推理的线索”。不过我也要提醒一句看到单个符号不要急着下结论至少结合三份快照和进程状态再判断。3.4 把栈翻译成人话三个真实的判断案例案例 A卡在子进程等待。栈里反复出现wait4、read(pipe)、uv__read同时ps --forest里能看到一个名叫npx的子进程那我基本判断是 Claude Code 拉起某个 MCP 服务器时这个子进程长时间没有输出。对应到用户感知就是每次启动 Claude Code 或设置工具时终端会卡十几秒甚至更久。案例 B日志里出现 auto-update failed进程并不完全卡死。Claude Code 的自动升级机制会在启动时检查新版本。如果 npm 前缀目录没有写权限它会报Auto-update failed: no write permission to npm prefix然后进入重试状态。pstack 上表现为反复出现clock_nanosleep和access类系统调用每次间隔很短。表层现象是“启动变慢”底层原因是权限配置不正确。解决之后整个启动过程畅通无阻。案例 C模型请求发出去了但响应一直没回来。主栈卡在recvfrom或 TLS 相关函数同时网络连接状态显示 ESTABLISHED但数据包不再增加。这种情况大概率是上游服务迟迟没有返回或者本地代理层把响应挂住了。pstack 至少能帮你确认“不是自己的业务代码死循环而是等待外部响应”。4. 我在 Claude Code 里遇到的四种卡死姿势以及对应的 pstack 特征工具在手更重要的还是经验。这一章把我在实际使用 Claude Code 过程中遇到的高频卡死场景整理成一份特征库每个场景都给出 pstack 的表现和修复思路。4.1 场景一MCP 子进程拉起后停住整个会话被拖死MCPModel Context Protocol服务器是 Claude Code 和外部工具沟通的桥梁。很多人会配置一些通过 npx 启动的 MCP server比如文件系统、数据库、浏览器自动化工具。正常情况下 Claude Code 启动时会自动拉起这些服务器但如果某个服务器安装不完整、npx 拉包特别慢、或者服务器内部自己在等交互输入问题就来了。用户看到的现象是启动 Claude Code 或调用某个工具时会话立刻失去响应终端停在那里一动不动。pstack 的表现非常典型父进程栈里出现read(pipe)或者wait4子进程栈里出现epoll_wait或其他等待逻辑ss -tlnp能看到子进程监听了一个端口但没有任何连接进来。我的处理套路是先不着急杀 Claude Code单独执行这个子进程的启动命令看它能不能独立跑起来。比如配置里写的是npx -y some/mcp-server那我就在终端单独跑一遍这条命令观察它是否正常初始化。如果单独跑也要卡那就是服务器本身的问题和 Claude Code 无关。4.2 场景二启动时的自动升级和 npm 权限冲突这个坑在真实用户里出现频率很高。Claude Code 每次启动都会检查新版本而自动升级逻辑本质上是要写全局 npm 目录。如果你的 Node.js 是装在系统目录里的npm 全局前缀可能是/usr/local或/usr/lib/node_modules普通用户根本没有写入权限。于是启动流程变成Claude Code 被唤起后台检查新版本发现需要升级尝试写入 npm 全局目录失败进入短等待重试再失败。整个过程会让进程“看起来卡住”但并没有真正死锁。pstack 的特征是主线程栈反复出现clock_nanosleep和access、openat之类的文件权限检查调用而且连续几次采样之间栈会变化因为它在循环重试。修复思路也很简单把 npm 全局前缀改成用户目录下的可写目录npm config set prefix ~/.npm-global export PATH$HOME/.npm-global/bin:$PATH改完之后重启 Claude Code升级流程能正常走通启动时的卡顿感也会明显消失。4.3 场景三集成终端里的 TTY 背压这个坑最隐蔽也最容易被误判为网络问题。在 VSCode 的集成终端里跑 Claude CodeClaude Code 会输出大量带颜色的日志、编译器输出、状态表格。终端本身是一个 PTY而 PTY 的缓冲区是有上限的。如果输出洪峰一次性灌进来终端来不及消费进程往 stdout 写数据的系统调用就会阻塞。这时候拿 pstack 抓进程栈里会出现和 write、tty、ioctl 相关的符号。注意它和网络请求完全无关但用户感知就是“整个会话卡住了打字也没反应”。验证方法不复杂把输出重定向到文件跑一次如果换成重定向之后不再卡那问题基本就锁定了 TTY。解决方式也简单要么在 VSCode 里加大终端缓冲要么不要使用集成终端改用独立的终端窗口或者把 Claude Code 的日志输出级别调低一点减少非关键内容。4.4 场景四上游模型服务长时间无响应还有一种最让人焦虑的情况任务看起来还在继续请求发出去了但迟迟没有增量输出。这时候 pstack 里网络相关的符号会非常扎眼常见的是recvfrom、poll、epoll_wait混合出现同时网络连接状态是 ESTABLISHED。我的经验是先观察两分钟连续抓三次 pstack。如果每次栈都停在网络等待上那就把重心转向网络层用ss -tp确认连接目标地址和端口再用 tcpdump 看有没有数据包流动。没有数据包流动说明请求可能还没到上游或者到了但上游没有回包有数据包但服务无法处理那就更可能是上游的模型服务超时或限流。这里不讨论任何复杂的网络环境和绕行方案只说一句朴素的建议如果你的本地网络到目标服务本身不稳定那就需要先把自己的网络环境调稳而不是反复重试。5. 从一次排障到日常巡检把 pstack-claude 沉淀成一套顺手脚本单次排障只能解决眼前的问题真正提升效率的是把这套方法工具化。我最后把 pstack 相关的常用操作写成了一套简单脚本既适合临时用也适合交给团队共享。5.1 一条巡检命令批量抓取所有相关进程的堆栈下面的脚本适合放在.bashrc或者独立脚本文件里#!/bin/bash # 自动查找所有和 claude 相关的 node 进程依次抓取堆栈 PID_LIST$(ps -ef | grep -E cli\.js|claude-code|anthropic | grep -v grep | awk {print $2}) if [ -z $PID_LIST ]; then echo 没有找到 claude 相关进程 exit 1 fi for PID in $PID_LIST; do echo PID: $PID sudo pstack $PID 2/dev/null || echo [ERROR] 抓取 PID $PID 的堆栈失败 done保存成脚本后在卡死现场只需要执行一次就会输出所有 Claude Code 相关进程的完整堆栈。如果你希望做定时采样可以在外面套一个循环for round in 1 2 3; do sudo bash claude_stack_scan.sh /tmp/stack_round_$round.txt sleep 3 done连续三轮采样结果对比基本就能判断是“死等”还是“慢重试”。我把这套采集脚本收到 pstack-claude 项目目录里遇到新问题就只更新符号对照表命令本身几乎不用改。5.2 进阶搭配perf 和 llnode 怎么和 pstack 互补pstack 的短板是看不到 JS 层调用关系所以当你判断出阻塞不在底层、而是某个 JS 回调有问题时需要进一步上更强的手段。perf 是 Linux 下的性能分析工具它可以通过采样构建调用图。配合 Node.js 的--perf-basic-prof-only-functions启动参数V8 会额外生成一个 symbol map 文件perf 就能把原生栈和 JS 函数名对应起来。启动 Claude Code 时可以这样加环境变量测试NODE_OPTIONS--perf-basic-prof-only-functions claude想抓取样例时再用 perf 执行sudo perf record -F 99 -p pid -g -- sleep 10 sudo perf report --no-children这样一个耗时任务卡住时你能直接看到消耗时间最多的究竟是在读文件、走网络还是跑正则。llnode 则更适合用来抓 JS 堆栈。它是针对 Node 的 lldb 插件支持v8 bt这类命令打印 JavaScript 调用栈。安装配置步骤比 pstack 繁琐一点但一旦遇到需要精确理解 JS 层执行顺序的场景它比 pstack 远更准确。我的建议是pstack 先摸方向perf 和 llnode 再深入三级排查思路组合起来用。5.3 三句经验总结踩过几次坑后才真正懂得的事第一件抓堆栈之前想一下“值不值得”。如果只是偶发卡顿而且重来一次代价不高直接重启就够了不必每次都做完整取证。但如果是高复现率的卡死那就一定要把现场留着反复采样把特征库补全。第二件pstack 输出一定要结合时间。单次堆栈只是快照它只能告诉你“这一刻在哪”。分析卡死问题至少要两次采样最好间隔两三秒观察栈是否固定。固定不变说明真死锁或者死等待不断变化说明进程还在干活只是某个循环没有收敛。第三件不要把 MCP 子进程忘掉。Claude Code 卡死十次里有三四次是底层 MCP 服务器惹的祸。抓完主进程栈再花一分钟看一下子进程栈很多疑难杂症立刻迎刃而解。做这套 pstack-claude 之前我遇到卡死多半是跟着感觉试各种姿势浪费不少时间。现在流程固定下来从抓栈到出结论基本在十分钟之内。想复现这套方法的人不需要照抄我的目录结构只要记住两个原则先备份现场再对症下药先抓底层栈再深入 JS 层。工具不在多用得熟才是关键。