Linux 进程管理
遵循统一结构:函数原型 → 核心参数/说明 → 完整代码示例 → 返回值解析,覆盖进程创建、替换、退出、回收、状态解析、进程组管理全知识点,兼顾理论与实战。
一、进程基础核心概念
| 进程 | 程序的一次执行实例,是操作系统资源分配和调度的基本单位 | 唯一 PID、独立地址空间、动态生命周期 |
| PCB | 进程控制块,内核用 task_struct 结构体实现,是进程的“身份证” | 包含 PID/PPID/PGID、进程状态、内存映射、文件描述符表、信号处理规则 |
| 进程组 | 一组关联进程的集合,共享同一个 PGID(进程组 ID) | 组长进程 PID = PGID,子进程默认继承父进程的进程组,终端信号(如 Ctrl+C)作用于整个进程组 |
| 写时复制 | fork 后的核心机制,父子进程共享代码段和数据段,仅当某一进程修改数据时才复制对应内存页 | 大幅减少 fork 开销,提升进程创建效率 |
二、进程标识:获取 PID/PPID
1. getpid – 获取当前进程 PID
函数原型
#include <unistd.h>
pid_t getpid(void);
核心参数/说明
- 无参数,直接调用即可。
- 作用:返回当前进程的唯一进程 ID,常用于进程身份标识、日志打印。
完整代码示例
#include <stdio.h>
#include <unistd.h>
int main() {
// 获取当前进程 PID
pid_t pid = getpid();
printf("当前进程 PID:%d\\n", pid);
return 0;
}
返回值解析
- 成功:返回非负整数(当前进程的 PID)。
- 失败:无失败场景,内核始终能返回有效 PID。
2. getppid – 获取父进程 PID
函数原型
#include <unistd.h>
pid_t getppid(void);
核心参数/说明
- 无参数,直接调用即可。
- 作用:返回当前进程的父进程 PID,用于判断进程亲缘关系。
完整代码示例
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t my_pid = getpid();
pid_t parent_pid = getppid();
printf("当前进程 PID:%d,父进程 PID:%d\\n", my_pid, parent_pid);
return 0;
}
返回值解析
- 成功:返回非负整数(父进程的 PID)。
- 失败:无失败场景。
三、进程创建:fork / vfork(核心系统调用)
1. fork – 标准子进程创建
函数原型
#include <unistd.h>
pid_t fork(void);
核心参数/说明
- 无参数,调用后内核完成 3 件事:1. 复制父进程 PCB;2. 分配新 PID;3. 采用写时复制机制共享内存。
- 关键特性:父子进程代码段共享,数据段/堆/栈独立,执行顺序由 CPU 调度器决定(不可预测)。
完整代码示例(含写时复制验证)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int global_val = 100; // 全局变量,验证写时复制
pid_t pid = fork(); // 分裂为父子进程
if (pid > 0) {
// 父进程分支:等待子进程执行完毕,再修改全局变量
wait(NULL); // 阻塞等待子进程退出,避免僵尸进程
global_val += 10;
printf("[父进程] PID:%d,global_val:%d\\n", getpid(), global_val);
} else if (pid == 0) {
// 子进程分支:独立修改全局变量
global_val += 20;
printf("[子进程] PID:%d,父进程 PID:%d,global_val:%d\\n",
getpid(), getppid(), global_val);
return 0; // 子进程正常退出
} else {
// fork 创建失败
perror("fork 失败");
return 1;
}
return 0;
}
返回值解析
| > 0 | 父进程 | 返回新创建的子进程 PID |
| = 0 | 子进程 | 表示当前为子进程,无返回值意义 |
| -1 | 父进程 | 创建失败(如系统进程数达到上限、内存不足),同时设置 errno |
2. vfork – 高效进程创建(仅用于 exec 场景)
函数原型
#include <unistd.h>
pid_t vfork(void);
核心参数/说明
- 无参数,与 fork 的核心区别:
- 不采用写时复制,直接共享父进程地址空间(子进程修改数据会直接影响父进程);
- 强制子进程先执行,父进程阻塞,直到子进程调用 exec 或 _exit;
- 仅适用于“子进程创建后立即执行新程序”的场景,效率远高于 fork。
完整代码示例(配合 exec 使用)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = vfork();
if (pid > 0) {
// 父进程:子进程执行 exec 后才会唤醒
int status;
wait(&status);
printf("[父进程] 子进程执行完毕\\n");
} else if (pid == 0) {
// 子进程:必须立即调用 exec,禁止修改父进程数据
execlp("ls", "ls", "-l", NULL);
// 若 exec 失败,必须用 _exit 退出,禁止用 exit(避免刷新父进程缓冲区)
_exit(1);
} else {
perror("vfork 失败");
return 1;
}
return 0;
}
返回值解析
- 与 fork 完全一致:父进程返回子进程 PID,子进程返回 0,失败返回 -1。
四、进程替换:exec 函数族(核心工具)
总述
exec 函数族的核心作用:在当前进程地址空间中加载并执行新程序,替换原进程的代码段、数据段、堆、栈,PID 保持不变。
- 命名规则:l(参数列表)、v(参数数组)、p(自动搜索 PATH)、e(自定义环境变量)。
- 核心特性:执行成功后,原进程后续代码不再执行;仅当执行失败时,才会返回并执行后续错误处理代码。
1. execlp – 最常用的进程替换函数(自动搜 PATH)
函数原型
#include <unistd.h>
int execlp(const char *file, const char *arg, ... /*, (char *)NULL */);
核心参数/说明
| file | 要执行的程序名(如 “ls”、“ps”),内核会在 PATH 环境变量中自动查找 |
| arg | 程序的命令行参数,第一个参数必须等于程序名,后续为具体参数 |
| … | 可变参数,以 (char *)NULL 结尾,标识参数列表结束 |
完整代码示例(子进程执行系统命令)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid > 0) {
// 父进程:回收子进程
int status;
waitpid(pid, &status, 0);
} else if (pid == 0) {
// 子进程:替换为 ls -l 命令
printf("子进程执行 ls -l 命令:\\n");
execlp("ls", "ls", "-l", "–color=auto", NULL);
// 若执行到这里,说明 execlp 失败
perror("execlp 失败");
_exit(1);
} else {
perror("fork 失败");
return 1;
}
return 0;
}
返回值解析
- 成功:无返回值(原进程代码已被替换,无法执行后续代码)。
- 失败:返回 -1,同时设置 errno(如程序不存在、权限不足)。
2. execvp – 数组传参的进程替换(自动搜 PATH)
函数原型
#include <unistd.h>
int execvp(const char *file, char *const argv[]);
核心参数/说明
| file | 要执行的程序名,同 execlp |
| argv | 命令行参数数组,最后一个元素必须为 NULL |
完整代码示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 定义参数数组
char *const argv[] = {"ps", "aux", NULL};
// 执行 ps aux 命令
execvp("ps", argv);
perror("execvp 失败");
_exit(1);
} else if (pid > 0) {
wait(NULL);
} else {
perror("fork 失败");
return 1;
}
return 0;
}
返回值解析
- 与 execlp 完全一致:成功无返回,失败返回 -1。
3. execl / execve – 绝对路径传参
函数原型
// 列表传参,需绝对路径
#include <unistd.h>
int execl(const char *path, const char *arg, ... /*, (char *)NULL */);
// 底层系统调用,自定义环境变量,需绝对路径
int execve(const char *path, char *const argv[], char *const envp[]);
核心参数/说明
- path:程序的绝对路径(如 “/bin/ls”),不再自动搜索 PATH。
- envp:自定义环境变量数组(如 char *envp[] = {"PATH=/bin", "USER=root", NULL};)。
完整代码示例(execl)
// 执行 /bin/date 命令,输出当前时间
execl("/bin/date", "date", NULL);
返回值解析
- 与其他 exec 函数一致:成功无返回,失败返回 -1。
五、进程退出:return / exit / _exit(生命周期终止)
1. 核心对比表
| return | return int status; | 语言关键字 | 刷新缓冲区 | main 函数正常退出 |
| exit | #include <stdlib.h>void exit(int status); | 标准库函数 | 刷新缓冲区、执行 atexit 注册的清理函数、关闭文件描述符 | 进程通用正常退出 |
| _exit | #include <unistd.h>void _exit(int status); | 系统调用 | 不刷新缓冲区、直接终止 | 子进程异常退出、vfork 子进程退出 |
2. 关键说明
- 退出码 status:仅低 8 位有效(取值范围 0-255),父进程通过 WEXITSTATUS 宏获取。
- 退出码约定:0 表示正常退出,非 0 表示异常退出(可自定义含义)。
3. 完整代码示例(对比 exit 与 _exit)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程 1:用 exit 退出,刷新缓冲区
printf("子进程 1(exit):输出内容"); // 无换行,缓冲区未刷新
exit(0);
}
pid = fork();
if (pid == 0) {
// 子进程 2:用 _exit 退出,不刷新缓冲区
printf("子进程 2(_exit):输出内容"); // 无换行,缓冲区不会刷新
_exit(0);
}
// 父进程回收两个子进程
wait(NULL);
wait(NULL);
printf("\\n父进程执行完毕\\n");
return 0;
}
返回值解析
- 三者均无返回值,直接终止进程,将退出状态传递给父进程。
六、进程回收:wait / waitpid(解决僵尸进程核心)
1. wait – 基础阻塞回收
函数原型
#include <sys/wait.h>
pid_t wait(int *status);
核心参数/说明
| status | 输出参数,存储子进程的退出状态;若传入 NULL,表示不关心退出状态 |
| 核心特性 | 1. 阻塞父进程,直到任意一个子进程退出;2. 自动回收子进程的 PCB 资源,避免僵尸进程;3. 一次调用只能回收一个子进程 |
完整代码示例(配合退出状态宏)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程正常退出,退出码 9
printf("子进程 PID:%d,即将退出\\n", getpid());
return 9;
} else if (pid > 0) {
int status;
// 阻塞等待子进程退出
pid_t wpid = wait(&status);
printf("回收的子进程 PID:%d\\n", wpid);
// 解析退出状态
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码:%d\\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程被信号杀死,信号编号:%d\\n", WTERMSIG(status));
}
} else {
perror("fork 失败");
return 1;
}
return 0;
}
返回值解析
- 成功:返回被回收的子进程 PID。
- 失败:返回 -1(如无未回收的子进程、被信号中断),同时设置 errno。
2. waitpid – 精准可控回收(核心重点)
函数原型
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
核心参数/说明
| pid | -1:回收任意一个子进程(最常用)>0:回收指定 PID 的子进程0:回收同进程组的任一子进程<0:回收进程组 ID 为 ` |
| status | 同 wait,存储退出状态,可传 NULL |
| options | 0:阻塞模式(默认)WNOHANG:非阻塞模式(无子进程退出则立即返回)WUNTRACED:报告被停止的子进程状态WCONTINUED:报告被恢复运行的子进程状态 |
完整代码示例(3 种核心场景)
场景 1:非阻塞循环回收所有子进程(解决多子进程僵尸问题)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
// 创建 3 个子进程
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
printf("子进程 %d(PID:%d)运行中\\n", i+1, getpid());
sleep(i+1); // 模拟不同运行时长
return i+1; // 不同退出码
} else if (pid < 0) {
perror("fork 失败");
return 1;
}
}
// 父进程:非阻塞循环回收所有子进程
int status;
pid_t wpid;
while ((wpid = waitpid(–1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
printf("回收子进程 PID:%d,退出码:%d\\n", wpid, WEXITSTATUS(status));
}
}
printf("所有子进程已回收完毕\\n");
return 0;
}
场景 2:回收指定 PID 的子进程
// 假设子进程 PID 为 12345
waitpid(12345, &status, 0);
场景 3:回收同进程组的子进程
// 回收当前进程所在进程组的任一子进程
waitpid(0, &status, 0);
返回值解析(核心)
| > 0 | 成功回收的子进程 PID |
| 0 | 非阻塞模式(WNOHANG)下,无子进程退出,所有子进程仍在运行 |
| -1 | 回收失败(如无未回收的子进程、参数错误),同时设置 errno |
七、退出状态解析:核心宏
基于 wait/waitpid 的 status 参数,内核提供专用宏解析退出原因,仅在对应判断宏为真时,才能调用取值宏。
函数原型(直接调用)
// 正常退出相关
int WIFEXITED(int status);
int WEXITSTATUS(int status);
// 信号终止相关
int WIFSIGNALED(int status);
int WTERMSIG(int status);
// 进程停止/恢复相关(扩展)
int WIFSTOPPED(int status);
int WSTOPSIG(int status);
int WIFCONTINUED(int status);
核心参数/说明
| WIFEXITED | 判断子进程是否正常退出(return/exit/_exit) | 正常退出场景 |
| WEXITSTATUS | 获取正常退出的退出码(仅低 8 位) | WIFEXITED 为真时 |
| WIFSIGNALED | 判断子进程是否被信号杀死(如 kill -9、段错误) | 异常终止场景 |
| WTERMSIG | 获取杀死子进程的信号编号 | WIFSIGNALED 为真时 |
| WIFSTOPPED | 判断子进程是否被信号停止(如 Ctrl+Z) | 进程暂停场景 |
| WSTOPSIG | 获取使子进程停止的信号编号 | WIFSTOPPED 为真时 |
| WIFCONTINUED | 判断子进程是否被信号恢复(如 fg 命令) | 进程恢复场景 |
完整代码示例(含异常终止模拟)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:模拟被信号杀死(空指针解引用,触发段错误 SIGSEGV,信号编号 11)
int *p = NULL;
*p = 100; // 非法访问,触发信号
_exit(0); // 不会执行到
} else if (pid > 0) {
int status;
waitpid(pid, &status, 0);
// 完整解析退出状态
if (WIFEXITED(status)) {
printf("正常退出,退出码:%d\\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("被信号杀死,信号编号:%d\\n", WTERMSIG(status));
}
} else {
perror("fork 失败");
return 1;
}
return 0;
}
返回值解析
- 布尔型宏(WIFEXITED/WIFSIGNALED 等):返回非 0 表示真,0 表示假。
- 取值型宏(WEXITSTATUS/WTERMSIG 等):返回对应数值(退出码/信号编号)。
八、特殊进程:孤儿进程 & 僵尸进程(重点避坑)
1. 孤儿进程
定义
父进程先于子进程退出,子进程失去父进程,被 init 进程(PID=1) 或 systemd 进程收养。
核心特性
- 子进程正常运行,不会变成僵尸进程。
- 退出后由收养进程自动回收,无需手动处理。
代码示例
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:父进程退出后,子进程成为孤儿进程
printf("子进程 PID:%d,父进程 PID:%d\\n", getpid(), getppid());
sleep(10); // 等待父进程退出
printf("孤儿进程,新父进程 PID:%d\\n", getppid()); // 变为 1
} else if (pid > 0) {
// 父进程:立即退出
printf("父进程 PID:%d,即将退出\\n", getpid());
return 0;
} else {
perror("fork 失败");
return 1;
}
return 0;
}
2. 僵尸进程
定义
子进程已退出,但父进程未调用 wait/waitpid 回收其 PCB 资源,子进程残留在内核进程表中,状态为 Z(Zombie)。
危害
- 占用 PID 和内核进程表项,系统 PID 数量有限,过多僵尸进程会导致无法创建新进程。
解决方法
代码示例(模拟僵尸进程 + 解决)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:运行 2 秒后退出
printf("子进程 PID:%d,运行 2 秒后退出\\n", getpid());
sleep(2);
return 0;
} else if (pid > 0) {
// 父进程:休眠 10 秒,期间子进程退出成为僵尸进程
printf("父进程 PID:%d,休眠 10 秒(此时子进程为僵尸进程)\\n", getpid());
sleep(10);
// 回收子进程,解决僵尸问题
wait(NULL);
printf("子进程已回收,僵尸进程消失\\n");
sleep(5); // 验证是否还有僵尸进程
} else {
perror("fork 失败");
return 1;
}
return 0;
}
九、进程组管理(扩展核心)
1. getpgid – 获取进程组 ID
函数原型
#include <unistd.h>
pid_t getpgid(pid_t pid);
核心参数/说明
- pid:进程 ID;若传入 0,表示获取当前进程的进程组 ID。
- 作用:获取指定进程所属的进程组 ID。
完整代码示例
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = getpid();
pid_t pgid = getpgid(0); // 获取当前进程的进程组 ID
printf("进程 PID:%d,进程组 ID:%d\\n", pid, pgid);
return 0;
}
返回值解析
- 成功:返回进程组 ID(非负整数)。
- 失败:返回 -1,设置 errno。
2. setpgid – 设置进程组(创建进程组)
函数原型
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
核心参数/说明
| pid | 要设置的进程 ID;若为 0,表示当前进程 |
| pgid | 要设置的进程组 ID;若为 0,表示以 pid 为进程组 ID(创建新进程组) |
| 核心规则 | 1. 进程只能设置自己或子进程的进程组;2. 子进程必须在执行 exec 前设置;3. 进程组组长的 PID = PGID |
完整代码示例(创建新进程组)
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:创建新进程组,自身为组长
setpgid(0, 0);
printf("子进程(组长)PID:%d,进程组 ID:%d\\n", getpid(), getpgid(0));
sleep(10);
} else if (pid > 0) {
// 父进程:查看自身进程组
printf("父进程 PID:%d,进程组 ID:%d\\n", getpid(), getpgid(0));
sleep(10);
} else {
perror("fork 失败");
return 1;
}
return 0;
}
返回值解析
- 成功:返回 0。
- 失败:返回 -1,设置 errno。
十、进程管理常用命令(实战工具)
| ps | 查看进程状态 | aux:所有进程详细信息-ef:查看父子进程关系-o:自定义输出字段 | `ps aux |
| top | 实时监控进程资源 | M:按内存排序P:按 CPU 排序k:杀死指定进程 | top -p 12345 |
| pstree | 树状展示进程关系 | -p:显示 PID-u:显示用户 | pstree -p |
| kill | 向进程发送信号 | -l:查看所有信号-9:强制杀死(SIGKILL)-15:优雅退出(SIGTERM) | kill -9 12345 |
| killall | 按进程名杀死进程 | -9:强制杀死 | killall -9 firefox |
| pgrep | 按进程名查询 PID | -g:按进程组查询 | pgrep ls |


