Linux 进程控制是操作系统对进程生命周期(创建、运行、终止、资源回收)的管理机制,核心涉及进程创建、进程终止、进程等待、进程程序替换等操作,以及相关的关键概念(如僵尸进程、孤儿进程)。
一、进程创建
进程创建的核心是通过系统调用从已存在的进程(父进程)生成新进程(子进程),Linux 中最常用的是
fork() 函数,另有 vfork()(较少使用)。1. fork () 函数
fork() 是创建新进程的主要系统调用,功能是复制当前进程(父进程)的地址空间,生成一个几乎完全相同的子进程。-
函数原型:
#include <unistd.h> pid_t fork(void); -
返回值:
- 对父进程:返回子进程的 PID(正数);
- 对子进程:返回 0;
- 失败:返回 -1(如内存不足、进程数达上限)。
-
原理:
fork()会为子进程复制父进程的 PCB(进程控制块)、地址空间(代码段、数据段、堆、栈等),但父子进程共享文件描述符(如打开的文件)。
注意:fork()后,父子进程并发执行,执行顺序由操作系统调度器决定,不确定。 -
示例:
#include <stdio.h> #include <unistd.h> int main() { pid_t pid = fork(); if (pid < 0) { // 失败 perror("fork error"); return 1; } else if (pid == 0) { // 子进程 printf("我是子进程,PID:%d,父进程PID:%d\n", getpid(), getppid()); } else { // 父进程 printf("我是父进程,PID:%d,子进程PID:%d\n", getpid(), pid); } return 0; }
2. vfork () 函数
vfork() 也用于创建子进程,但与 fork() 有本质区别:- 子进程共享父进程的地址空间(不复制),即父子进程操作同一块内存;
- 子进程优先运行,直到子进程调用
exit()或exec系列函数后,父进程才会恢复运行。
由于共享地址空间容易引发冲突(如子进程修改变量影响父进程),且存在安全风险,现在已很少使用,推荐用
fork()。二、进程终止
进程终止即进程生命周期结束,分为正常终止和异常终止。
1. 正常终止
-
main 函数返回:
return n等价于exit(n)(会触发exit函数)。 -
调用 exit ():标准库函数,会先刷新缓冲区(如
printf的输出缓冲区),再调用_exit()终止进程。 -
调用 _exit ()/_Exit ():系统调用(或标准库的底层实现),直接终止进程,不刷新缓冲区。
-
exit 与 _exit 的区别:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { printf("Hello"); // 缓冲区未刷新(默认行缓冲,无换行符) exit(0); // 会刷新缓冲区,输出 "Hello" // _exit(0); // 不刷新缓冲区,无输出 }
2. 异常终止
由信号(Signal)触发,如:
- 手动输入
Ctrl+C(触发SIGINT信号); - 访问非法内存(触发
SIGSEGV信号); - 除以 0(触发
SIGFPE信号)。
三、进程等待
父进程需要等待子进程终止并回收其资源(如 PCB、内存),否则子进程会变成僵尸进程(占用系统资源)。进程等待通过
wait() 或 waitpid() 实现。1. wait () 函数
-
功能:阻塞等待任意一个子进程终止,回收其资源。
-
原型:
#include <sys/wait.h> pid_t wait(int *status); // status 用于接收子进程的退出状态 -
返回值:成功返回终止的子进程 PID;失败返回 -1(如无子进程)。
2. waitpid () 函数
比
wait() 更灵活,可指定等待的子进程、设置非阻塞等。-
原型:
pid_t waitpid(pid_t pid, int *status, int options); -
参数:
pid:指定等待的子进程(pid > 0等待 PID 为pid的子进程;pid = -1等待任意子进程,同wait());status:接收退出状态(需用宏解析,如WIFEXITED(status)判断是否正常终止,WEXITSTATUS(status)获取退出码);options:WNOHANG表示非阻塞(若子进程未终止,立即返回 0)。
-
示例:父进程等待子进程终止并获取退出码
#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t pid = fork(); if (pid == 0) { // 子进程 sleep(2); // 模拟运行 return 10; // 退出码为 10 } // 父进程等待 int status; pid_t ret = waitpid(pid, &status, 0); // 阻塞等待子进程 pid if (ret > 0) { if (WIFEXITED(status)) { // 正常终止 printf("子进程 %d 正常终止,退出码:%d\n", ret, WEXITSTATUS(status)); } } return 0; }
四、进程程序替换
进程程序替换是用新程序的代码和数据覆盖当前进程的地址空间,实现 “运行另一个程序” 的效果(进程 PID 不变,仅替换代码、数据、堆栈等)。通过
exec 系列函数实现。1. exec 系列函数
exec 有 6 个变体,区别在于参数传递方式、是否带路径、是否自定义环境变量:| 函数名 | 特点(参数形式 / 路径 / 环境变量) |
|---|---|
execl |
参数为列表(const char *path, const char *arg, ...),需完整路径 |
execlp |
同 execl,但可自动搜索 PATH 环境变量(无需完整路径) |
execle |
同 execl,且可自定义环境变量(最后一个参数为环境变量数组) |
execv |
参数为数组(const char *path, char *const argv[]),需完整路径 |
execvp |
同 execv,可自动搜索 PATH |
execvpe |
同 execvp,可自定义环境变量 |
- 示例:子进程替换为
ls -l命令#include <stdio.h> #include <unistd.h> int main() { pid_t pid = fork(); if (pid == 0) { // 子进程 printf("子进程替换为 ls -l...\n"); // 替换为 ls -l(execlp 自动搜索 PATH,参数列表以 NULL 结尾) execlp("ls", "ls", "-l", NULL); // 若替换失败,才会执行下面的代码 perror("exec error"); exit(1); } wait(NULL); // 父进程等待子进程 return 0; }
五、关键概念
-
僵尸进程:
子进程终止后,父进程未调用wait()/waitpid()回收资源,子进程的 PCB 仍保留在系统中(仅占少量内存)。僵尸进程过多会耗尽进程号资源,导致无法创建新进程。
解决:父进程及时调用wait()回收;若父进程已退出,僵尸进程会被init(或systemd)收养并回收。 -
孤儿进程:
父进程先于子进程终止,子进程会被init(或systemd)进程(PID=1)收养,成为 “孤儿进程”。孤儿进程终止后会被收养者回收,不会成为僵尸进程。
总结
Linux 进程控制通过
fork() 创建进程、exit() 终止进程、wait()/waitpid() 回收资源、exec 系列函数替换程序,核心目标是管理进程生命周期,避免资源泄漏(如僵尸进程),确保系统稳定运行。