目录
前言
一、进程程序替换
什么是程序替换?
首先我们来回答一个问题:为什么要创建子进程? 创建子进程就是为了让子进程帮我们执行特定的任务。 那么请问我们的子进程使用的是一个全新程序的代码和数据吗? 显然不是,在之前我们使用fork函数创建子进程时,我们就说过子进程会继承父进程的大部分PCB属性以及和父进程共享大部分代码和数据,所以此时的子进程使用的并不是一个全新的程序的代码和数据!!那么如果我们想要子进程指向一个全新的程序代码呢?
进行程序替换,它的出现就是为了让子进程指向一个全新的程序代码。这里用一个例子感性的理解一下:你的老爸是干养殖产业的,你未来可能会继承你老爸的产业,但是你不想重新走你老爸的路,你想自己去创业。
一个程序被执行起来了,OS会先为进程创建PCB还是先将代码和数据加载到内存中?
操作系统会先为进程创建PCB,再将代码和数据加载到内存中。程序在执行起来时变为了进程,OS会为进程分配一个唯一的进程标识符,并创建PCB内核数据结构来记录进程的各项信息,进程被创建PCB就被创建了。一旦PCB被创建了操作系统就会为进程分配内存,并将程序代码和数据加载到该进程的内存空间中,与进程地址空间和权限空间建立映射关系,最后让进程开始执行。
二、程序替换原理
我们先通过一个例子来见见程序替换:
我们来看看上述这个单进程进程程序替换的原理图:
结论:进程程序替换的本质就是将一个正在执行的进程中的代码和数据等内容替换为从磁盘中加载过来新的程序的内容,进程继续执行新的这段代码。
注:当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行,提高了进程的动态性和灵活性。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变,这种替换可以让不同的程序复用同一个进程,减少了系统资源的浪费,提高了进程的执行效率。
三、替换函数
exec 函数族提供了一个在进程中启动另一个程序执行的方法,其可以根据指定的文件名或目录名找到可执行程序,并用它来取代原调用进程的数据段、代码段和堆栈段。在执行完之后,原调用进程的内容除了进程 ID 外,其他全部被新的进程替换了。
以exec为开头的函数有七种,其中我们将重点讲解六个库函数,还有一个其实是我们的系统调用函数,这六个库函数最终都将封装这个系统调用函数!!
3.1 exec函数原型说明
- exec 函数如果调用成功则加载新的程序从启动代码开始执行,不再返回;如果调用出错则返回 -1。
- exec 函数调用成功不需要返回值?返回值也是原来的数据也需要写入,但是exec函数只要调用成功了,exec函数之后的代码就被新的程序的代码和数据替换掉了,所以这里的返回值毫无意义;而exec函数调用失败了那么就不会进行程序替换,而是继续执行原先的代码。
- 参数 pathname:可执行程序的路径名。
- 参数 filename:可执行程序的名字。
- 带l(list)的 exec 函数:可变参数列表,要以NULL结尾。
- 带p(path)的 exec 函数:不需要给该函数传可执行程序的路径,只需要告诉该函数要执行程序的名字,该函数会在环境变量 PATH 里的路径中进行查找。
- 带v的 exec函数:v表示 vector,即将所有的执行参数放入数组中统一传递,而不使用可变参数列表的方式进行传递,本质上它跟带 l的 exec 函数也就没什么区别。
- 带e(envp)的 exec 函数:自定义环境变量表。
3.2 exec族六大替换库函数
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
我们可以在3号手册看到这六个库函数的原型和用法,另外一个系统调用函数在2号手册里面:
3.2.1 execl
下面我们故意写错,看看exec函数调用失败会出现什么情况:
我们故意写错,此时可以调用execl函数失败,进程继续执行下面的代码,exit(1)退出子进程,父进程waitpid接收到子进程的退出码1。
下面我们继续来看一个例子,看看子进程的执行:
上述execl函数调用成功时,当execl函数调用成功时,此时执行ls file.txt指令,但是当前路径下并没有file.txt文件,所以子进程退出码为2,此时父进程只需要根据子进程的退出码就能判断子进程的执行结果的正确性了。
结论:当进程程序替换成功之后,此时进程的退出码为新的内容的执行结果。
我们来看看这个多进程的替换原理:
3.2.2 execv
关于execv函数本质上跟execl函数是一样的,这里就不做过多的讲述了。
3.2.3 execlp
3.2.4 execvp
上述进程执行的都是指令,那么我们能不能来执行我们自己写的程序呢?
C程序执行C++程序
C程序执行shell脚本程序
C程序执行Python程序
上述我们可以看到C程序可以去执行其他程序,这是C语言的功能吗?其实并不是,这是操作系统为我们做了相关的处理,不管是shell.sh还是test.py它们都是程序,只要是程序执行起来都是进程,那么操作系统是一定可以对进程进行管理处理的!!
3.2.5 execle
通过上图我们知道我们自定的环境变量表将PATH变量也覆盖掉了,这跟我们之前讲的环境变量的赋值产生的效果很相似,那么该如何得到我们的系统PATH变量呢?使用environ --> PATH指针!!
如何我想添加MYENV到环境变量表中那么该如何操作呢?我们可以使用putenv函数将自定义环境变量导入环境变量表中:
我们还可以使用export将MYENV导入环境变量表:
3.2.6 环境变量具有全局性,如何办到?
在讲完execle函数之后我们就能回答环境变量具有全局属性,可以被子进程继承下去的理由了。
在Linux中大部分指令都是bash的子进程,而bash执行所有的指令都可以通过exec去执行,所以我们就可以通过execle函数将bash的环境变量作为第三个参数传递给子进程,于是子进程就拿到了父进程的环境变量!!
我们导入环境变量都是向父进程导入,由此子进程也能继承父进程的环境变量:
3.2.7 execvpe
3.3 系统调用接口
execve函数它其实才是真正的系统调用接口,上述exec族函数对它进行了封装。
int execve(const char *filename, char *const argv[], char *const envp[]);
关于execve函数就不再给大家讲解了,这里我们来讲解一下它与其他exec函数之间的关系:
3.4 main函数是由谁调用的?
在环境变量那一篇文章中我们通过main函数的第三个参数同样的也获取到了环境变量,那么我们想想是谁给main函数传参的呢?必定是调用它的函数对吧,我们执行一个程序,这个程序里面包含了main函数,那么main函数就是exec系统调用的,main函数的参数就是操作系统给你传递的,那么自然而然main函数就有了一份环境变量表,这也就是为什么我们既能通过命令行拿到环境变量也能通过第三方变量拿到的原因。
本篇文章的内容就讲到这里了,如果文章有任何问题或者错处欢迎大家评论区相互交流orz~~🙈🙈