各位读者大佬好,我是落羽!一个坚持不断学习进步的学生。
如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步!
也欢迎关注我的blog主页: 落羽的落羽
文章目录
- 一、进程终止
-
- [1. 进程的退出码](#1. 进程的退出码)
- [2. 进程退出的方法](#2. 进程退出的方法)
- 二、进程等待
-
- [1. 进程等待的方法](#1. 进程等待的方法)
- 三、进程替换
-
- [1. 程序替换的方法](#1. 程序替换的方法)
一、进程终止
1. 进程的退出码
一个进程终止,本质上是释放系统资源,也就是释放进程相关内核数据结构和对应代码和数据。
而进程退出时,无非就是以下三种情况:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止(被信号终止了)
而进程执行的结果状态,可以用两个数字表示出来,即退出码 和终止信号 :int exit_code, int exit_signal:
- 当代码运行完毕且结果正确时,终止信号为0,退出码为0
- 当代码运行完毕且结果不正确,终止信号为0,退出码不为0
- 当代码异常终止时,终止信号不为0,退出码无意义
这两个数字不用由我们维护,OS会把进程退出的详细信息写到进程的task_struct中:

所以,进程需要僵尸状态维持自己的退出状态!
话说回来,为什么自此开始学习编程后,main函数总是要return 0?这就是因为,main函数的返回值就是这个进程的退出码,而一般规定退出码0代表进程结果正确!
当一个进程正常终止后,在Linux系统中我们可以用命令echo $?查看上一个进程的退出码!

可以使用strerror函数获取退出码对应的描述:

2. 进程退出的方法
进程常见的退出方法有:
- 代码执行完毕,正常终止的方法:
- main函数return
- 使用exit函数或_exit系统调用
- 代码执行异常终止方法:
- ctrl c、发送信号终止
exit是一个C库函数,作用是使当前进程终止,参数是想要返回的退出码

_exit是一个系统调用,它和exit的唯一区别是exit终止会强制刷新缓冲区,但_exit不会。
而本质上,exit的实现也是封装了_exit。这代表刷新缓冲区的操作,一定不是系统内核中的,而是由C/C++维护的!

写一段代码验证一下:
c
#include<stdio.h>
#include<stdlib.h>
void test()
{
exit(1);
}
int main()
{
test();
return 0;
}

在非main函数中,要注意return和exit的使用区别------return仅退出当前子函数、向函数调用处返回值、程序继续执行;exit 直接终止整个进程、程序彻底停止!
二、进程等待
之前已经讲过,子进程退出,父进程如果不管,就会导致子进程一直处于僵尸状态,而造成内存泄漏!除此之外,父进程还可能需要知道子进程的任务完成如何,也就是需要获取子进程的退出信息。
所以,父进程需要通过进程等待的方式,回收子进程资源,获取子进程退出信息!
1. 进程等待的方法
实现进程等待主要依靠系统调用wait和waitpid

wait可以等待任意一个子进程,且是阻塞等待。它的返回值是:如果等待成功,返回等待的子进程pid;等待失败则返回-1。
至于它的参数,是用于获取子进程退出信息的,下面讲
waitpid的功能比wait更丰富,返回值规则与wait类似,它有三个参数:
- 参数
pid_t pid:表示等待特定的子进程pid。若传-1则表示等待任意一个子进程。如果调用中出错,如传的pid不是自己子进程的pid,waitpid返回-1,程序的error会被设置成相应的错误码。 - 参数
int options:传0,表示阻塞等待,这是默认情况;传WNOHANG(一个宏),表示非阻塞等待。阻塞等待时,父进程不会继续执行代码,直到等到了子进程退出;非阻塞等待时,若指定pid的子进程还没有退出,waitpid返回0,不予等待。
可以看出,如果waitpid第一个参数传-1,第三个参数传0,效果就和wait一样。
wait的参数和waitpid第二个参数int* status,是要传一个int变量的地址,这个变量用于接收子进程的退出信息;若不想获取子进程退出信息,则可以传NULL。
写一个程序验证一下:
c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("我是子进程, pid是%d, 终止信号是%d, 退出码是%d\n", getpid(), 0, 1);
exit(1);
}
else
{
int status = 0;
pid_t retpid = waitpid(id, &status, 0); // 子进程退出前,父进程一直阻塞在这里
printf("我是父进程, 子进程%d已回收, status是%d\n", retpid, status);
}
return 0;
}

其他地方没有问题,可是status是256怎么来的??
其实,status不能当做简单的int看待,而是一个位图 :
status有32个比特位,只用后16个比特位记录信息。在后16个比特位中,低8位表示子进程的终止信号,高8位表示子进程的退出码!
所以,在上面的例子中,子进程终止信号为0,即00000000;子进程退出码为1,即00000001。那么status的32比特位为:00000000000000000000(前16位不用)00000001(退出码)00000000(终止信号),转为十进制就是256!
换句话说,想从status中得到子进程的具体信息,还需要这样位运算:
终止信号 = status & 0x7F,退出码 = (status >> 8) & 0x7F
但其实并不需要我们手动运算,系统中已经为我们提供了相应的宏函数:
WIFEXITED(status):如果是正常终止的子进程,返回真WEXITSTATUS(status):如果WIFEXITED(status)是真,返回子进程退出码
试验一下:
c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("我是子进程, pid是%d, 终止信号是%d, 退出码是%d\n", getpid(), 0, 10);
exit(10);
}
else
{
int status = 0;
pid_t retpid = waitpid(id, &status, 0); // 子进程退出前,父进程一直阻塞在这里
printf("我是父进程, 子进程%d已回收, status是%d\n", retpid, status);
printf("子进程是否正常终止:%d\n", WIFEXITED(status));
printf("子进程退出码:%d\n", WEXITSTATUS(status));
}
return 0;
}

