为什么要进行进程替换
我们通过fork函数创建子进程,而创建子进程的目的有两种,一种是让子进程执行父进程中的一部分代码。
我们之前写的代码都是这样,就类似下面的代码:
pid_t id=fork();
if(id==0)
{
//子进程代码
}
else
{
//父进程代码
}
还有一种是让子进程执行另一个全新的程序,此时子进程不再执行父进程的代码,而进程替换就是为了这种情况而产生的。
我们还要注意两个概念的区别:进程替换和进程切换,进程替换就是本文介绍的内容,而进程切换是指每个进程都有属于自己的时间片,而cpu只有一个,为了保证多个进程同时运行,cpu每次只执行一个进程一个时间片长度的时间,而后就会执行别的程序,依次循环,这个叫做进程切换。
进程替换的本质
通过我们上面的描述,进程替换是为了让子进程执行其他程序的代码,而第三方可执行程序的代码和数据还在磁盘上。进程替换是使用写时拷贝的策略,使得第三方可执行程序的数据和代码替换掉子进程的数据和代码让子进程执行,由于父子进程间有独立性,所以不会影响父进程。此时的情形可以使用下面的图来描述:
由于第三方程序还在磁盘里,所以进程替换就要先在内存中开辟空间来存储新程序的代码和数据,之后修改子进程页表的映射关系,此时父子进程就完全没有了任何关系,彻底脱离,这就是进程替换所做的工作。所以进程替换本质上不会改变进程PCB以及虚拟地址空间(证明:后续我们做实验会发现子进程的pid一直不变),只会改变进程的页表结构使其指向刚刚加载到内存的新程序的代码和数据。
进程替换函数有哪些?
#include<unistd.h>
int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execvpe(const char* file, char* const argv[], char* const envp[]);
int execve(const char* path, char* const argv[], char* const envp[]);
替换函数总共有7个,最特殊的是execve,因为系统实际提供的只有这一个接口,其他的底层调用的都是这一个函数,提供这么多函数种类的原因是:可以让用户根据不同的场景来使用更加便利的方法。
函数调用及返回值
- 若函数调用成功,新的程序代码和数据替换掉原有的代码和数据,不会再返回
- 若函数调用失败,返回-1
- 总的来说:替换函数只有失败时的返回值-1,没有成功时的返回值
函数名称含义以及参数意义
替换函数的前4个英文字母都是exec,这是单词execute的简写,是执行的意思;
而后面跟着的字母都有他自己的意义:
- l是list,列表的意思,是说用来替换的程序地址以list的形式传入
- p是PATH的意思,就是我们在环境变量的那章介绍的PATH,也就是说,如果我们调用的是系统自带的命令程序的话,是不需要添加路径的;如果我们想替换我们自己的程序的时候也想不添加路径,我们可以用方法将我们自己的程序的地址放入环境变量PATH中,具体方法在环境变量章节已讲过。
- v是vector,数组的意思,是说我们传入用来替换的程序的地址时,使用指针数组的方式。
- e时environ,环境变量的意思,也就是说我们被替换后的程序运行的时候,使用的环境变量是我们自己传入的,如果函数后面不带e的话,环境变量是默认的系统的环境变量。
下面我们根据掌握的知识分别对这几个exec函数做测试:
execl:
int execl(const char* path,const char* arg,...)
//介绍一下此函数的三个参数
//path:表示要替换的程序的路径(包括最后的程序名)
//arg:表示要替换来的程序名,包括程序名,及选项。这里要注意不要和path中的程序名混淆,path中的是路
//径,表示的是进程去哪里找这个程序,而arg中的程序名及选项是用来告诉进程怎样执行的,一个是去哪找,一
//个是怎样执行
//...:是c语言中的可变参数列表,我们没怎么涉及到,感兴趣的同学可以去自学一下
具体使用方法如下:
结果为:
在此同学们可能有一个疑惑,为什么子进程中“进程替换完毕!”这句话没有被打印出来?而且子进程最后退出的时候传入的是10,为什么最后退出码是0?这是因为exec函数本质会把后面的所有代码都替换成我们传入的程序代码,也就是说exec函数后面的代码都被换走了,也就不会再被执行,因此printf语句没有被执行。为什么子进程的退出码是10也是同理,子进程后续代码被替换成了ls -al系统指令,而被替换上来的ls程序是正常结束退出的,所以子进程的退出码是0。
并且通过上面的程序我们可以发现,替换前的替换后子进程的pid都是一样的,这也就作证了我们之前的观点:程序替换不会创建新的PCB,地址空间,只是在内存中开辟空间将新程序的代码和数据移入,并且修改页表指向而已。
execv:
int execv(const char* path, char* const argv[]);
//这个函数和execl只有一点不同,就是他的程序执行方法(程序名和选项)是用指针数组传入的,也就是说除了
//程序地址,其他的信息都放在一个数组中传入
上面代码产生的警告是因为标黄的几个是常量字符串,c++11标准下,数据类型对应的更加准确,只要在第14行前面最前面加上const,使my_argv成为一个常量指针数组就可以消除警告,但是这样做的话又会使下面的execv函数产生函数参数不匹配的错误,所以我们现在只能留着这个警告。
正常运行后,作用和execl是一样的:
execlp:
int execlp(const char* file,const char* arg,...)
//和execl函数相比,只有一点区别:替换的程序如果在系统默认路径PATH中的话,传第一个参数时不用带前面的
//路径,只带程序名即可
结果也是一样的:
execle,execve:
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execve(const char* path, char* const argv[], char* const envp[]);
//这两个我们一块说,两个函数都是要用户自己传入环境变量的
做这两组实验的时候我们用自己的程序做替换,这时候就需要先做一些准备工作:
首先要创建一个被替换上来的程序test.c,打印10遍我们传入的环境变量,代码如下:
然后我们需要改变一下我们的makefile,因为这次替换的是我们自己写的程序,所以我们想要在make的时候同时生成两份可执行程序,而make的时候只会执行makefile的第一条命令,所以我们需要一个伪目标,这个伪目标的依赖文件就是我们想要的可执行程序的名称,当make的时候,执行第一条命令,makefile就会发现没有伪目标的依赖文件,所以就会接着执行后面的命令以生成这两个依赖文件,这也就是我们真实的目的。file代码如下,我假设伪目标的名称为all:
代码如下:
运行结果正确:
execve无非就是用数组传入程序名和选项,在此我们没有必要再做演示。
execvpe:
这个函数跟上面两个函数相比最大的区别就是,会在环境变量PATH所指的路径中查找目标程序,但是此时目标程序是我们自己写的程序,路径就在当前目录下,我们无法用这个函数做环境替换,如果执意要用这个函数的话,只能将这个程序的地址添加到PATH中去,我们不建议这样的行为。
以上就是进程替换的内容,现在我们已经可以用子进程替换执行别的程序,那么我们在循环中一直调用默认的命令行指令,不就可以写出一个简单的命令行解释器了吗。下篇文章就将介绍怎样用进程替换的知识写出一个简单的命令行解释器。