Linux命令-pstack(打印进程的堆栈跟踪)
pstack 是一个诊断工具,用于打印指定进程当前所有线程的调用栈(call stack)。它让开发者能够在不暂停进程的情况下,查看进程正在执行什么代码。这在排查进程卡死、死锁、性能瓶颈等问题时极其有用------不需要 gdb 附加,也不需要重启进程。
当你的生产服务器上某个进程突然 CPU 飙到 100%,但你手头没有 perf、没有火焰图工具时,连续执行几次
pstack <PID>就能看到卡在哪个函数------这就是最快速的现场诊断。
命令语法
pstack PID
工作原理
pstack 本质上是一个 Shell 脚本,它使用 gdb(GNU Debugger)以非交互模式附加到目标进程,执行 thread apply all bt(打印所有线程的回溯),然后立即分离。
bash
# pstack 的实质操作等价于
$ gdb -batch -ex "thread apply all bt" -p PID
# 部分系统的 pstack 实现使用
$ gstack PID # RHEL/CentOS 上的等价命令
实战示例
1. 基础用法
bash
# 打印某个进程的堆栈
$ sudo pstack 12345
Thread 3 (Thread 0x7f8a1bfff700 (LWP 12346)):
#0 0x00007f8a1c3e8a3d in poll () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x00007f8a1d4c5e82 in ?? () from /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0
#2 0x00007f8a1d4c5f3c in g_main_loop_run () from /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0
#3 0x000055c3a4b0c123 in main_loop() at src/main.c:142
#4 0x000055c3a4b0b456 in thread_func(void*) at src/worker.c:56
#5 0x00007f8a1c3b8609 in start_thread () from /lib/x86_64-linux-gnu/libpthread.so.0
#6 0x00007f8a1c3f7293 in clone () from /lib/x86_64-linux-gnu/libc.so.6
Thread 2 (Thread 0x7f8a1affd700 (LWP 12347)):
#0 0x00007f8a1c3eb32d in nanosleep () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x000055c3a4b0c789 in timer_thread(void*) at src/timer.c:23
#2 0x00007f8a1c3b8609 in start_thread () from /lib/x86_64-linux-gnu/libpthread.so.0
#3 0x00007f8a1c3f7293 in clone () from /lib/x86_64-linux-gnu/libc.so.6
Thread 1 (Thread 0x7f8a1d80e780 (LWP 12345)):
#0 0x00007f8a1c3f24b4 in __libc_read () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x000055c3a4b0a012 in process_request(int) at src/handler.c:89
#2 0x000055c3a4b09de4 in main(int, char**) at src/main.c:256
# 解读:
# Thread 1(主线程):正在 read() 等待数据(正常 I/O 阻塞)
# Thread 2(定时器线程):正在 nanosleep()
# Thread 3(工作线程):正在 poll() 事件循环中
2. 问题诊断
bash
# 场景1:进程 CPU 100%------连续采样看卡在哪里
$ for i in {1..5}; do
echo "=== Sample $i ==="
sudo pstack 12345 | grep '#0'
sleep 1
done
=== Sample 1 ===
#0 0x000055c3a4b0e456 in process_loop() at src/loop.c:234
=== Sample 2 ===
#0 0x000055c3a4b0e456 in process_loop() at src/loop.c:234
=== Sample 3 ===
#0 0x000055c3a4b0e456 in process_loop() at src/loop.c:234
# → 所有采样都在同一函数!这是死循环或密集计算的典型表现
# 场景2:进程疑似死锁
$ sudo pstack 12345
# 查看多个线程是否都在等待锁
Thread 2:
#0 pthread_cond_wait() from libpthread
#1 mutex_lock() at lock.c:23
Thread 3:
#0 pthread_cond_wait() from libpthread
#1 mutex_lock() at lock.c:23
# → 两个线程都在等待锁!高度怀疑死锁
# 场景3:排查内存泄漏(配合其他工具)
$ sudo pstack 12345
# 查看是否在 malloc/new 附近
#0 malloc() from libc
#1 allocate_buffer() at mem.c:45
# → 频繁出现在内存分配函数上,可能存在内存泄漏
# 场景4:排查 I/O 瓶颈
$ for i in {1..10}; do
sudo pstack 12345 | grep -E '#0.*(read|write|fsync|send|recv|poll|epoll)'
sleep 2
done
# → 频繁出现在 I/O 调用,说明 I/O 是瓶颈
3. 获取调试符号
bash
# pstack 输出中如果有 ?? (),表示没有调试符号
#0 0x00007f8a1d4c5e82 in ?? () from /usr/lib/libglib-2.0.so.0
# 安装调试符号包(Debian/Ubuntu)
$ sudo apt install libglib2.0-0-dbg
$ sudo apt install nginx-dbg
# 安装调试符号包(RHEL/CentOS)
$ sudo debuginfo-install nginx
$ sudo debuginfo-install glibc
# 对于自编程序,编译时加 -g
$ gcc -g -O2 -o myapp myapp.c
# 带符号的 pstack 输出对比
#0 0x00007f8a1c3e8a3d in __GI___poll (fds=0x..., nfds=3, timeout=-1) at poll.c:29
# → 现在能清楚地看到参数名、文件名和行号!
4. 与 gdb/gstack 对比
bash
# pstack:简洁,一行命令
$ sudo pstack 12345
# gstack:RHEL/CentOS 上的等价命令
$ sudo gstack 12345
# gdb 手动:更灵活,可以交互
$ sudo gdb -p 12345
(gdb) thread apply all bt # 等价于 pstack
(gdb) thread apply all bt full # 含局部变量值
(gdb) info threads # 查看线程列表
(gdb) thread 1 # 切换到线程 1
(gdb) frame 3 # 切换到第 3 层调用
(gdb) info locals # 查看局部变量
(gdb) detach # 分离(进程继续运行)
(gdb) quit
# gdb 非交互模式(等价 pstack + 更多信息)
$ sudo gdb -batch \
-ex "thread apply all bt" \
-ex "thread apply all bt full" \
-ex "info registers" \
-p 12345
# 使用 /proc 直接获取(无需 gdb,但信息有限)
$ cat /proc/12345/stack
[<0>] poll_schedule_timeout.constprop.0+0x46/0xb0
[<0>] do_sys_poll+0x4e3/0x5e0
[<0>] __x64_sys_poll+0xa9/0x140
[<0>] do_syscall_64+0x59/0xc0
[<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9
# /proc/PID/stack 只显示内核态调用栈!
5. 批量诊断脚本
bash
# 对齐多个进程的堆栈
multi_pstack() {
for pid in "$@"; do
echo "========================================="
echo " PID: $pid ($(cat /proc/$pid/comm 2>/dev/null))"
echo "========================================="
sudo pstack $pid 2>/dev/null
echo ""
done
}
# 使用
$ multi_pstack 1234 5678 9012
# 定时采样指定进程(排查间歇性问题)
sample_pstack() {
local pid=$1
local interval=${2:-1}
local count=${3:-10}
local output="pstack_${pid}_$(date +%Y%m%d_%H%M%S).txt"
for i in $(seq 1 $count); do
echo "=== $(date) - Sample $i/$count ===" >> "$output"
sudo pstack $pid >> "$output" 2>&1
sleep $interval
done
echo "Output saved to: $output"
}
$ sample_pstack 12345 2 20 # 每2秒采样,共20次
发行版差异
| 发行版 | pstack 可用 | 备注 |
|---|---|---|
| RHEL/CentOS 7+ | gstack |
命令名为 gstack |
| Debian/Ubuntu | pstack |
包名:pstack |
| Ubuntu 22.04+ | 可能需手动安装 | apt install pstack |
| Arch Linux | pstack (AUR) |
需要从 AUR 安装 |
| Fedora | gstack |
在 gdb 包中 |
| Alpine Linux | 无 | 无此工具 |
注意:pstack 本质是 Shell 包装脚本。如果系统没有这个命令,可以手动调用 gdb 实现同样功能。
手动创建 pstack
bash
# 如果系统没有 pstack,可以手动创建
$ cat > /usr/local/bin/pstack << 'EOF'
#!/bin/bash
if test $# -ne 1; then
echo "Usage: $(basename $0) <PID>"
exit 1
fi
pid=$1
if ! kill -0 $pid 2>/dev/null; then
echo "Process $pid not found or not accessible"
exit 1
fi
gdb -batch -ex "thread apply all bt" -p $pid 2>/dev/null
EOF
$ chmod +x /usr/local/bin/pstack
使用限制
| 限制 | 说明 |
|---|---|
| 需要 root 或进程所有者 | 调试其他用户的进程需要权限 |
| 需要 ptrace 权限 | /proc/sys/kernel/yama/ptrace_scope = 0 或 root |
| 短暂暂停进程 | gdb 附加时进程会短暂挂起 |
| 内核线程无法调试 | 只能调试用户态进程 |
| 需要调试符号 | 否则只能看到地址和偏移 |
bash
# 临时允许非 root 用户使用 ptrace
$ sudo sysctl -w kernel.yama.ptrace_scope=0
# 永久设置
$ echo "kernel.yama.ptrace_scope = 0" | sudo tee -a /etc/sysctl.conf
总结
pstack 是最快速的"现场取证"工具。核心用法:
bash
sudo pstack <PID> # 查看进程当前调用栈
for i in {1..10}; do sudo pstack <PID> | grep '#0'; sleep 1; done # 持续采样
它不能替代 perf 火焰图或 strace,但在"看一眼就知道问题在哪"的场景中是无价的------特别是排查 CPU 飙升和疑似死锁时。记住:CPU 100% 先看 pstack,I/O hang 先看 strace。