前言
目前我们接触到我们所创建的所有的子进程,它执行的代码都是父进程代码的一部分!那么如果我们想让子进程执行新的程序呢???执行全新的代码和访问全新的数据,不在和父进程有瓜葛,我们该怎么做呢?
这就需要引入一种技术叫做程序替换。
下面我们从这三个方面来介绍程序替换:
1.单进程版的程序替换的代码(没有子进程)--见见程序替换
2.理解和掌握程序替换的原理,更改多进程版的程序替换的代码,扩展理解和掌握多进程程序替换的原理
3.大量的使用其他的程序替换的方法--父子进程场景中
1.初见程序替换
下面我们先通过man手册来看看关于程序替换的一些接口:
下面我们通过第一个接口execl接口写一段代码来进行说明:
下面我们运行这段代码:
我们竟然神奇的发现,通过该程序我们把我们系统的命令ls -a -l这条命令给执行起来了。但是我们发现有一个现象就是只打印了begin那条语句,而没有打印end那条语句,这是我们目前观察到的现象。
我们再修改成几个其他的系统命令:
top
pwd
上述说明我们可以用"语言"调用其他程序
我们要替换哪一个程序->文件 -- > 程序文件的路径+文件名 --- 先找到
如何执行的问题? 命令行怎么写,就将参数怎么传。
最后一个参数必须以NULL结尾,表示参数传递完毕!
2.程序替换的原理
在这个过程中并没有创建新进程,为什么呢?因为当我们进行程序替换时,我们并没有增加或减少或改变进程的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.那么如果调用该函数之后还执行后续代码说明程序替换失败了,不用再继续判断,也就是可以看该函数调用后的后续代码是否被执行来判断程序替换是否成功了。
3.程序替换的使用场景
程序替换最基本的要求:
a.必须先找到这个可执行程序
b.必须告诉exec* 函数,怎么执行。
我们的程序替换,既然能替换系统指令程序,那么能替换我们自己写的程序吗?
myprocess.c源文件代码:
mytest.cpp源文件代码:
运行结果:
我们发现程序替换成功了。
exec*执行的操作是将程序替换,那么这不就是将我们前面谈到的一个程序要运行必须先加载到内存的那个过程吗?这还不就是加载器最重要的一个功能吗?