目前我们接触到我们所创建的所有的子进程,它执行的代码都是父进程代码的一部分!那么如果我们想让子进程执行新的程序呢???执行全新的代码和访问全新的数据,不在和父进程有瓜葛,我们该怎么做呢?
这就需要引入一种技术叫做程序替换。
下面我们从这三个方面来介绍程序替换:
1.单进程版的程序替换的代码(没有子进程)--见见程序替换
2.理解和掌握程序替换的原理,更改多进程版的程序替换的代码,扩展理解和掌握多进程程序替换的原理
3.大量的使用其他的程序替换的方法--父子进程场景中
下面我们先通过man手册来看看关于程序替换的一些接口:
下面我们通过第一个接口execl接口写一段代码来进行说明:
下面我们运行这段代码:
我们竟然神奇的发现,通过该程序我们把我们系统的命令ls -a -l这条命令给执行起来了。但是我们发现有一个现象就是只打印了begin那条语句,而没有打印end那条语句,这是我们目前观察到的现象。
我们再修改成几个其他的系统命令:
top
pwd
上述说明我们可以用"语言"调用其他程序
我们要替换哪一个程序->文件 -- > 程序文件的路径+文件名 --- 先找到
如何执行的问题? 命令行怎么写,就将参数怎么传。
最后一个参数必须以NULL结尾,表示参数传递完毕!
在这个过程中并没有创建新进程,为什么呢?因为当我们进行程序替换时,我们并没有增加或减少或改变进程的pid,进程的pcb并没有发生改变,发生改变的只是进程PCB中通过进程地址空间通过页表映射到对应物理内存的代码和数据。
下面我们用一个多进程的场景来写程序替换:
上述代码其实是让子进程进行程序替换,而父进程只进行进程阻塞等待。
其实我们前面就说过,父进程创建子进程之后,代码和数据是共享的,如果父子进程中有一个进程需要修改数据的话那么会发生写时拷贝,那么我们上面又说程序替换是将进程的代码和数据进行替换的,那么一旦替换父子进程的代码和数据都会被替换,但是我们这里仅仅只是子进程发生程序替换,父进程不需要发生程序替换,所以这种情况不存在。因为进程具有独立性,所以程序替换代码和数据由于都会被覆盖所以父子进程的代码和数据都会发生写时拷贝。所以多进程发生程序替换需要发生写时拷贝。
而我们这里还是会有疑问:
子进程怎么知道,要从新的程序的最开始执行呢?
其实c语言编译之后会形成可执行程序,而在我们的Linux中可执行程序是以ELF的格式保存的,这里面会有一张表,然后表里面会有一个字段 entry:可执行程序的入口地址。
子进程怎么知道最开始的地方在哪里呢?
我们平常写的代码,代码都是被一行一行执行的,其实我们应该听过一个概念叫做程序计数器,叫做pc指针或者eip,CPU内的寄存器。这种寄存器CPU内只有一个,但是一个寄存器可以保存多套内容,所以每一个进程都有自己私有的eip,其中eip保存的是当前正在执行的指令的下一条指令的地址,所以我们平常c语言遇到的判断,循环,函数调用都是需要修改这个eip的内容的,也就是下一条指令的地址。所以子进程如何知道最开始的地方在哪里,就是通过哪一个进程调用*exec接口,那么程序替换之后就把可执行程序中的一张表中的entry字段填到对应的eip寄存器当中,让其成为下一条指令的地址,这样子进程就能够知道从最开始的地方执行了。
我们上面还有一个现象:发现就是只打印了begin那条语句,而没有打印end那条语句,通过以上结论就能够解释这个现象。
那就是如果我们的进程执行exec*这样的函数成功了,也就是程序替换成功了,那么该进程的代码和数据都会被新的程序的代码和数据给覆盖掉,同时eip保存的下一条指令也会被覆盖,那么此时进程执行的下一条指令的地址就不再是end那条语句了,所以后续代码不会再被执行了。
所以说:调用exec*这样的函数,如果当前进程执行成功,则后续代码没有机会执行了!因为被替换掉了!
exec* 这样的函数只有失败的返回值,没有成功的返回值。失败的返回值是-1.那么如果调用该函数之后还执行后续代码说明程序替换失败了,不用再继续判断,也就是可以看该函数调用后的后续代码是否被执行来判断程序替换是否成功了。
程序替换最基本的要求:
a.必须先找到这个可执行程序
b.必须告诉exec* 函数,怎么执行。
我们的程序替换,既然能替换系统指令程序,那么能替换我们自己写的程序吗?
myprocess.c源文件代码:
mytest.cpp源文件代码:
运行结果:
我们发现程序替换成功了。exec*执行的操作是将程序替换,那么这不就是将我们前面谈到的一个程序要运行必须先加载到内存的那个过程吗?这还不就是加载器最重要的一个功能吗?
1.当我们进行程序替换的时候,子进程对应的环境变量是可以直接从父进程来的。如何验证呢?
我们可以从两个角度去进行验证:
也就是说子进程mytest的环境变量是从myprocess父进程里面来的,那么这个myprocess的环境变量又是从哪来的呢?我们之前在环境变量那里说到过,环境变量在系统当中本身是具有全局属性的,那么具有全局属性的表现形式就是可以被它的所有子进程继承的,是被谁的子进程呢?因为我们在启动系统登录的时候,我们有一个叫shell的东西,也就是说当我们系统启动的时候,我们的myprocess是我们对应的bash的子进程,所以myprocess这个进程中的环境变量只能从bash进程里来 。其实也就是bash,myprocess,mytest这三个进程是爷孙三代用的一套环境变量的。
那么为了验证,下面我们用bash导入一个环境变量,根据我们上面所说的,那么子进程myprocess也会继承父进程的环境变量,mytest也会继承myprocess的环境变量,所以我们导入之后去查看子进程的环境变量,如果找到了子进程中有在父进程导入的环境变量,那么说明上述解释是正确的:
myprocess.c
mytest.cpp
我们是通过myprocess的子进程去进行程序替换的,所以可以通过运行myprocess来执行mytest里面的代码的。
我们先导入环境变量
运行结果:
我们发现 环境变量被继承了。
下面我们直接给myprocess进程导入一个环境变量,怎么导入呢?下面我们查看一下这个函数的手册:
下面我们直接给myprocess进程导入环境变量:
在这里导入环境变量的话跟bash进程是没关系的,因为只有子进程能继承到父进程新增的环境变量。
运行之后:
所以最终的子进程也能看到。
也就是说一开始bash有一批自己的环境变量是被myprocess和mytest继承的,然后我们通过给myprocess新增一个环境变量,那么这个环境变量就不会影响我们的bash进程。
执行这条命令发现是没有对应的环境变量的。但是mytest是会将myprocess和bash的环境变量都继承下来的。
2、环境变量被子进程继承下去是一种默认行为,不受程序替换的影响-----为什么?可以通过进程地址空间可以让子进程继承父进程的环境变量。
而我们知道程序替换是替换程序的代码和数据的啊,环境变量也是数据,为什么环境变量不会受程序替换的影响呢?
程序替换只替换新程序的代码和数据,环境变量不会被替换!
3.让子进程执行的时候,获得环境变量
a.将父进程的环境变量原封不动的传递给子进程 1.直接用 2.直接传
b.我们想传递我们自己的环境变量!我们可以直接构建环境变量表,给子进程传递。
c.新增传递
所以我们就可以得出结论,程序替换可以将命令行参数和环境变量通过自己的参数,传递给被替换的程序的main函数中!
而这个函数不是新增,而是覆盖式传递。
这上面这6个三号手册的接口其实底层实现都是通过用下面这个2号手册进行实现的
这六个程序替换的接口最终系统调用的其实就只有下面这一个,也就是说execve这个接口才是系统调用接口。无论上层的参数如何传递,最终都是会被转化成该系统调用的传参形式进行传参,也就是说上面接口的各种参数最终会转化成文件名,命令行参数,环境变量的形式在execve中进行传递。而有这么多的调用接口主要还是为了满足各种调用场景!
所以这就是这6+1的接口之间的关系。