Linux命令-pstack(打印进程的堆栈跟踪)

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。