深入 Linux C/C++ 调试工具之 ps(进程快照一眼看清)
目录
一、前言——ps 工具简介
1.1 什么是 ps
ps(process status)是 Linux/Unix 上最基础的 进程快照查看工具,一次性列出当前系统中的进程(或线程)信息:PID、状态、CPU/内存占比、命令行、父进程、TTY 等。
一句话定位:
ps 是进程世界的 「相机快门」 —— 不像 top 持续刷新,而是瞬间拍一张;脚本、排查「进程在不在 / 僵尸 / 线程数」时首选。
1.2 与相关工具对比
|
工具 |
特点 |
典型场景 |
|---|---|---|
|
ps |
快照、可脚本化、字段丰富 |
查 PID、状态、命令行、线程 |
|
top / htop |
实时刷新、可交互 |
盯 CPU/内存热点 |
|
pgrep / pidof |
按名找 PID |
快速定位进程号 |
|
pstree |
树状父子关系 |
看 fork 树、僵尸挂谁下面 |
|
pidstat |
按进程采样 CPU/IO |
持续统计 |
|
gdb / strace |
单进程深入 |
断点、系统调用 |
服务异常
├─ ps / pgrep → 进程在不在?几个?什么状态?
├─ top → 谁最吃 CPU/内存?
├─ pstree → 父子关系、僵尸挂在哪?
└─ strace / gdb → 卡住的具体原因?
二、使用场景——何时使用 ps
1、确认 C/C++ 服务是否在跑、有几个实例
ps -ef | grep my_server
pgrep -a my_server
ps -C my_server -o pid,ppid,stat,cmd
2、查僵尸进程(Zombie)
典型现象: 子进程退出,父进程未 wait/waitpid。
ps -ef | grep ' Z '
# 或
ps axo pid,ppid,stat,cmd | awk '$3 ~ /Z/'
STAT 含 Z → 僵尸;看 PPID 找未回收的父进程。
3、多线程程序:列出所有线程
ps -eLf | grep my_server
# 或指定进程
ps -T -p $(pidof my_server)
ps -L -p $(pidof my_server) -o pid,tid,psr,pcpu,stat,comm
NLWP / -L 可见线程数;C/C++ 里 pthread_create 出来的都能看到。
4、进程卡住:看状态 D / S / R
ps -eo pid,stat,wchan:30,cmd | grep my_server
|
STAT |
含义 |
常见原因 |
|---|---|---|
|
|
Running / runnable |
在跑或待调度 |
|
|
Interruptible sleep |
等事件(多数正常) |
|
|
Uninterruptible sleep |
常卡在磁盘 I/O |
|
|
Zombie |
父进程未 wait |
|
|
Stopped |
被信号停下(Ctrl-Z / SIGSTOP) |
wchan 显示内核等待点(如 futex_wait、poll_schedule)。
5、脚本里取 PID / 监控
PID=$(pgrep -n my_server) # 最新一个
ps -p "$PID" -o pid=,rss=,pcpu=
# 定时采样
while true; do ps -C my_server -o pid,pcpu,rss,stat; sleep 1; done
6、看完整命令行与环境相关信息
ps -efww | grep my_server # ww:不截断 cmd
ps e -p PID # 显示环境变量(部分版本)
cat /proc/PID/cmdline | tr '\0' ' '; echo
7、按 CPU / 内存排序找「凶手」
ps aux --sort=-%cpu | head
ps aux --sort=-%mem | head
ps -eo pid,pcpu,pmem,rss,cmd --sort=-pcpu | head
三、核心原理——工作原理深度解析
3.1 数据从哪来
ps 不向内核发特殊调试协议,而是读 /proc 伪文件系统:
/proc/
├── 1/ # PID=1 的进程目录
│ ├── stat # 状态、ppid、utime…
│ ├── status # 可读字段:State、Threads、VmRSS…
│ ├── cmdline # 启动命令行(\0 分隔)
│ ├── comm # 短进程名(≤15 字符)
│ ├── wchan # 等待信道
│ ├── task/ # 线程(每个 TID 一个目录)
│ └── ...
├── 2/
└── ...
因此:
-
快照:执行瞬间的
/proc内容,不是连续采样 -
权限:别人进程的部分字段可能受限(cmdline 等)
-
开销:进程极多时全量
ps aux也会扫很多目录,脚本宜用-p/-C收窄
3.2 STAT 字段详解(读懂第二列)
ps 的 STAT/S 常由 主状态字母 + 修饰符 组成,例如 Ssl、R+:
|
字符 |
含义 |
|---|---|
|
|
主状态(见上表) |
|
|
高优先级(nice 较小) |
|
|
低优先级(nice 较大) |
|
|
有页面锁在内存(mlock) |
|
|
session leader |
|
|
多线程(用 CLONE_THREAD) |
|
|
前台进程组 |
示例:Ssl ≈ 休眠中的多线程 session leader(很多守护进程)。
3.3 进程 vs 线程在 ps 里长什么样
Linux 线程本质是共享地址空间的 轻量进程(TID):
# 只看线程组(进程)
ps -p PID -o pid,nlwp,cmd
# 展开每个线程
ps -L -p PID -o pid,tid,stat,pcpu,comm
-
PID:线程组 ID(TGID),对应用户态「进程」 -
TID/SPID/LWP:单个线程 ID -
NLWP:该进程线程数
C/C++:pthread_create → NLWP 增加;主线程 TID 通常等于 PID。
3.4 僵尸进程机制(Z)
子进程 exit
→ 内核保留 task_struct 最小信息(供父进程 wait 取退出码)
→ ps 显示 Z(zombie)
→ 父进程 wait/waitpid 后,条目消失
→ 若父进程永远不 wait:僵尸一直在,占 PID 号
父进程若已退出,僵尸会被 init/systemd(PID 1) 收养并回收。
因此:
-
快照:执行瞬间的
/proc内容,不是连续采样 -
权限:别人进程的部分字段可能受限(cmdline 等)
-
开销:进程极多时全量
ps aux也会扫很多目录,脚本宜用-p/-C收窄
3.2 STAT 字段详解(读懂第二列)
ps 的 STAT/S 常由 主状态字母 + 修饰符 组成,例如 Ssl、R+:
|
字符 |
含义 |
|---|---|
|
|
主状态(见上表) |
|
|
高优先级(nice 较小) |
|
|
低优先级(nice 较大) |
|
|
有页面锁在内存(mlock) |
|
|
session leader |
|
|
多线程(用 CLONE_THREAD) |
|
|
前台进程组 |
示例:Ssl ≈ 休眠中的多线程 session leader(很多守护进程)。
3.3 进程 vs 线程在 ps 里长什么样
Linux 线程本质是共享地址空间的 轻量进程(TID):
# 只看线程组(进程)
ps -p PID -o pid,nlwp,cmd
# 展开每个线程
ps -L -p PID -o pid,tid,stat,pcpu,comm
-
PID:线程组 ID(TGID),对应用户态「进程」 -
TID/SPID/LWP:单个线程 ID -
NLWP:该进程线程数
C/C++:pthread_create → NLWP 增加;主线程 TID 通常等于 PID。
3.4 僵尸进程机制(Z)
子进程 exit
→ 内核保留 task_struct 最小信息(供父进程 wait 取退出码)
→ ps 显示 Z(zombie)
→ 父进程 wait/waitpid 后,条目消失
→ 若父进程永远不 wait:僵尸一直在,占 PID 号
父进程若已退出,僵尸会被 init/systemd(PID 1) 收养并回收。
四、命令参数——常用选项详解
4.1 高频「整句」命令
|
命令 |
含义 |
|---|---|
|
|
所有用户进程,BSD 详细列 |
|
|
所有进程,UID/PID/PPID/CMD |
|
|
同上,命令行不截断 |
|
|
某用户的进程 |
|
|
按可执行名选中 |
|
|
指定 PID |
|
|
显示线程 |
|
|
树状父子关系 |
4.2 自定义输出列 -o
ps -eo pid,ppid,user,stat,pcpu,pmem,rss,nlwp,wchan:20,cmd
常用列名:
|
列 |
含义 |
|---|---|
|
|
进程号 / 父进程号 |
|
|
用户 |
|
|
状态 |
|
|
CPU 占比(自启动以来平均,非瞬时) |
|
|
物理内存占比 |
|
|
常驻内存(KB) |
|
|
虚拟地址空间大小 |
|
|
线程数 |
|
|
启动时间 / 已运行时长 |
|
|
等待的内核函数 |
|
|
命令行 |
|
|
短名(15 字符) |
去掉表头便于脚本:ps -eo pid= ,rss=(列名后加 =)。
4.3 排序
ps aux --sort=-%cpu
ps -eo pid,rss,cmd --sort=-rss
ps -eo pid,pcpu,cmd --sort=+pid # 升序
4.4 BSD vs UNIX 选项对照(易混)
|
目的 |
BSD |
UNIX |
|---|---|---|
|
所有进程 |
|
|
|
含其他用户 |
|
|
|
指定 PID |
|
|
|
完整命令 |
|
|
|
线程 |
|
|
五、使用实战——实际案例分析
案例:ps_demo.c
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
static atomic_int g_stop;
static void on_alarm(int sig)
{
(void)sig;
atomic_store(&g_stop, 1);
}
static void arm_alarm(int sec)
{
signal(SIGALRM, on_alarm);
alarm((unsigned)sec);
}
static void usage(const char *argv0)
{
fprintf(stderr,
"Usage:\n"
" %s sleep [sec]\n"
" %s threads [n] [sec]\n"
" %s zombie [sec]\n"
" %s fork-tree [depth] [sec]\n"
" %s cpu [n] [sec]\n",
argv0, argv0, argv0, argv0, argv0);
}
static void run_sleep(int sec)
{
printf("pid=%d sleep %ds STAT~S\n", getpid(), sec);
printf(" ps -p %d -o pid,stat,wchan:30,cmd\n", getpid());
arm_alarm(sec);
while (!atomic_load(&g_stop))
pause();
}
static void *worker_sleep(void *arg)
{
int id = (int)(long)arg;
printf(" [thread %d] tid-like self, sleeping\n", id);
while (!atomic_load(&g_stop))
sleep(1);
return NULL;
}
static void run_threads(int n, int sec)
{
pthread_t *th;
int i;
if (n < 1)
n = 1;
th = calloc((size_t)n, sizeof(*th));
if (!th) {
perror("calloc");
exit(1);
}
printf("pid=%d threads=%d hold %ds watch NLWP\n", getpid(), n, sec);
printf(" ps -p %d -o pid,nlwp,stat,cmd\n", getpid());
printf(" ps -L -p %d -o pid,tid,stat,pcpu,comm\n", getpid());
arm_alarm(sec);
for (i = 0; i < n; i++) {
if (pthread_create(&th[i], NULL, worker_sleep, (void *)(long)i) != 0) {
perror("pthread_create");
exit(1);
}
}
for (i = 0; i < n; i++)
pthread_join(th[i], NULL);
free(th);
}
static void run_zombie(int sec)
{
pid_t child = fork();
if (child < 0) {
perror("fork");
exit(1);
}
if (child == 0) {
printf("child pid=%d exiting now → parent should show Z\n",
getpid());
_exit(0);
}
printf("parent pid=%d child=%d (NOT calling wait)\n", getpid(),
child);
printf(" ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/ || /ps_demo/'\n");
printf("holding %ds without waitpid...\n", sec);
arm_alarm(sec);
while (!atomic_load(&g_stop))
pause();
/* 退出前回收,避免留下僵尸给用户环境 */
(void)waitpid(child, NULL, 0);
}
static void fork_level(int depth, int sec)
{
if (depth <= 0) {
arm_alarm(sec);
while (!atomic_load(&g_stop))
pause();
_exit(0);
}
pid_t c = fork();
if (c < 0) {
perror("fork");
_exit(1);
}
if (c == 0) {
fork_level(depth - 1, sec);
_exit(0);
}
arm_alarm(sec);
while (!atomic_load(&g_stop))
pause();
(void)waitpid(c, NULL, 0);
}
static void run_fork_tree(int depth, int sec)
{
if (depth < 1)
depth = 1;
printf("pid=%d fork-tree depth=%d\n", getpid(), depth);
printf(" ps --forest -C ps_demo\n");
fork_level(depth, sec);
}
static void *worker_cpu(void *arg)
{
int id = (int)(long)arg;
volatile double x = 1.0;
printf(" [cpu thread %d] busy loop\n", id);
while (!atomic_load(&g_stop)) {
int i;
for (i = 0; i < 100000; i++)
x = x * 1.000001 + 0.000001;
}
(void)x;
return NULL;
}
static void run_cpu(int n, int sec)
{
pthread_t *th;
int i;
if (n < 1)
n = 1;
th = calloc((size_t)n, sizeof(*th));
if (!th) {
perror("calloc");
exit(1);
}
printf("pid=%d cpu threads=%d for %ds\n", getpid(), n, sec);
printf(" ps -L -p %d -o tid,pcpu,stat,comm --sort=-pcpu\n",
getpid());
arm_alarm(sec);
for (i = 0; i < n; i++) {
if (pthread_create(&th[i], NULL, worker_cpu, (void *)(long)i) != 0) {
perror("pthread_create");
exit(1);
}
}
for (i = 0; i < n; i++)
pthread_join(th[i], NULL);
free(th);
}
int main(int argc, char **argv)
{
const char *mode;
int a = 60, b = 60;
if (argc < 2) {
usage(argv[0]);
return 1;
}
mode = argv[1];
if (argc >= 3)
a = atoi(argv[2]);
if (argc >= 4)
b = atoi(argv[3]);
if (a <= 0)
a = 1;
if (b <= 0)
b = 1;
printf("ps_demo mode=%s pid=%d\n", mode, getpid());
if (!strcmp(mode, "sleep"))
run_sleep(a);
else if (!strcmp(mode, "threads"))
run_threads(a, argc >= 4 ? b : 60);
else if (!strcmp(mode, "zombie"))
run_zombie(a);
else if (!strcmp(mode, "fork-tree"))
run_fork_tree(a, argc >= 4 ? b : 60);
else if (!strcmp(mode, "cpu"))
run_cpu(a, argc >= 4 ? b : 30);
else {
usage(argv[0]);
return 1;
}
return 0;
}
Makefile
CC ?= gcc
CFLAGS ?= -Wall -Wextra -g -O0
LDFLAGS ?= -pthread
TARGET := ps_demo
.PHONY: all clean check help
all: $(TARGET)
$(TARGET): ps_demo.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
check: $(TARGET)
@echo "=== ps version ==="
@ps --version 2>&1 | head -3 || echo "ps not found (install procps)"
@echo ""
@echo "Build OK. Examples:"
@echo " ./$(TARGET) sleep 60"
@echo " ./$(TARGET) threads 4 90"
@echo " ./$(TARGET) zombie 30"
@echo " ./$(TARGET) fork-tree 3 60"
@echo " ./$(TARGET) cpu 2 30"
@echo "Observe:"
@echo " ps -C ps_demo -o pid,ppid,stat,nlwp,wchan,cmd"
@echo " ps -L -p \$$(pgrep -n ps_demo) -o pid,tid,pcpu,stat,comm"
clean:
$(RM) $(TARGET)
help:
@echo "make && make check"
@echo "./ps_demo sleep|threads|zombie|fork-tree|cpu ..."
5.1 准备工作
cd examples/ps_demo
make
./ps_demo sleep 60 # 终端 A:普通睡眠进程
# 终端 B 观测
ps -C ps_demo -o pid,ppid,stat,nlwp,wchan,cmd

