前言
上一期对进程的创建、终止、以及等待做了详细的介绍,对于进程控制的内容基本介绍的差不多了,本期来介绍进程控制的最后一个内容即进程的程序替换!
本期内容介绍
• 什么是进程的程序替换
• 单进程的程序替换
• 程序替换的基本原理
• 多进程的程序替换
• exec系列函数的参数含义
• exec系列函数的使用
● 什么是进程的程序替换?
以前我们所有创建的子进程执行的代码,都是父进程代码的一部分;现在,我们想要让子进程执行全新的任务,即执行全新的程序(全新的代码和数据),该如何实现呢?其实这就是我们要介绍的进程的程序替换!
程序替换是指让一个已经存在的进程加载并执行一个全新的程序,同时替换掉原来进程加载的代码和数据的技术!
注意:
1、程序替换会保留原来进程的PCB等内核数据结构
2、新加载的代码和数据会与原来的内核数据结构重新构成映射关系
● 单进程程序替换
程序替换其实上是学习一些接口,这些接口就是exec*系列的程序替换函数!由于我们还没有对程序替换的函数介绍,我们这里用一个最简单的先来看看现象:
我们发现,当前进程果然执行了ls指令(本质就是2进制的程序)!OK,也就是成功的进行了程序替换,但是有一个细节就是:为什么execl后面的代码不见了?要解释这个问题我们就得先介绍一下程序替换的基本原理!
● 程序替换的基本原理
使用exec*系列程序替换函数会将要替换的程序的代码和数据,加载到内存直接替换掉原来进程地址空间中的的代码和数据,然后新的代码和数据与原来进程的PCB等内核数据节后重新建立映射关系!注意:调用exec*系列函数并不会创建新的进程!
介绍到这里我们就可以回答上面栗子的问题了:为什么exec*后的代码不见了?原因是,exec*函数已经替换了原来进程的代码和数据,该进程原来的exec*后面的代码被覆盖了所以就无法被执行了!
• 扩展:站在替换程序的角度讲,就是该程序被OS加载到内存了!其实我们平时的进程加载本质都是OS内核去调用exec*的系统调用了!exec*类似于Linux的加载器函数!
● 多进程程序替换
上面是单进程直接执行程序的替换,但是我们的想法性不是这样的,我们想要的是让父子进程执行不一样程序;即让子进程执行新的代码和数据!所以我们下面来让子进程替换:
这里我们发现程序替换前后的进程的pid都是一样的,这也验证了前面的exec*系列函数替换不会创建新的进程!
介绍到这里就有出现了一个新的问题:
• 问题
当前进程怎么知道要从替换的新程序的最开始执行?最开始的地方在哪里?
首先,在我们编译形成可执行程序的时候,它的代码和数据不是杂乱无章的随便放的,而是有自己的存放规则即有自己的存放格式!Linux中可执行程序是以EFL格式的文件存放的,该文件中有一个表头的字段entry,这个字段就是存放的可执行程序的入口地址!所以我们在调用exec*后会直接获取该程序的头部信息entry!而我们每一个进程中,都有一个程序计数器eip(当前执行指令的下一条指令),本质就是寄存器!eip寄存器的内容每个进程都会私有一份,当进程切换时把自己的eip属性给eip寄存器,该进程就会知道从哪一行开始执行了。当程序替换的时候,子进程获取entry,将这个字段的地址填入到eip中,当前进程就知道新程序的入口了!
OK,本来我们是接着演示用我们当前的进程调用其他语言的程序,但是由于我们当前由于还没有系统的介绍exec*系列的函数,所以我们无法将替换ls指令变成替换其他的程序,例如C++等!我们下面来系统的介绍一下exec*系列的函数,然后再来做当前进程替换其他程序的演示!
● exec系列函数的参数含义
exec*系列的函数一共是有7个,其中一个是系统调用,其他六个是库函数!
系统调用
库函数
其实下面的这六个库函数的底层都是去调用execve这个系统调用的
上面的六个库函数主要分为两类:l类型和v类型;两个类型又都包含带p不带p和带e不带e;OK,要搞清楚他们是什么意思,就要先介绍exec*系列的参数了!
第一个参数是指要替换程序的路径(path)或替换的程序名(file)
第二个参数是指如何执行新程序(arg)
带e的有第三个参数(envp)是指环境变量!
l(list)是指第二个参数是连续的参数列表的形式
v(vector)是指第二个参数是数组的形式
p(path)是指有自动搜索的环境变量PATH
e(env)表示自己维护环境变量
上面的exec*函数的参数主要解决以下问题:
• 如何找到新替换的程序(第一个参数)?
如果不带p,此时第一个参数path表示替换程序的路径;如果带p说明该替换的程序有全局的环境变量PATH此时第一个参数表示替换程序的名称!
• 如何执行新替换的程序(第二个参数)?
如果带l表示执行方式采用参数列表,可以将替换的程序的执行以可变参数的形式传进去(命令行怎么执行,这里怎么传)最后以NULL结尾!如果不带l带的是v,说明将替换程序的执行以数组(指针数组)的形式传过去,最后以NULL结尾!其实这个参数会传递以明命令行参数的形式传递给新替换的程序!
• 是否维护自己的环境变量(第三个参数)?
如果exec*函数带了e且需要维护自己的环境变量,就需要第三个参数envp了,这个参数也是一个字符串指针数组,替换之后envp这个参数会完全替换掉原来的环境变量!
• 新替换的程序(带p)是如何知道自己的替换程序的位置的呢?
在程序地址空间那里介绍过,在程序的地址空间中有一部分存的就是命令行参数和环境变量:
而每个进程在创建的时候都会继承父进程的虚拟地址空间的内容(拷贝一份),如果当前进程替换了新的代码和数据,但是当前进程的PCB等内核数据结构没有变所以依然有PATH等环境变量(在替换过程中,环境变量不会替换)!所以就可以找到,不带p的其实也是继承了PATH等内容,但是它的接口应该是特殊设计过的所以得必须带路径!其实,不带p的exec*也可以和带p的一样指定绝对会相对路径!
• exec*的返回值
你肯定注意到了exec*系列的函数是有返回值的,它的返回值是啥意思呢?
exec*系列函数如果调用成功了,从执行位置开始执行不在返回!如果调用出错了,返回-1,继续执行替换以前exec*以后的代码!所以exec*只有失败的时候才返回,成功不会返回!
所以,我们不用关心exec*的返回值,如果失败了就不会替换且继续执行原来进程的exec*后面的代码!
● exec系列函数的使用
• execl
第一个使用过了,这里就简单的介绍一下!
• execlp
OK,这里我们可以在验证一下,程序替换不会修改环境变量的内容:
先给bash导一个环境变量:
export CPDD=123456
我们下面就用带p的去验证:
我们可以使用makefile生成,但是我们目前的makefile只能生成一套依赖关系(以前在make和makefile那一期介绍过)如何生成两套生成关系呢?
可以加一个伪目标all生成两套关系:
OK,看结果:
这里我们不光验证了程序替换不会修改环境变量的值,还验证了当前进程替换我们自己写的程序!
• 注意:这里的process是要带路径的,因为他不是环境变量!!!
• 补充:如果要在当前进程中新增环境变量可以调用putenc接口
• execle
前两个和前面的一样不在介绍了,最后一个是替换程序的环境变量数组,可以是全新的自己的环境变量;也可以是纯老的环境变量:envrion
先验证,给替换的进程原来老的环境变量envrion
果然继承过来了!这也验证了子进程是可以看到父进程的环境变量表的!OK,现在我们想用我们自己的换将变量该怎么办呢??下面是,导入自己想到的环境变量的代码:
• execv
这个和上面的execl唯一的差别是上面的是将替换程序的执行以列表的形式传递,而以v结尾的exec系列函数是以数组的形式传递的:
当然也可以像上面的多进程程序的替换!
• execvp
• execvpe
后面的这两个由于和前面的l系列的只是如何执行的传参不一样其他都一样,这里就不在一一的演示了!
OK,好兄弟,本期分享就到这里,我们下期再见!
结束语:知不足而奋进,望远山而前行!