没有问题!
三、进程替换
fork之后,父子进程各自执行当前程序的代码的一部分,如果我们想让子进程执行一个全新的程序怎么办呢?进程的程序替换来满足这个需求!
进程替换是指,通过特定的接口,加载磁盘上的一个全新程序,加载到调用进程的地址空间中!
1. 程序替换的方法
实现进程替换,主要依靠exec系列库函数:

这六个函数的功能都是替换一个程序,但是参数上有所区别,观察发现它们的参数总共是这几种:
const char* path:代表要传一个路径名字符串,可以是相对路径或绝对路径,如"/usr/bin/ls"const char* arg, ...:代表要传若干个字符串,最后一个必须是NULL。你想替换的程序在命令行中怎么执行,这里就怎么传,如"ls", "-a", "-l", NULLconst char* file:代表要传一个文件名字符串,不用写路径,程序会从环境变量PATH中的寻找这个文件。如lschar* const envp[]:代表要传一张环境变量表,这套环境变量会被新程序继承使用,覆盖原来的环境变量,数组元素也需要以NULL结尾;如果不想覆盖原环境变量表,则这个参数可以传environchar* const argv[]:代表要传一张命令行参数表,其实就是将上面的const char* arg, ...内容写进数组再传递,数组元素也需要以NULL结尾。本质上,程序的命令行参数,都是通过父进程使用程序替换函数传递给子进程的!
不难想到,exec系列函数通过上面不同的参数组合,有了六种函数,以应对不同的使用场景:
比如:想使用int execlp(const char* file, const char* arg, ...)函数,想把当前进程替换为执行ls。在当前进程中调用
c
execlp("ls", "ls", "-a", "-l", NULL); // 可以省略一个"ls",但是不建议,因为还要额外记忆
比如:想使用int execv(const char* file, char* const argv[])函数,想把当前进程替换为执行touch test.c。在当前进程中调用
c
char* argv[] = {"touch", "test.c", NULL};
execv("/usr/bin/touch", argv);
// 或execv(argv[0], argv);
这几个函数,如果程序替换成功,则没有返回值;如果调用出错则返回-1。所以exec函数只有出错的返回值,而没有成功的返回值。
在之前的学习中,我们fork创建子进程后,子进程和父进程执行的还是同一个程序,只是进入了不同的代码分支。
程序替换,没有创建新的进程 ,而是直接覆盖原有程序继续执行。所以,通常是父进程创建子进程后,让子进程替换成别的程序。程序替换后,原程序后面的代码就不再执行了。
替换的本质是:代码和数据拷贝到内存中。而只有OS有权做IO过程,所以进程替换要依靠系统调用!
真正的系统调用函数是:

上面说的exec系列函数,底层都是调用它。
我们来举个栗子完成一次程序替换:
c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
execlp("ls", "ls", "-a", "-l", NULL);
//一般而言,替换的新程序中正常执行完就会在它内部退出终止子进程,不会执行这一句
//而如果代码执行到了这里,就说明程序替换失败了
exit(1);
}
else
{
waitpid(id, NULL, 0);
}
return 0;
}

结果符合预期,子进程替换成了ls -a -l命令!
本篇完,感谢阅读。