5.2 案例一:确认进程在跑与命令行
./ps_demo sleep 120 &
ps -ef | grep '[p]s_demo'
ps -C ps_demo -o pid,etime,cmd
ps -p $(pgrep -n ps_demo) -o pid=,args=
期望: 看到 ./ps_demo sleep 120,STAT 多为 S。
5.3 案例二:多线程 —— NLWP 与 -L
./ps_demo threads 4 90 &
PID=$(pgrep -n ps_demo)
ps -p $PID -o pid,nlwp,stat,cmd
ps -L -p $PID -o pid,tid,psr,stat,pcpu,comm
期望: NLWP ≈ 5(1 主线程 + 4 工作线程);-L 列出多行 TID。

5.4 案例三:僵尸进程
./ps_demo zombie 30 &
# 稍等子进程退出
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/ || /ps_demo/'
pstree -p $(pgrep -n ps_demo)
期望: 有一行 STAT=Z,PPID 指向仍活着的 ps_demo(父进程故意不 wait)。
修复思路:父进程 waitpid;或杀掉父进程让 init 收养回收。

5.5 案例四:D 状态与 wchan(I/O 等待示意)
真实 D 状态依赖慢磁盘;演示可用睡眠对比 wchan:
./ps_demo sleep 60 &
ps -p $(pgrep -n ps_demo) -o pid,stat,wchan:30,cmd

