pstack-claude:用进程栈破解Claude Code卡顿之谜 如果你跟我一样把 Claude Code 当成日常项目的编程副驾一定遇到过这种场景任务跑到一半终端像死机一样卡住光标闪烁日志也不更新。重启吧舍不得进度不重启吧又不知道它卡在哪。我在 Linux 上用 Claude Code 做自动化重构的时候被这种“黑盒卡顿”反复折磨后来想到 Linux 上最硬的排查工具 pstack——直接把进程的调用栈拉出来看看它到底停在哪一行、在等什么。于是就有了 pstack-claude 这个小项目一套用 pstack 抓取 Claude Code 进程栈、快速定位卡死原因的诊断工作流。这篇博文会从 Claude Code 的进程模型开始讲到 pstack 的原理和权限再给出一套可以直接抄的采样脚本和排查流程最后把我踩过的坑和解决方式整理出来。1. Claude Code 先想清楚它到底是怎么跑的1.1 终端里的自动化编程助手到底在忙什么Claude Code 本质上是一个跑在终端里的编程助手它跟网页版最大的区别是它拥有项目工作区的读写能力会主动读取你的目录结构、打开文件、搜索代码甚至直接在终端里执行命令。你可以把它理解成一个“能操作电脑的模型”而不只是“一个聊天的模型”。这种能操作的特性意味着它的大多数工作并不是在纯文本对话里完成的而是拆成一个又一个动作读文件、查代码、跑测试、改代码、再验证。每一个动作背后都涉及实际的系统调用比如read、write、execve。所以当你看到终端里光标停了很久它可能正在做下面几件事之一模型正在思考等待推理结果返回子进程正在执行某个命令比如npm test、grep -R程序在等待网络请求返回某个 MCP 工具服务卡住拖住了主任务真的死锁了线程互相等待资源。不把进程内部的状态暴露出来光靠肉眼看终端永远区分不了“它在思考”和“它卡死了”。这也是我在 pstack-claude 里最先想解决的问题能不能在任何一个可疑时刻给 Claude Code 的进程拍一张快照直接看到它停在哪里。1.2 安装前把环境收拾利索Claude Code 的安装入口非常简单一个 npm 全局安装命令就能搞定npm install -g anthropic-ai/claude-code安装完成之后直接敲claude就能进入交互式对话界面。但实际使用之前有几个环境问题非常值得提前处理否则后面全是坑。首先是 Node.js 的版本。Claude Code 依赖较新的 Node 运行时建议至少 18 以上。我遇到过老 node 版本导致 CLI 启动直接报错的情况所以如果你已经装了 node先用node -v看一下。然后是 Windows 环境。Claude Code 在 Windows 下跑经常会出现一个报错Claudes workspace requires the Virtual Machine Platform on Windows。这个报错的意思是你的系统没有打开“虚拟机平台”这个 Windows 功能Claude Code 的隔离工作区依赖这个虚拟化能力。解决办法是在管理员权限的终端里执行dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart执行完要重启电脑然后在 Microsoft Store 安装 WSL 2或者用wsl --install -d Ubuntu-22.04快速装一个 Linux 发行版。如果你在 WSL 里跑 Claude Code整体体验比在 Windows 原生环境下稳定得多MCP 和 pstack 这些调试工具也能直接用。1.3 让日志成为排查的第一手资料Claude Code 会把运行过程的每一步轨迹写到本地日志文件里路径一般在用户目录下的.claude/logs里。你可以在启动 Claude Code 后开另一个终端窗口用下面的命令实时盯日志tail -f ~/.claude/logs/claude*.jsonl这些日志是 JSONL 格式每行一条事件记录包含时间戳、事件类型、模型调用信息等。比如tool_use事件代表它准备调用某个工具tool_result代表工具执行完成。日志的价值在于你可以在用 pstack 抓完栈之后把栈里的时间点和日志里的最后一条事件对上立刻知道卡之前它在做什么。我习惯每次跑长任务之前先把日志文件路径记下来卡住的第一反应不是杀进程而是先看日志最后几条写了什么。这是后面整套排查链路的关键地基。2. pstack 工具选型一把快准稳的进程探针2.1 为什么选中 pstack 而不是一上来就 gdbLinux 下查看进程栈的方式有好几种pstack、gdb、strace 各有各的适用场景。我在设计 pstack-claude 的时候首选 pstack原因很直接它快。pstack 的用法一句话就能说明白pstack pid它会输出目标进程当前所有线程的调用栈而且不需要你手动 attach、继续运行、再 detach。整个操作是一次性的就像给进程拍一张 X 光片拍完马上恢复运行。Claude Code 在生产环境跑自动化任务的时候我不想用 gdb 那种重型工具去打断它pstack 快进快出侵入性非常低。从原理上讲pstack 是通过ptrace系统调用 attach 到目标进程读取/proc/pid/task目录下的线程信息然后借助调试接口把每个线程的调用栈符号化最后再 detach。所以它能看到的是某一个瞬间的状态。这个特性和我排查卡顿的需求完美匹配我想知道的就是“这一刻它停在哪”。对比几个常见工具我的选型逻辑是这样的工具侵入性输出完整度上手难度适合场景pstack低中低快速查看所有线程栈gdb attach中高高崩溃分析、条件断点strace低只跟踪系统调用中分析文件、网络、进程阻塞如果 Claude Code 是直接崩溃退出了那我会用 gdb 去分析 core dump如果只是卡住不动pstack 的输出已经完全够用。2.2 三个备选工具的真实差距pstack 的输出比较简洁默认只给线程号和每一层的函数名。gdb attach 能做的事情更多比如动态打断点、查看内存变量但对于“进程还活着只是卡住”这个场景gdb 的能力是过剩的。strace 是另一个常见选择它不输出栈而是输出系统调用序列。你可以通过strace -p pid看到进程正在执行的系统调用如果它卡在read上你会看到一直阻塞在read调用里。这个信息其实很有价值我在分析 Claude Code 卡在网络请求的时候也会用。但 strace 有个问题它只告诉你“卡在 read”却不一定告诉你“是哪个业务逻辑触发这次 read”。pstack 能直接告诉你调用链是从哪个函数走到这一步的定位更直观。所以我在 pstack-claude 里的做法是默认先上 pstack拿不到有效信息再上 strace 补系统调用维度最后才考虑 gdb。没必要一上来就上大炮打蚊子。2.3 权限和 ptrace_scope 是绕不开的坎在你第一次运行pstack pid的时候很有可能会看到Operation not permitted。这不是 pstack 坏了而是 Linux YAMA 安全模块在拦你。检查一下这个文件cat /proc/sys/kernel/yama/ptrace_scope如果输出是1说明系统默认只允许进程追踪它的子进程。Claude Code 是在你终端里启动的照理说你可以 trace 它但如果你把 Claude Code 放到后台、或者用服务方式启动它可能就不是你的子进程了这时候 pstack 会被拒绝。临时解决方案是在测试机上把限制调低echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope或者直接对 pstack 用 sudosudo pstack pid我的建议是尽量用 sudo 去抓不要轻易改 ptrace_scope 全局配置尤其是在公司共享的开发机上。这个改动的安全影响范围太大你不想因为排一个 bug 把整台机器的不设防状态留给别人。3. pstack-claude 实战从抓栈到定位卡顿的完整链路3.1 先写一个不会污染现场的采样脚本我最开始是纯手工操作卡住了就打开另一个终端pgrep找 PID再pstack。但手工操作有个问题卡顿往往是间歇性的你人不在终端前的时候它开始卡回来的时候已经恢复根本抓不到现场。所以我写了第一个采样脚本思路很简单定时对目标进程抓一次栈连续抓十次把结果存成带时间戳的文件。#!/bin/bash # sample-claude-stack.sh target_pid$(pgrep -f claude | head -n 1) if [ -z $target_pid ]; then echo no claude process found exit 1 fi out_dirstack_$(date %Y%m%d_%H%M%S) mkdir -p $out_dir for i in $(seq 1 10); do timestamp$(date %H%M%S) pstack $target_pid $out_dir/${timestamp}.txt 21 sleep 10 done echo saved to $out_dir为什么连续采样十次而不是一次因为单次栈只能代表一瞬间的状态。进程可能恰好停在一个正常的等待点比如事件循环在等下一次循环你误判成卡死。连续采样之后如果每次的栈都停在同一位置那基本可以断定它真的卡在这里如果栈的内容各不相同说明它还在推进只是输出比较慢。实际使用的时候我会把采样间隔调成 5 秒连续抓 20 次这样能覆盖 100 秒左右的窗口基本上能抓住卡顿的完整形态。3.2 Node.js 进程栈到底怎么看Claude Code 是 Node.js 应用所以 pstack 抓出来的栈里会看到大量 libuv、V8 和 Node 底层的函数。很多人第一次看这种栈会懵觉得不像是自己能理解的东西。其实不用怕你只需要关注几个关键信息。一个典型的正常等待中的栈可能是这样的Thread 1 (process 12345): #0 epoll_wait (k3, events0x7ffe..., timeout...) #1 uv_run (...) #2 node::Start (...) ...epoll_wait是事件循环在等待新事件这是 Node.js 最正常的空闲状态。如果 Claude Code 正在等待用户输入或者等待网络数据你大概率会看到这个栈。另一种情况是栈里出现大量 V8 内部编译和执行相关函数比如v8::internal::...这说明它还活着正在密集计算可能就是模型推理过程中的本地处理。这种情况下不需要太担心过一会儿它自然会继续输出。真正值得警惕的是多个线程卡在互相等待的锁上。如果多个线程的栈里都有futex_wait、pthread_cond_wait这类调用说明可能有锁竞争或者死锁趋势。Claude Code 会启动一些并行的后台任务比如 MCP 工具服务器它们跟主进程之间的协同一旦出问题就容易出现这种局面。你不需要理解每一层函数先扫栈顶再找重复出现的符号然后对着日志看时间线基本就能还原现场。3.3 用 MCP 工具子进程把范围扩大Claude Code 支持通过 MCP 协议接入外部工具服务器常见的启动方式会用到npx。比如你在配置里写一个 mcpServers 项Claude Code 就会拉一个本地子进程起来跑那个工具服务。这些 MCP 子进程也是会卡住的。它们卡住的时候Claude Code 的日志里会一直显示等待tool_result但实际命令可能根本没执行完。这时候 pstack 要用两次一次抓 Claude Code 主进程一次抓 MCP 子进程。先找所有相关进程pgrep -af claude|mcp|npx然后分别抓栈。如果有某个 MCP 工具的子进程一直停在一个业务函数里那问题几乎可以定位到那个工具本身。用这种分层抓栈的方式我在实际项目中排除过很多次“看起来是 Claude Code 卡死其实是某个代码搜索工具慢”的情况。3.4 把诊断流程收进一个函数采样脚本解决了“抓”的问题但每次都要敲那么一长串命令还是麻烦。所以我后来把它收成了 shell 函数放进~/.bashrc里claude-stack() { pid$(pgrep -f anthropic-ai/claude-code | head -n 1) if [ -z $pid ]; then echo Claude Code 没在运行 return 1 fi echo pstack $pid pstack $pid echo 最近日志 tail -n 5 ~/.claude/logs/claude*.jsonl 2/dev/null | tail -n 5 }这样每次卡住了新开一个终端敲claude-stack就能同时拿到进程栈和最近日志。这个命令我从头用到现在是 pstack-claude 整套流程里最顺手的一环。4. 高频问题实录安装、版本与权限的坑4.1 Windows 下虚拟机平台报错怎么解Windows 原生跑 Claude Code 的朋友肯定见过这条Claudes workspace requires the Virtual Machine Platform on Windows。我第一次看到也愣了一下后来才知道这是它要用到 Windows 的虚拟化能力来构造隔离工作区。报错的根源基本是两个一是虚拟机平台功能没有开启二是 WSL 内核没更新或者没安装。虚拟机平台功能的开启方式我在前面已经写过了用 dism 命令。如果执行完还是报同样的错误检查一下系统是否启用了 Hyper-V部分旧版本 Windows 需要去“控制面板 - 程序和功能 - 启用或关闭 Windows 功能”里手动勾选“虚拟机平台”和“适用于 Linux 的 Windows 子系统”。开启后重启再跑一次wsl --update大概率就解决了。我个人的建议是如果只是日常写代码直接装一个 WSL2 发行版把项目放在 Linux 文件系统里跑这个问题能避就避。Windows 和 Linux 文件系统的交互本身就是一把性能损耗刀Claude Code 大量读写文件的时候放在 WSL 里会明显顺滑一些。4.2 自动更新失败no write permission to npm prefixClaude Code 默认会自动更新更新失败时会报这类错误auto-update failed: no write permission to npm prefix这个错误的本质是Claude Code 被安装到了系统级 npm 全局目录通常是/usr/lib/node_modules普通用户没有写权限。每次启动时它尝试检查自身版本、准备升级一写就报权限错误。最干净的解法是让 npm 的全局目录落在用户目录下而不是系统目录。先看当前 npm prefixnpm prefix -g如果返回的是/usr或者/usr/local那就把它改到用户级目录npm config set prefix ~/.npm-global然后把~/.npm-global/bin加进环境变量echo export PATH~/.npm-global/bin:$PATH ~/.bashrc source ~/.bashrc重新安装npm install -g anthropic-ai/claude-code之后启动 Claude Code 就不会再遇到权限类自动更新问题了。这里有个教训不要去chmod -R 777系统目录来绕过问题往后版本越升越乱用户级目录才是干净方向。4.3 pstack 抓不到栈或者只有一行pstack 偶尔会给你一个“空手而归”的结果。常见原因有三个目标进程不是你的子进程且 ptrace_scope 限制了你需要加 sudo进程已经退出了或者 PID 被复用导致 pstack 抓到一个跟 Claude Code 无关的进程Claude Code 的某些子进程是内核线程或者已经进入不可中断睡眠用户态栈为空。如果 pstack 确实拿不到有效栈我还有一个备用方案gdb -p pid -batch -ex thread apply all bt这条命令能拿到每个线程的完整 backtrace信息量比 pstack 更大只是速度慢一些。遇到特别诡异的问题时我会用这个命令做交叉验证。4.4 排查卡顿时的几条铁律用 pstack-claude 排过几十次问题之后我总结出三条特别重要的原则。第一先采样再重启。进程一杀掉现场就没了任何栈信息都不会留下。哪怕你觉得已经没救了也先抓一次栈再决定下一步。第二多抓几次别信一次快照。单次抓出来的可能只是正常状态连续抓几次才能看出趋势。我的脚本默认抓十次就是为了避免误判。第三时间戳对齐。pstack 输出的文件名带时间日志里每条事件也有时间把两者对齐才能重建完整的现场。很多问题看到栈里卡在某个工具调用再回到日志里看对应的工具执行时间原因瞬间就清楚了。5. 写在最后的一点私货从构思到写完这套流程我最大的感受是开发工具再聪明也终究是个进程进程就一定会遇到卡死、阻塞、资源等待的问题。与其等到出了问题拍脑袋重启不如提前准备一把趁手的“探针”让它在你需要的时候给你答案。pstack-claude 并不是一个复杂的项目它只是一个把 pstack、日志、定时采样这些基础能力组合起来的工作流。但正是这样的工作流帮我把几十次“救不回来”的长时间任务救回来了。我的习惯是把claude-stack函数放在所有长期任务的启动脚本旁边每次 Claude Code 开始执行长时间重构任务前我都会先开启日志跟踪然后随时准备采样。现在如果再遇到它卡住我不会急着按 CtrlC而是先抓栈再翻日志基本都能说出个一二三来。如果你也在被 Claude Code 的长任务卡顿困扰可以从一个最简单的采样脚本开始不用一开始就做得很重。关键是让数据替你说“它到底卡在哪”而不是凭感觉盲猜。这套思路也不只适用于 Claude Code所有 Node.js 后台服务卡顿pstack 都是同样的用法。希望这篇文章能帮你在排查路上省下一点时间。