文章目录
- 前言
- [一. 进程创建](#一. 进程创建)
-
- [1.1 fork 函数](#1.1 fork 函数)
- [1.2 写时拷贝](#1.2 写时拷贝)
- [1.3 fork 常规用法](#1.3 fork 常规用法)
- [1.4 fork 调用失败的原因](#1.4 fork 调用失败的原因)
- [二. 进程终止](#二. 进程终止)
-
- [2.1 进程退出](#2.1 进程退出)
- [2.2 进程常见的退出方法](#2.2 进程常见的退出方法)
-
exit和_exit的区别-
- [🔍 详细区别](#🔍 详细区别)
- [✅ 使用建议](#✅ 使用建议)
return退出和exit退出的区别
- [2.3 退出码](#2.3 退出码)
- [三. 进程等待](#三. 进程等待)
- [四. 进程程序替换](#四. 进程程序替换)
- 最后
前言
在上一篇文章中,我们详细介绍了命令行参数、环境变量和进程地址空间的内容,内容还是挺多的,希望大家可以多去练习熟悉一下,那么本篇文章将带大家详细讲解进程控制的内容,包括进程创建、进程终止、进程等待和进程程序替换,接下来一起看看吧!
一. 进程创建
1.1 fork 函数
在 Linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

fork 函数一个系统调用,创建子进程成功时,返回0给子进程,返回子进程的pid给父进程;创建子进程失败则返回-1给父进程。
我们可以通过 fork 函数的返回值来对父子进程进行分流:
c
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0){
// 创建子进程失败
perror("fork");
return 1;
}
else if(id == 0){
//子进程
printf("子进程, pid : %d\n",getpid());
}
else{
//父进程
printf("父进程, pid : %d\n",getpid());
}
return 0;
}

进程调用 fork ,当控制转移到操作系统内核中的 fork 代码后,操作系统内核做了什么?
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
fork函数返回,调度器开始调度

fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意 :fork之后,谁先执行完全由调度器决定。
1.2 写时拷贝
当创建子进程时,父子进程的代码和数据共享,当父/子进程要进行数据修改时,操作系统就会进行写时拷贝,重新开辟一块空间给这个进程拷贝数据,然后修改它的页表(虚拟地址和物理地址)的映射关系,这样就完成了写时拷贝。
操作系统通过权限 来触发写时拷贝的,当创建子进程后,父和子进程的数据都改为只读 的,如果任意一个进程想要修改数据,就会报错,操作系统就会进行写时拷贝。

写时拷拷贝是一种延时申请技术,可以提高整机内存的使用率。
1.3 fork 常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如:父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如:子进程从
fork返回后,调用exec函数(进程程序替换)。
1.4 fork 调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
二. 进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
2.1 进程退出
当我们执行一个程序时,如果程序代码执行完了,那么它的运行结果可能符合我们的预期,也可能不符合我们的预期;程序也可能没有执行完,而是异常终止了。
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
2.2 进程常见的退出方法
进程正常退出的场景有三种:
- 从
main函数返回- 调用
exit函数退出- 调用
_exit退出
当我们使用Ctrl + C杀死一个进程属于进程的异常退出,通过信号让进程终止也属于异常退出。
exit函数

通过查看手册,可以发现exit函数属于3号手册,也就是C标准库的库函数;
那它的作用就是,退出一个进程并返回退出码;之前在C中执行的exit(1)就是退出程序并返回1
_exit系统调用

通过查看手册,我们发现_exit属于2号手册,也就是系统调用
它的作用也是退出进程,并且返回退出码。
exit和_exit的区别
在Linux中,exit()和_exit()都用于终止进程,但它们在行为细节 上有重要区别,主要体现在是否清理资源 和是否调用用户注册的退出处理函数。
exit()是标准库函数 ,会执行清理操作;
_exit()是系统调用 ,直接让内核终止进程,不做任何清理。
exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:
- 执行用户通过
atexit或on_exit定义的清理函数。- 关闭所有打开的流,所有的缓存数据均被写入
- 调用
_exit

🔍 详细区别
| 特性 | exit()(标准库函数) |
_exit()(系统调用) |
|---|---|---|
| 是否刷新stdio缓冲区 | ✅ 会刷新(如printf的输出) |
❌ 不会刷新 |
是否调用atexit()注册的函数 |
✅ 会调用 | ❌ 不会调用 |
| 是否关闭打开的文件描述符 | ✅ 会关闭(通过标准库) | ❌ 不会关闭(由内核处理) |
| 是否返回退出码给父进程 | ✅ 会 | ✅ 会 |
| 是否属于系统调用 | ❌ 不是,封装了_exit() |
✅ 是,直接调用内核 |
✅ 使用建议
| 场景 | 推荐 |
|---|---|
| 正常退出程序 | 用 exit() |
| 子进程中避免刷新父进程缓冲区 | 用 _exit()(如fork()后子进程出错) |
| 写底层系统程序或库 | 用 _exit() 避免副作用 |
exit函数
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Hello Linux");
exit(1);
return 0;
}

刷新缓冲区
_exit系统调用
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("Hello Linux");
//exit(1);
_exit(1);
return 0;
}

不刷新缓冲区
return退出和exit退出的区别
return是语言级 的函数返回,把控制权交还给上一层调用者;exit是库函数/系统级 的进程终止,把控制权直接交给操作系统。
2.3 退出码
退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。代码 1 或 0 以外的任何代码都被视为不成功。
Linux Shell 中的主要退出码:

可以通过 echo $? 查看最近一次进程的退出码:
c
#include <stdio.h>
int main()
{
printf("Hello Linux\n");
return 1;
}

通过echo $?命令可以查看最近程序的退出码为1,如果再执行一次echo $?命令可以看到输出的是1,因为echo也是一个程序,当成功执行时返回的就是0
注意 :当程序异常终止时,程序的退出码是没有意义的。
三. 进程等待
3.1 进程等待的必要性
父进程必须等待子进程结束,内核才能彻底回收子进程的 PCB(进程控制块),否则子进程变成"僵尸",占用系统资源。
| 理由 | 后果 |
|---|---|
| 1. 回收退出码/状态 | 子进程的返回值、终止信号存在 PCB 里,只有父进程 wait 后内核才会释放这段内存。 |
| 2. 避免僵尸进程(Zombie) | 子进程已死但 PCB 残留,PID 仍被占用;大量僵尸会耗尽系统 PID(pid_max),导致 fork 失败。 |
| 3. 向父进程传递 SIGCHLD | 子进程终止时内核发送 SIGCHLD;父进程通常在该信号处理函数里调用 wait/waitpid 异步回收。 |
| 4. 同步父子生命周期 | 父进程可能要根据子进程是否成功、退出码是多少再做下一步动作;等待提供同步点。 |
总结 :父进程通过进程等待的方式,回收子进程资源 ,获取子进程退出信息。
3.2 进程等待的方法
可以通过系统调用wait和waitpid

wait方法
wait只有一个参数status,status是一个输出型参数,简单来说就是:我们想要让父进程获得子进程的退出信息,就传递一个int* 指针,这样在函数调用结束后,父进程就拿到了子进程的退出信息;(如果不需要获取子进程的退出信息,可以传NULL)。
如果等待成功则返回被等待进程pid,等待失败则返回-1。
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
int id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){
//子进程
printf("子进程, pid : %d, ppid : %d\n",getpid(),getppid());
sleep(3);
exit(1);
}
//父进程
sleep(5);
wait(NULL);
sleep(100);
return 0;
}

waitpid方法
与wait不同的是:waitpid多了两个参数:pid和option
参数:
pid:pid=-1,等待任一个子进程,与wait等效;pid>0,等待其进程ID与pid相等的子进程。status:输出型参数。WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出);WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)options:默认为0,表示阻塞等待;WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID ;
如果设置了选项WNOHANG ,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
现在来模拟一下非阻塞等待:
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
if(id == 0){
//子进程
int cnt = 3;
while(cnt--)
{
printf("子进程, pid : %d, ppid : %d\n",getpid(),getppid());
sleep(1);
}
exit(1);
}
//父进程
printf("等 待 开 始\n");
while(waitpid(-1,NULL,WNOHANG) == 0)
{
printf("父进程: pid : %d, ppid: %d\n",getpid(), getppid());
sleep(1);
}
printf("等 待 结 束\n");
sleep(100);
return 0;
}

我们使用非阻塞等待要轮循调用waitpid,再等待过程中,父进程可以执行自己的代码。
3.3 获取子进程的status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

只研究status低16比特位,高16比特位没有用,所以要把它看作一个位图。在低16比特位中的高8位表示进程退出时的退出码,低8位表示进程的退出信号(实际上是7位,有一位是core dump标志,后面会讲解)。
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
int id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){
//子进程
printf("子进程, pid : %d, ppid : %d\n",getpid(),getppid());
exit(1);
}
//父进程
int status = 0;
//阻塞等待
waitpid(-1,&status,0);
printf("child status : %d\n",status);
return 0;
}

为什么退出码是256?而不是1?
因为退出码占8位,退出码为1,而退出信号占低8位,为全0,所以输出的是2的8次方,即256
获取退出码
只需让
status>>8然后再按位与&上0xFF(1111 1111)即可获得退出码
获得退出信号
退出信号实际上只占低7位,只需让
status按位与&上0x7F(0111 1111)即可获得退出信号
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
int id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){
//子进程
printf("子进程, pid : %d, ppid : %d\n",getpid(),getppid());
exit(1);
}
//父进程
int status = 0;
waitpid(-1,&status,0);
printf("child status : %d\n",status);
printf("status exit code : %d\n",(status>>8)&0xFF);
printf("status exit signal : %d\n",status&0x7F);
return 0;
}