5.6 案例五:父子关系与进程树
./ps_demo fork-tree 3 60 &
ps --forest -C ps_demo
ps -efH | grep '[p]s_demo'
pstree -ap $(pgrep -o ps_demo)

5.7 案例六:按资源排序定位
./ps_demo cpu 2 30 & # 忙等线程
ps -eo pid,pcpu,pmem,rss,nlwp,cmd --sort=-pcpu | head -15
ps -L -p $(pgrep -n ps_demo) -o tid,pcpu,stat,comm --sort=-pcpu
对照 top -H -p PID:top 看瞬时,ps 的 %CPU 多为 启动以来平均,短生命周期进程数字可能「怪」。

5.8 案例七:脚本监控模板
#!/bin/bash
NAME=my_server
while true; do
date
ps -C "$NAME" -o pid,stat,pcpu,rss,nlwp,etime,cmd --no-headers \
|| echo "NOT RUNNING"
sleep 2
done
六、常见问题——疑难解答
6.1 ps aux 和 ps -ef 结果差一行?
字段集合不同(%CPU/%MEM vs C 等),且时刻不同进程集合可能变化。对比时固定同一套命令。
6.2 %CPU 加起来超过 100%?
多核上正常;多线程进程可显示超过 100%(视版本与选项)。看线程用 ps -L。
6.3 为什么 %CPU 和 top 里差很多?
|
ps |
top |
|
|---|---|---|
|
含义 |
常为自启动以来平均 |
刷新间隔内瞬时 |
|
用途 |
历史偏高进程 |
当前谁在烧 CPU |
6.4 grep 出自己?
ps -ef | grep my_server # 常多出 grep 行
ps -ef | grep '[m]y_server' # 技巧:字符类避开匹配
pgrep -a my_server # 更干净
6.5 命令行被截断?
ps -efww
ps auxww
cat /proc/PID/cmdline | tr '\0' ' '; echo
comm 永远最多约 15 字符,看全路径用 args/cmdline。
6.6 容器里 ps 进程很少?
容器有独立 PID 命名空间,只能看到命名空间内进程。要看宿主机进程需进宿主机或加特权/挂载。
6.7 权限不够看别人 cmdline?
非 root 可能对部分 /proc/PID/* 受限。需要时用 sudo ps ...。
七、总结——要点回顾
-
ps 读
/proc做进程快照,适合脚本与「现在有没有 / 什么状态」。 -
先看 STAT / PPID / NLWP / CMD,再决定是僵尸、卡 I/O 还是线程爆炸。
-
实时盯资源用 top,深挖用 strace/gdb;ps 负责快速定位对象。
更多推荐

所有评论(0)