Linux 进程管理深度解析:从查看进程到进程状态
一、查看进程
在 Linux 中,进程是资源分配的基本单位。
系统中每时每刻都有大量进程在运行,如何查看它们是理解进程管理的第一步。
1.1
ps命令
ps(process status)是最常用的进程查看工具,它报告当前系统中进程的快照信息。常用组合:
ps aux
ps aux输出示例:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.1 169248 13136 ? Ss 08:00 0:02 /sbin/init root 2 0.0 0.0 0 0 ? S 08:00 0:00 [kthreadd] wxx 1234 0.5 1.2 523416 98304 ? Sl 08:05 0:15 ./myapp字段解读:
字段 含义 USER进程所属用户 PID进程 ID,全局唯一 %CPU占用 CPU 百分比 %MEM占用物理内存百分比 VSZ虚拟内存大小(KB) RSS常驻内存大小(KB),即实际占用物理内存 TTY进程所属终端, ?表示与终端无关(守护进程)STAT进程状态(后文详解) START进程启动时间 TIME累计占用 CPU 时间 COMMAND启动进程的命令 另一个常用组合:
ps -ef
ps -ef
-e显示所有进程,-f显示完整格式。与
aux的区别在于:-ef会显示 PPID(父进程 ID),这在追踪进程父子关系时非常有用。
UID PID PPID C STIME TTY TIME CMD root 1 0 0 08:00 ? 00:00:02 /sbin/init root 2 0 0 08:00 ? 00:00:00 [kthreadd] wxx 1234 1100 0 08:05 ? 00:00:15 ./myapp
1.2
top命令
ps给的是快照,而top提供的是实时动态刷新的进程视图,默认每 3 秒刷新一次。
top关键信息区域:
- 第一行:系统时间、运行时长、登录用户数、系统负载(1/5/15 分钟平均)
- 第二行:进程总数、运行中、睡眠、停止、僵尸进程数量
- 第三行:CPU 使用情况(us 用户态、sy 内核态、id 空闲)
- 第四五行:内存和交换分区使用情况
- 下方列表:按 CPU 使用率降序排列的进程
常用交互按键:
按键 功能 P按 CPU 使用率排序 M按内存使用率排序 T按累计 CPU 时间排序 k杀死指定 PID 的进程 q退出 top
1.3
/proc文件系统Linux 中一切皆文件,进程信息也不例外。****
/proc是一个虚拟文件系统,它以文件的形式暴露内核数据结构,并不占用实际磁盘空间。每个进程在
/proc下都有一个以其 PID 命名的目录:
ls /proc/1234/关键文件:
文件 内容 /proc/[pid]/status进程状态信息(名称、PID、PPID、状态、内存等) /proc/[pid]/cmdline启动命令(以 \0分隔参数)/proc/[pid]/cwd当前工作目录的符号链接 /proc/[pid]/exe可执行文件的符号链接 /proc/[pid]/maps进程地址空间映射 /proc/[pid]/fd/进程打开的文件描述符目录 查看进程状态:
cat /proc/1234/status Name: myapp State: S (sleeping) Tgid: 1234 Pid: 1234 PPid: 1100 ... VmRSS: 98304 kB本质:
ps、top等工具底层都是读取/proc文件系统来获取进程信息的。
二、通过系统调用获取进程标识符
2.1
getpid()与getppid()在用户态程序中,我们可以通过两个系统调用来获取进程 ID:
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); // 获取当前进程的 PID pid_t getppid(void); // 获取当前进程的父进程 PID(PPID)
getpid()返回当前进程的 PIDgetppid()返回父进程的 PID,如果父进程已退出,则返回1(init 进程)
2.2 代码示例
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { pid_t pid = getpid(); pid_t ppid = getppid(); printf("当前进程 PID: %d\n", pid); printf("父进程 PPID: %d\n", ppid); // 让进程暂停,方便用 ps 查看 while (1) { sleep(1); } return 0; }编译运行:
gcc -o getpid_demo getpid_demo.c ./getpid_demo输出:
当前进程 PID: 5678 父进程 PPID: 5600另开一个终端验证:
ps aux | grep getpid_demo
2.3 内核视角:PID 是如何管理的
在内核中,每个进程由
task_struct结构体描述(定义在
include/linux/sched.h)。其中与 PID 相关的字段:
struct task_struct { pid_t pid; // 进程 ID pid_t tgid; // 线程组 ID(对用户态,进程的 tgid == pid) struct task_struct *real_parent; // 真正的父进程 struct task_struct *parent; // 父进程(通常相同) // ... };
getpid()系统调用在内核中的实现非常直接------返回当前进程task_struct中的tgid字段:
// kernel/sys.c SYSCALL_DEFINE0(getpid) { return task_tgid_vnr(current); // 返回当前进程的 tgid }为什么是
tgid而不是pid?因为在 Linux 中,线程和进程在内核层面都用
task_struct表示。对多线程程序,每个线程有自己的pid(内核线程 ID),但它们共享同一个tgid(线程组 ID)。用户态看到的"进程 ID"实际上是tgid。
三、通过系统调用创建进程------
fork初识3.1
fork函数原型
#include <unistd.h> pid_t fork(void);
fork是 Unix/Linux 中最核心的进程创建机制。调用fork后,内核会:
- 为子进程分配新的
task_struct- 复制父进程的资源(页表、文件描述符表、信号处理等)
- 父子进程从
fork返回处继续执行
3.2
fork的返回值 ------ 一次调用,两次返回这是
fork最精妙也最容易困惑的地方:
返回值 含义 > 0在父进程中,返回子进程的 PID = 0在子进程中,返回 0 -1创建失败(通常是进程数达到上限或内存不足) 经典示例:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { printf("fork 之前,PID = %d\n", getpid()); pid_t ret = fork(); if (ret < 0) { perror("fork 失败"); return 1; } else if (ret == 0) { // 子进程执行路径 printf("【子进程】PID = %d, PPID = %d, fork 返回值 = %d\n", getpid(), getppid(), ret); } else { // 父进程执行路径 printf("【父进程】PID = %d, 创建的子进程 PID = %d, fork 返回值 = %d\n", getpid(), ret, ret); } return 0; }编译运行:
gcc -o fork_demo fork_demo.c ./fork_demo可能的输出:
fork 之前,PID = 6000 【父进程】PID = 6000, 创建的子进程 PID = 6001, fork 返回值 = 6001 【子进程】PID = 6001, PPID = 6000, fork 返回值 = 0注意:父子进程的执行顺序是不确定的,取决于内核调度器。可能父进程先打印,也可能子进程先打印。
3.3 写时拷贝(Copy-On-Write, COW)
早期的
fork实现会把父进程的全部地址空间完整复制一份给子进程,这非常低效------因为很多情况下子进程紧接着就调用exec替换掉整个地址空间,之前的复制全白费了。现代 Linux 采用写时拷贝优化:
fork时,子进程的页表指向父进程的物理页面(只读共享)父子进程都将这些页面标记为只读
当任一方尝试写入某个页面时,触发缺页异常(Page Fault)
内核为写入方复制一份独立的物理页面,并修改页表映射
之后双方各自拥有该页面的独立副本
fork 前:
父进程页表 ──→ 物理页面 A(可读写)fork 后(COW):
父进程页表 ──→ 物理页面 A(只读) ←── 子进程页表父进程写入页面 A 时:
父进程页表 ──→ 物理页面 A'(可读写,A 的副本)
子进程页表 ──→ 物理页面 A(只读)COW 的核心优势:
只有真正发生写入时才复制,
fork之后立即exec的场景几乎零拷贝开销。
3.4
fork之后父子进程共享什么?
资源 是否共享 说明 代码段(text) 共享(只读) 不可修改,天然共享 数据段/堆/栈 写时拷贝 COW 机制 文件描述符表 共享(引用计数) 父子共享同一文件偏移 信号处理方式 复制 子进程继承父进程的信号处理 进程 ID 不同 子进程获得新 PID 挂起的信号 不继承 子进程的挂起信号集被清空 文件描述符共享的坑:
#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("test.txt", O_WRONLY | O_CREAT, 0644); pid_t pid = fork(); if (pid == 0) { // 子进程写入 write(fd, "child\n", 6); } else { // 父进程写入 write(fd, "parent\n", 7); } close(fd); return 0; }由于父子进程共享文件偏移量,两次写入不会互相覆盖,而是依次追加。这也是为什么
fork后需要注意文件描述符的管理。
四、进程状态
4.1 Linux 内核源码怎么说
进程状态定义在内核头文件
include/linux/sched.h中,通过task_struct的__state字段表示:
struct task_struct { volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ // ... };具体的状态标志定义在
include/linux/sched.h:
/* Used in tsk->state: */ #define TASK_RUNNING 0x0000 #define TASK_INTERRUPTIBLE 0x0001 #define TASK_UNINTERRUPTIBLE 0x0002 #define TASK_STOPPED 0x0004 #define TASK_TRACED 0x0008 /* Used in tsk->exit_state: */ #define EXIT_ZOMBIE 0x0010 #define EXIT_DEAD 0x0020 /* Used in tsk->state again: */ #define TASK_PARKED 0x0040 #define TASK_DEAD 0x0080 #define TASK_WAKEKILL 0x0100 #define TASK_WAKING 0x0200 #define TASK_NOLOAD 0x0400 #define TASK_NEW 0x0800 #define TASK_STATE_MAX 0x1000注意:
state字段用于运行中的状态,exit_state字段专门用于退出过程中的状态(僵尸、死亡)。
4.2 七大进程状态详解
状态 字符 内核宏 核心含义 运行态 RTASK_RUNNING正在运行或在就绪队列等待 CPU 可中断睡眠 STASK_INTERRUPTIBLE等待事件,可被信号唤醒 不可中断睡眠 DTASK_UNINTERRUPTIBLE等待 I/O,不可被信号唤醒 停止态 TTASK_STOPPED被信号暂停,等待 SIGCONT 恢复 追踪态 tTASK_TRACED被调试器暂停(gdb 断点命中) 僵尸态 ZEXIT_ZOMBIE已退出,父进程未回收资源 死亡态 XEXIT_DEAD最终状态,task_struct 即将释放 ① R --- TASK_RUNNING(运行态/就绪态)
#define TASK_RUNNING 0x0000
- 进程正在 CPU 上执行,或者在运行队列中等待调度
- 这是唯一可以在 CPU 上运行的状态
ps中显示为R注意:Linux 中没有单独的"就绪态",就绪和运行统一称为
TASK_RUNNING,区别仅在于是否正在被 CPU 执行。
② S --- TASK_INTERRUPTIBLE(可中断睡眠态)
#define TASK_INTERRUPTIBLE 0x0001
- 进程在等待某事件发生(如等待终端输入、等待网络数据、等待锁)
- 可以被信号唤醒(如
Ctrl+C发送的 SIGINT)ps中显示为S,这是最常见的状态典型场景:
sleep(10)、read()阻塞等待、wait()等待子进程。
③ D --- TASK_UNINTERRUPTIBLE(不可中断睡眠态)
#define TASK_UNINTERRUPTIBLE 0x0002
- 进程在等待不可中断的事件(通常是 I/O 操作,如磁盘读写)
- 不能被信号唤醒,只能由等待的事件完成来唤醒
ps中显示为D为什么需要不可中断?
假设进程正在执行磁盘
read系统调用,内核已向磁盘发出 I/O 请求。如果此时允许信号中断,进程可能在 I/O 完成前就退出,导致内核态的 I/O 操作无法正常完成,可能造成数据不一致。因此 I/O 等待期间必须设为不可中断。排查系统负载高时,如果看到大量
D状态进程,通常意味着磁盘 I/O 存在瓶颈。
④ T --- TASK_STOPPED(停止态)
#define TASK_STOPPED 0x0004
- 进程被暂停执行,收到
SIGSTOP或SIGTSTP(Ctrl+Z)信号- 可以通过
SIGCONT信号恢复运行ps中显示为T典型场景:在终端中按
Ctrl+Z将前台进程挂到后台。
sleep 100 # 按 Ctrl+Z # [1]+ Stopped sleep 100 jobs # 查看停止的作业 fg %1 # 恢复到前台运行 bg %1 # 在后台继续运行
⑤ t --- TASK_TRACED(追踪态)
#define TASK_TRACED 0x0008
- 进程被调试器追踪(如
gdb附加到进程)- 当进程被断点命中时进入此状态
ps中显示为t(小写)与
T(STOPPED)的区别:TRACED是被调试器暂停,只能由调试器通过ptrace恢复。
⑥ Z --- EXIT_ZOMBIE(僵尸态)
#define EXIT_ZOMBIE 0x0010
- 进程已经执行结束,但父进程尚未调用
wait()/waitpid()回收其资源- 进程的
task_struct仍然保留(占用一个 PID 槽位),但用户态资源已释放ps中显示为Z,通常标注为<defunct>僵尸进程是怎么产生的:
#include <stdio.h> #include <unistd.h> int main() { pid_t pid = fork(); if (pid == 0) { // 子进程:立刻退出 printf("子进程退出,PID=%d\n", getpid()); _exit(0); } else { // 父进程:不调用 wait,一直运行 printf("父进程运行中,不回收子进程\n"); while (1) sleep(1); } return 0; }运行后查看:
ps aux | grep Z # wxx 6500 0.0 0.0 0 0 ? Z 10:00 0:00 [zombie_demo] <defunct>如何避免僵尸进程:
父进程调用
wait()或waitpid()回收父进程注册
SIGCHLD信号处理函数,在其中调用waitpid()父进程先退出,子进程被
init(PID 1)收养,init 会自动回收// 方法2:信号处理回收
void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}int main() {
signal(SIGCHLD, sigchld_handler);
// ...
}
⑦ X --- EXIT_DEAD(死亡态)
#define EXIT_DEAD 0x0020
- 进程的最终状态,
task_struct即将被释放- 这个状态非常短暂,
ps几乎捕捉不到ps中显示为X
4.3 进程状态转换图
fork() 新建 ──────────────→ R (运行/就绪) │ ┌───────────┼───────────┐ │ │ │ 等待事件 时间片到 收到信号 │ │ │ ▼ ▼ ▼ S (可中断睡眠) R T (停止) D (不可中断睡眠) │ │ │ 事件完成/信号 SIGCONT │ │ └───────────┬───────────┘ │ ▼ R │ exit() / 收到终止信号 │ ▼ Z (僵尸) ──→ wait() 回收 ──→ X (死亡)
五、进程状态查看
5.1
ps中的 STAT 列在
ps aux的输出中,STAT列显示进程状态。除了基本状态字符外,还可能有附加字符:
字符 含义 R运行/就绪 S可中断睡眠 D不可中断睡眠 T停止 t追踪停止 Z僵尸 X死亡 s会话首进程(session leader) l多线程进程 +位于前台进程组 <高优先级(nice 值为负) N低优先级(nice 值为正) L有页面锁定在内存中 例如
Ssl+表示:可中断睡眠(S)+ 会话首进程(s)+ 多线程(l)+ 前台进程组(+)。
5.2 实用查看技巧
查看指定进程的状态:
ps -o pid,stat,cmd -p 1234只看僵尸进程:
ps aux | awk '$8 ~ /Z/'查看进程状态分布统计:
ps aux | awk '{print $8}' | sort | uniq -c | sort -rn输出示例:
42 S 3 R 1 Ss 1 S+ 1 Z用
top查看状态:在
top中按f进入字段选择,添加S(Process Status)列即可实时查看每个进程的状态。查看进程的完整状态信息:
cat /proc/1234/status | grep State # State: S (sleeping)
总结
主题 核心要点 查看进程 ps aux/ps -ef快照,top实时,底层读/proc获取 PID getpid()返回当前进程 PID,getppid()返回父进程 PIDfork创建进程一次调用两次返回;父返回子 PID,子返回 0;COW 写时拷贝 进程状态 R 运行、S 可中断睡眠、D 不可中断睡眠、T 停止、t 追踪、Z 僵尸、X 死亡 状态查看 ps的 STAT 列,/proc/[pid]/status,注意附加字符含义理解进程状态是深入 Linux 系统编程的基石。从
fork创建进程,到进程在各个状态之间流转,再到最终被回收,整个生命周期都围绕着task_struct和内核调度器展开。掌握这些原理,才能在实际开发中精准定位进程卡死、僵尸进程、负载过高等问题。