操作系统中还存在一些宏,可以直接使用这些宏来获取退出码和退出信息:
退出码:
WIFEXITED(status):判断程序是否正常退出;- 返回true就表示程序正常退出;
WEXITSTATUE(status):当进程正常退出时,可以通过WEXITSTATUE获取进程的退出码。退出信号:
WTERMSIG(status):当进程异常退出时,可以使用WTERMSIG获取当前进程的退出信号。
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
int id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){
//子进程
printf("子进程, pid : %d, ppid : %d\n",getpid(),getppid());
exit(1);
}
//父进程
int status = 0;
waitpid(-1,&status,0);
if(WIFEXITED(status)){
printf("status exit code : %d\n", WEXITSTATUS(status));
}
else{
printf("status exit signal : %d\n", WTERMSIG(status));
}
return 0;
}

四. 进程程序替换
fork() 之后,父子各自执行父进程代码的一部分,如果子进程就想执行一个全新的程序呢?进程的程序替换来完成这个功能!
程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!
4.1 替换原理
用 fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。

c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("进 程 切 换 开 始\n");
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("进 程 切 换 结 束\n");
return 0;
}

我们可以看到进程的程序是替换了,为什么没有执行最后的printf语句(printf("进 程 切 换 结 束\n"); )呢?
因为程序一旦被替换,就不会再执行往后的代码了,进程的代码和数据都被替换成新的代码和数据了。
4.2 替换函数
其实有六种以 exec 开头的函数,统称exec函数:

