Linux 软件编程学习笔记(五):从诞生到消亡,一文读懂进程管理
一、进程的基本概念
1.1 什么是进程?
进程 是程序的一次执行过程,它是系统进行资源分配(如 CPU 时间、内存、文件句柄等)的最小单位。程序是静态的、存放在磁盘上的可执行文件,而进程是动态的,拥有自己的生命周期。
1.2 进程标识符(PID)
每个进程在系统中都有一个唯一的整数编号,称为 PID(Process ID)。父进程的 PID 称为 PPID。系统中第一个进程是 init(或 systemd),PID 为 1,它是所有进程的祖先。
1.3 进程控制块(PCB)
操作系统内核为每个进程维护一个数据结构,称为 进程控制块(Process Control Block, PCB),在 Linux 中对应 task_struct 结构体。PCB 包含进程状态、PID、PPID、程序计数器、寄存器、文件描述符表、内存映射等信息,是内核管理进程的核心依据。
二、进程的状态
进程在其生命周期中会经历多种状态,理解这些状态对于调试和性能优化至关重要。
| 状态 | 说明 |
|---|---|
| 运行(Running) | 进程正在 CPU 上执行,或处于就绪队列中等待被调度(实际上可分为 运行态 和 就绪态,但在 Linux 的 ps 中统称为 R)。 |
| 可中断睡眠(Interruptible Sleep) | 进程正在等待某个事件(如 I/O 完成),可以被信号唤醒。对应 S 状态。 |
| 不可中断睡眠(Uninterruptible Sleep) | 进程正在等待硬件条件(如磁盘 I/O),不会被信号中断。对应 D 状态,常见于等待磁盘 I/O 时。 |
| 停止(Stopped) | 进程被暂停,通常由 SIGSTOP 或 SIGTSTP 信号引起。对应 T 状态。 |
| 僵尸(Zombie) | 进程已经终止,但其 PCB 仍保留,等待父进程回收。对应 Z 状态。如果父进程不及时回收,会形成大量僵尸进程,消耗内核资源。 |
状态转换图:
新建 → 就绪 → 运行 → 终止
↑ ↓ ↓
└── 阻塞 ──→ 退出等待
其中阻塞可细分为可中断/不可中断睡眠。
三、进程相关命令
| 命令 | 功能 |
|---|---|
ps |
显示当前终端中的进程快照。常用参数:ps aux(显示所有进程)、ps -ef(标准格式)、ps -l(显示更详细信息)。 |
top / htop |
动态查看进程资源占用(CPU、内存),支持交互操作。 |
pstree |
以树形结构显示进程间的父子关系。 |
kill |
发送信号给进程,默认 SIGTERM(终止),常用 kill -9(强制杀死)、kill -STOP(暂停)等。 |
jobs |
查看当前 shell 后台作业。 |
bg / fg |
将作业切换到后台/前台运行。 |
nice / renice |
设置或修改进程的优先级(nice 值,范围 -20~19,值越小优先级越高)。 |
pidof |
根据进程名查询 PID。 |
pgrep |
按条件查找进程 PID。 |
示例:
# 查看所有进程的 PID、PPID、状态、命令行
ps aux
# 动态查看 CPU 占用最高的进程
top -o %CPU
# 强制终止 PID 为 1234 的进程
kill -9 1234
# 以树形图显示进程关系
pstree -p
四、进程的创建
Linux 中创建进程的主要系统调用是 fork()、vfork() 和 clone()。我们最常用的是 fork()。
4.1 fork() 函数
- 原型:
pid_t fork(void); - 返回值:在父进程中返回子进程的 PID;在子进程中返回 0;出错返回 -1。
- 行为:
fork()会复制当前进程(父进程)的数据段、堆栈、代码段等,并采用 写时复制(COW, Copy-On-Write) 技术优化性能,即子进程和父进程共享物理内存,只有写操作发生时才会复制页面。 - 父子进程执行顺序:不确定,由调度器决定。
4.2 vfork() 和 clone()
vfork()是早期优化版本,子进程会阻塞父进程直到子进程调用exec或exit,且父子共享地址空间,现已不推荐使用。clone()是更底层的系统调用,用于创建线程(通过指定 CLONE_VM 等标志),也支持自定义创建行为。
4.3 使用 fork() 创建多个子进程的典型代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
int i;
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) { // 子进程
printf("子进程 %d: PID=%d, PPID=%d\n", i+1, getpid(), getppid());
return 0; // 子进程立即退出,防止继续循环
}
// 父进程继续循环
}
// 父进程等待所有子进程结束
for (i = 0; i < 5; i++) {
wait(NULL);
}
printf("父进程 PID=%d,所有子进程已回收\n", getpid());
return 0;
}
注意:子进程必须在 fork() 后立刻 return 或 exit,否则它会继续执行循环,导致更多子进程产生(形成进程树)。上述代码确保只创建 5 个直系子进程。
五、进程的调度
Linux 采用基于 完全公平调度算法(CFS, Completely Fair Scheduler) 的调度器,从内核 2.6.23 开始成为默认调度策略。
5.1 调度策略
- SCHED_OTHER(普通进程):CFS 默认策略,根据进程的 nice 值 和 虚拟运行时间 动态分配 CPU 时间片。
- SCHED_FIFO 和 SCHED_RR:实时调度策略,优先级高于普通进程。FIFO 为先进先出,RR 为时间片轮转。
- 可通过
sched_setscheduler()或chrt命令设置调度策略。
5.2 优先级与 nice 值
- 普通进程的优先级由 nice 值调节,范围 -20(最高)~ 19(最低),默认 0。
- 设置 nice 值:
nice -n 10 ./program(降低优先级);renice -5 -p 1234(修改已运行进程的 nice 值)。 - 实时进程优先级范围 1~99,数值越大优先级越高。
5.3 调度触发
- 时钟中断(例如 1000Hz)触发调度器检查是否需要抢占。
- 进程主动让出 CPU(如调用
sleep()、阻塞 I/O)。 - 进程用完时间片。
六、进程相关函数接口
| 函数 | 功能 |
|---|---|
pid_t getpid(void) |
获取当前进程 PID。 |
pid_t getppid(void) |
获取父进程 PID。 |
pid_t fork(void) |
创建子进程。 |
pid_t vfork(void) |
创建子进程并阻塞父进程(不推荐)。 |
unsigned int sleep(unsigned int seconds) |
当前进程睡眠指定秒数。 |
int pause(void) |
等待信号到来。 |
void exit(int status) |
终止当前进程,返回状态值,刷新缓冲区。 |
int execvp(const char *file, char *const argv[]) |
加载并执行新程序(替换当前进程映像)。 |
pid_t wait(int *status) |
等待任一子进程终止,返回子进程 PID,并获取退出状态。 |
pid_t waitpid(pid_t pid, int *status, int options) |
等待指定子进程,支持非阻塞等选项。 |
6.1 exec 系列函数
exec 族(如 execl、execv、execle 等)用于在当前进程上下文中执行一个新的程序,不会创建新进程,而是替换当前进程的代码段、数据段等。调用成功后,后续代码不再执行。典型用法:
if (fork() == 0) {
// 子进程
execlp("ls", "ls", "-l", NULL);
perror("execlp"); // 只有出错才会执行到这里
exit(1);
}
6.2 wait 和 waitpid
父进程通过 wait 回收子进程资源,获取退出状态,防止僵尸进程。
wait(&status):阻塞等待任意子进程结束。waitpid(pid, &status, WNOHANG):非阻塞等待指定子进程。
七、进程的消亡
7.1 正常终止
进程正常结束的方式:
- 在
main函数中执行return。 - 调用
exit()函数(标准库函数,会刷新缓冲区)。 - 调用
_exit()或_Exit()(系统调用,不刷新缓冲区)。
注意:return 和 exit 都会执行 atexit 注册的清理函数,而 _exit 则不会。
7.2 异常终止
- 收到不可捕捉的信号,如
SIGSEGV(段错误)、SIGABRT(abort 调用)。 - 调用
abort()函数。
7.3 僵尸进程(Zombie)
进程终止后,它的 PCB 并不会立即释放,而是保留退出状态和资源使用统计等信息,等待父进程调用 wait 或 waitpid 来读取。这期间进程处于 僵尸状态(Z状态)。如果父进程不回收,僵尸进程会一直存在,虽然不占用内存,但占用 PID 名额,大量僵尸会耗尽进程表项。
7.4 孤儿进程(Orphan)
如果父进程先于子进程结束,那么子进程成为孤儿进程,系统会将其收养,并挂载到 PID=1 的 init 进程下,由 init 负责回收。
7.5 避免僵尸进程的方法
- 父进程主动调用
wait()或waitpid()。 - 使用信号机制:父进程注册
SIGCHLD信号处理函数,在信号处理函数中调用waitpid回收子进程。 - 显式忽略
SIGCHLD信号(signal(SIGCHLD, SIG_IGN)),这样内核会自动回收子进程资源,但该方法不是所有系统都支持。 - 使用双重 fork:即父进程先 fork 一个子进程,该子进程再 fork 实际工作进程后立即退出,这样实际工作进程的父进程变为
init,由init自动回收。
示例:使用 SIGCHLD 回收
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
void sigchld_handler(int sig) {
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("回收子进程 %d\n", pid);
}
}
int main() {
signal(SIGCHLD, sigchld_handler);
// ... 创建子进程
while(1) pause(); // 让父进程不退出
return 0;
}
八、总结
本文我们从进程的基本概念出发,详细介绍了进程的状态、常用管理命令、创建与调度、核心 API 以及进程的消亡过程。掌握这些知识,不仅有助于编写高质量的多进程程序,也能在系统出现异常时快速定位问题。
- 进程是系统资源分配和调度的基本单位,拥有独立的地址空间和资源。
fork()+exec()是创建新程序的经典组合,wait()是回收子进程、避免僵尸的关键。- 调度器 CFS 保证了公平性,而实时调度则满足高优先级任务的需求。
- 僵尸进程 是常见问题,务必在父进程中及时回收。
更多推荐

所有评论(0)