目录

一、前言——ps 工具简介

1.1 什么是 ps

1.2 与相关工具对比

二、使用场景——何时使用 ps

1、确认 C/C++ 服务是否在跑、有几个实例

2、查僵尸进程(Zombie)

3、多线程程序:列出所有线程

4、进程卡住:看状态 D / S / R

5、脚本里取 PID / 监控

6、看完整命令行与环境相关信息

7、按 CPU / 内存排序找「凶手」

三、核心原理——工作原理深度解析

3.1 数据从哪来

3.2 STAT 字段详解(读懂第二列)

3.3 进程 vs 线程在 ps 里长什么样

3.4 僵尸进程机制(Z)

3.2 STAT 字段详解(读懂第二列)

3.3 进程 vs 线程在 ps 里长什么样

3.4 僵尸进程机制(Z)

四、命令参数——常用选项详解

4.1 高频「整句」命令

4.2 自定义输出列 -o

4.3 排序

4.4 BSD vs UNIX 选项对照(易混)

五、使用实战——实际案例分析

5.1 准备工作

5.2 案例一:确认进程在跑与命令行

5.3 案例二:多线程 —— NLWP 与 -L

5.4 案例三:僵尸进程

5.5 案例四:D 状态与 wchan(I/O 等待示意)

5.6 案例五:父子关系与进程树

5.7 案例六:按资源排序定位

5.8 案例七:脚本监控模板

六、常见问题——疑难解答

6.1 ps aux 和 ps -ef 结果差一行?

6.2 %CPU 加起来超过 100%?

6.3 为什么 %CPU 和 top 里差很多?

6.4 grep 出自己?

6.5 命令行被截断?

6.6 容器里 ps 进程很少?

6.7 权限不够看别人 cmdline?

七、总结——要点回顾


一、前言——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/'

STATZ → 僵尸;看 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

含义

常见原因

R

Running / runnable

在跑或待调度

S

Interruptible sleep

等事件(多数正常)

D

Uninterruptible sleep

常卡在磁盘 I/O

Z

Zombie

父进程未 wait

T

Stopped

被信号停下(Ctrl-Z / SIGSTOP)

wchan 显示内核等待点(如 futex_waitpoll_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 字段详解(读懂第二列)

psSTAT/S 常由 主状态字母 + 修饰符 组成,例如 SslR+

字符

含义

R/S/D/Z/T/t/X

主状态(见上表)

<

高优先级(nice 较小)

N

低优先级(nice 较大)

L

有页面锁在内存(mlock)

s

session leader

l

多线程(用 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_createNLWP 增加;主线程 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 字段详解(读懂第二列)

psSTAT/S 常由 主状态字母 + 修饰符 组成,例如 SslR+

字符

含义

R/S/D/Z/T/t/X

主状态(见上表)

<

高优先级(nice 较小)

N

低优先级(nice 较大)

L

有页面锁在内存(mlock)

s

session leader

l

多线程(用 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_createNLWP 增加;主线程 TID 通常等于 PID。

3.4 僵尸进程机制(Z)

子进程 exit
  → 内核保留 task_struct 最小信息(供父进程 wait 取退出码)
  → ps 显示 Z(zombie)
  → 父进程 wait/waitpid 后,条目消失
  → 若父进程永远不 wait:僵尸一直在,占 PID 号

父进程若已退出,僵尸会被 init/systemd(PID 1) 收养并回收。

四、命令参数——常用选项详解

4.1 高频「整句」命令

命令

含义

ps aux

所有用户进程,BSD 详细列

ps -ef

所有进程,UID/PID/PPID/CMD

ps -efww

同上,命令行不截断

ps -u USER

某用户的进程

ps -C name

按可执行名选中

ps -p PID

指定 PID

ps -L / ps -T

显示线程

ps --forest / ps -efH / ps f

树状父子关系

4.2 自定义输出列 -o

ps -eo pid,ppid,user,stat,pcpu,pmem,rss,nlwp,wchan:20,cmd

常用列名:

含义

pid / ppid

进程号 / 父进程号

user / uid

用户

stat / s

状态

pcpu / %cpu

CPU 占比(自启动以来平均,非瞬时)

pmem / %mem

物理内存占比

rss / rsz

常驻内存(KB)

vsz

虚拟地址空间大小

nlwp

线程数

lstart / etime

启动时间 / 已运行时长

wchan

等待的内核函数

cmd / args

命令行

comm

短名(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

所有进程

ps ax

ps -e

含其他用户

ps aux

ps -ef

指定 PID

ps -p 123ps p 123

ps -p 123

完整命令

ps auxww

ps -efww

线程

ps -L / ps m

ps -L / ps -T

五、使用实战——实际案例分析

案例: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 120STAT 多为 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=ZPPID 指向仍活着的 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 auxps -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 ...

七、总结——要点回顾

  1. ps 读 /proc 做进程快照,适合脚本与「现在有没有 / 什么状态」。

  2. 先看 STAT / PPID / NLWP / CMD,再决定是僵尸、卡 I/O 还是线程爆炸。

  3. 实时盯资源用 top,深挖用 strace/gdb;ps 负责快速定位对象。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