函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
- 如果调用出错则返回-1
- 所以
exec函数只有出错的返回值而没有成功的返回值
命名理解
l (list):表示参数采用列表v (vector):参数用数组p (path):有p自动搜索环境变量PATHe (env):表示自己维护环境变量
execl
execl 函数参数存在两个
- 第一个表示要执行新的程序所在的路径,执行系统指令时也要带上路径
- 第二个则是参数列表,表示要怎么执行这个新的程序;有
l时表示以参数列表 的形式调用,参数列表要以NULL结尾。
c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("进 程 切 换 开 始\n");
execl("/usr/bin/ls","ls","-l",NULL);
printf("进 程 切 换 结 束\n");
return 0;
}

执行自己写的程序:
code.c
c
#include <stdio.h>
int main(int argc, char* argv[])
{
for(int i = 0;i < argc;i++)
{
printf("argv[%d] : %s\n", i, argv[i]);
}
return 0;
}
test.c
c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("进 程 切 换 开 始\n");
execl("./code","./code","-a", "-b", "-c",NULL);
printf("进 程 切 换 结 束\n");
return 0;
}

execlp
execlp和execl相比唯一的不同就是,执行系统指令时不需要带路径(它会通过PATH环境变量去寻找指定的系统指令)
当然使用execlp函数也可以执行自己写的程序,只不过要带上路径。
c
#include <stdio.h>
#include <unistd.h>
int main()
{
execlp("pwd","pwd",NULL);
return 0;
}

execle
execle有三个参数:
- 新的程序所在的路径(系统指令也要带路径)
- 参数列表,以NULL结尾
- 环境变量表。
有时候我们可以传自己维护的环境变量表,而不用自带的。
code.c
c
#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
for(int i = 0;i < argc;i++)
{
printf("argv[%d] : %s\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %s\n", i, env[i]);
}
return 0;
}
test.c
c
#include <stdio.h>
#include <unistd.h>
int main()
{
extern char** environ;
execle("./code","./code", "-a", "-b",NULL, environ);
return 0;
}

execv
execv有两个参数
- 新的程序所在的路径(系统指令也要带路径)
- 命令行参数表,以数组的形式存储,最后一个存NULL
c
#include <stdio.h>
#include <unistd.h>
int main()
{
char* const argv[] = {
(char* const)"ls",
(char* const)"-a",
(char* const)"-l",
NULL
};
execv("/usr/bin/ls",argv);
return 0;
}

execvp
execvp和execv的区别就是,在执行系统命令时,可以不带路径;execvp会在环境变量PATH中找到指定程序。
c
#include <stdio.h>
#include <unistd.h>
int main()
{
char* const argv[] = {
(char* const)"ls",
(char* const)"-a",
(char* const)"-l",
NULL
};
execvp("ls",argv);
return 0;
}

execvpe
execvpe存在三个参数:file、argv和env
file:第一个参数,指的是新程序所在的路径。argv:第二个参数指的是命令行参数表,以数组的形式存储。env:第三个参数指的是环境变量表。
code.c
c
#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
for(int i = 0;i < argc;i++)
{
printf("argv[%d] : %s\n", i, argv[i]);
}
printf("\n");
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %s\n", i, env[i]);
}
return 0;
}
test.c
c
#include <stdio.h>
#include <unistd.h>
int main()
{
char* const argv[] = {
(char* const)"./code",
(char* const)"-a",
(char* const)"-b",
(char* const)"-c",
NULL
};
//使用自己的环境变量表
char* const env[] = {
(char* const)"MYNAME=XZX",
(char* const)"MYVAL=666666",
NULL
};
execvpe("./code", argv, env);
return 0;
}

这里简单总结一下这些函数:

4.3 execve
与上面的exec系列的函数不同,execve是一个系统调用

简单来说上面的exec系列是库函数,而execve是操作系统提供的系统调用
exec系列函数对execve系统调用做了封装
- 当使用
execlp执行系统命令不带路径时,execlp会根据环境变量PATH找到对应程序的路径,然后传新程序的路径给execve来调用。- 当使用
execl、execv等函数没有传环境变量表时,在调用execve系统调用时会传当前环境变量表environ。- 当我们使用
execl等带l的函数时,传递的参数列表都会被转化成参数数组 ,然后再将参数数组传递给execve。

最后
本篇关于进程创建、进程终止、进程等待和进程程序替换的内容到这里就结束了,其中还有很多细节值得我们去探究,需要我们不断地学习。如果本篇内容对你有帮助的话就给一波三连吧,对以上内容有异议或者需要补充的,欢迎大家来讨论!