东莞网页制作网站,跨国网站怎么做,百度权重工具,无锡企业网站制作前言
我们之前利用 fork#xff08;#xff09;函数来创建子进程#xff0c;这种方式是 父子进程 共用一个代码#xff0c;只是在代码当中使用了 if-else 语句来分流#xff0c;达到父子进程运行不同的代码块的目的。但是其实本质上#xff0c;还是父子共用一个代码和数…前言
我们之前利用 fork函数来创建子进程这种方式是 父子进程 共用一个代码只是在代码当中使用了 if-else 语句来分流达到父子进程运行不同的代码块的目的。但是其实本质上还是父子共用一个代码和数据只不过如果父子进程 其中某一个先对某一个变量数据进程修改的话那么操作系统就会为这个 先修改变量数据的进程在内存当中新开辟一块空间然后这个进程就会使用这个新的空间来修改数据这是我们之前说过的写时拷贝具体可以看上几篇文章Linux - 进程地址空间-CSDN博客
Linux - 进程控制上篇- 进程创建 和 进程终止-CSDN博客
那么有没有方式可以达到 父子进程运行不同的代码的效果呢
答案是有的。就是我们这篇博客的主题 -- 进程程序替换。 进程程序替换
单进程的程序替换
我们先来简单实现一个 单进程的进程替换先看看进程替换可以达到什么效果 利用上述这个函数就可以实现在程序当中的调用到另一个 可执行程序文件如上就是各个参数的使用方式最后是一个 ... 是 可变参数我们可以输入任意数量的 参数实现不同的调用效果而其中会自己解析这个可变参数列表。 那么在了解上述函数的作用之后我们就来调用 bin 目录当中的 ls 这个命令的可执行文件需要注意的是 execl 函数当中的 可变参数列表在传参之时最后要传入一个 NULL表示这个参数列表到此为止。 程序输出 发现程序在执行 “before” 打印之后就执行了 “ls” 这个命令但是程序最后的 “after” 没有的打印。 我们自己的程序可以把系统当中的命令封装起来有我们自己的程序跑起来变成进程之后就可以直接调用 系统命令。这种调用我们就称之为 -- 程序替换。 而实现程序替换呢有很多的接口如下图所示上述只是演示用一种接口的一种用法 简单叙述程序替换原理
那么为什么会出现上述的问题呢为什么后面的 “after” 没有打印的呢
我们知道当我们运行一个可执行程序这个程序就变成了一个进程 既然是一个进程操作系统一定会为这个进程创建一个 PCB 对象用于维护这个进程
而在 PCB 当中的代码区是一个虚拟地址通过页表 把这个虚拟的一直映射到 内存当中的 物理地址。此时这个程序就跑起来了。
当这个程序执行到 excel这个函数的时候它的做法非常的简单粗暴直接拿 excel 第一个 参数路径位置指向的 可执行程序当中的代码 和 数据直接替换掉 本来在 内存当中原本 进程的 代码 和 数据。 在 text 这个 可执行文件生成的进程开始运行之时或者说是 还没有运行到 excel这个函数之时PCB 当中映射的 代码 是 text 这个可执行文件当中 拷贝到内存当中代码。
当执行 当前程序执行到 excel这个函数之时此时 PCB 映射的 代码 就是 ls 这个可执行程序当中代码了而且此时不仅仅是 代码数据也跟着一起替换了。注意这里的操作是直接替换。 简单来说就是用 新的 可执行文件当中的代码和数据替换掉 自己的代码和数据。然后从0开始执行。 而上述只是 进行了 内存当中 代码 和数据的替换并没有创建新的PCB结构体也并没有创建新进程。-- 这种就叫做程序替换。
而发生程序替换之后来程序当中的原本的代码就被新可执行文件当中的代码给替换了所以上述例子当中才会被不会执行 excel函数之后的代码但是如果 发生替换失败了依然会按照老程序当中的老代码进行执行也就是继续执行
而且exce** 代表所以 exce 系列的函数这些函数这有失败才会有返回值成功是没有返回值的。
可执行程序的表头 深层理解 程序替换原理
在可执行程序编译时期cpu 是如何知道各个区当中的入口的也就是各个区的起始地址的
其实在 Linux 当中可执行程序是有 格式的一般是 ELF。
而在可执行程序的最开始有一个区专门存储这些程序 的 各个区的起始地址我们把这个 可执行程序当中的这可以快空间称之为 -- 表头。 所以在加载这个可执行程序之时就算不加载这个程序的 代码和数据都要先把这个程序的 表头加载到内存当中。
所以再把编译时期cpu 是如何拿到 代码的起始地址的就是从这个可执行程序的表头当中获取到 各个区的起始地址。
当cpu 把表头加载到内存当中之后既可以通过这个表头读取到这个可执行程序的 入口。加载到内存当中运行。
换句话说如果某一个进程发生了程序替换那么新的可执行程序当中一定是有表头的那么。cpu就可以通过这个 表头来获取到 这个可执行程序的 入口从而替换到 原来的 代码区当中。 多进程的程序替换 在上述单进程的例子的基础之上创建子进程在子进程当中 调用 excel函数 调用 ls 这个可执行文件子进程在开始和结束的时候都打印上 开始和结束的 提示当上述子进程的操作执行完毕之后就直接 终止掉这个子进程 输出 发现程序在执行子进程之后当调用的到 excel函数之后调用到了 ls 这个可执行文件替换过来的代码而且子进程打印了 开始提示语句但是没有打印结束语句这也就印证了子进程PCB 指向的代码和数据已经被 ls 替换了。最后父进程等待 子进程的退出。打印了父进程打印的 提示语句。 现在就出现了一个问题了我们之前说过子进程开始是直接继承了父进程的 代码和数据如果 父子进程都没有修改数据的话父子进程甚至连 数据都是 共用了。但是 父子进程代码是共用的啊
上述子进程调用 excel函数那么按道理来说子进程修改了代码就会影响到 父进程因为我们现在的理解是父子进程是共用代码区当中的代码的。
但是按照上述多进程的 例子的输出来说父进程在最后的等待 子进程退出是正常输出的没有收到影响。 所以我们得出结论子进程当中发生了进程替换是不会影响到 父进程的。 但是为什么会发生上述的输出结果呢 因为进程之间是有独立性的虽然父子进程之间共用 一个 代码和数据当中如果其中的一方 对某一项是数据进行了修改那么操作系统就会对这个修改的变量的进程新开辟一个空间用于存储这个进程修改的数据这叫做写时拷贝。
但是上述是对 代码进行修改代码在此时也是发生了写时拷贝吗 答案是的。写时拷贝不仅仅会发生在 父子进程 的数据修改当中代码修改也是会 发生写时拷贝的。 所以读到现在你应该就可以回答一个问题了程序替换到底有没有创建一个新的进程 答案肯定是没有的只是发生 代码 和 数据 的写时拷贝在单进程当中是直接进行 数据 和 代码的替换 像上述是使用了 ls 这个系统当中实现的 可执行程序来替换当前程序的我们不仅仅可以替换系统当中的只要是可执行程序不管是官方的第三方的还是自己实现的可执行程序都是可以进程替换的。
比如下面这个程序我们在上述的 text 这个可执行文件的目录下创建一个新的文件 -- mycommand 这个可执行文件然后再 text 运行之时替换为 mycommand 执行。
此时在 text 可执行文件的当前目录下有一些文件 text 可执行程序代码 mycommand 可执行程序的代码 此时text 当中执行了 excel函数替换为了 mycommand 当中的代码运行text 的结果输出 发现也是成功把 text 当中的子进程部分的代码 替换成功了。 我们上述在调用 excel函数之时也是先确定 要替换的 新可执行文件在哪像上述给的是 相对路径然后在确定 调用的方式。
而且上述的调用方式是直接 输入了 mycommand 调用了这个程序和我们在命令行当中类似于 ./mycommand 的方式调用不一样没有带上路径这是因为在第一个参数就已经知道了这个可执行程序的路径位置了所以就不用在调用方式当中再带上路径直接调用即可。 而且我们上述是在 C 的可执行程序当中调用 exce*系列的函数这系列的函数是可以调用 所有的可执行程序不管是用什么语言写的可执行程序都是可以调用的因为 exce*()是系统调用层面的函数。 同样的脚本文件也是可以 被替换到 其中来执行的。
text.sh 脚本文件如下 脚本的调用方式 和 上述脚本的结果输出 然后我同样在 text 这个可执行文件当中利用 excel函数调用上述脚本文件 需要注意的是上述第一个参数也就是要运行的 可执行程序的 位置不是 text.sh 所在的位置因为我们不是运行脚本文件而是运行这个 bash 来解释 text.sh 当中内容来一个一个命令的执行。
./text 输出 那么为什么 各种不同类型的语言都可以通过 C/C 当中 exec*()系列的函数 所替换调用呢其实不管是哪一种语言写出来的语言本质上 最后运行都是变成了进程而上述 exec*系列的函数就是系统调用层面的函数他是在运行的程序当中进行代码和数据的替换。
另外基本上各个语言都会给我们提供 类似 C 当中的 exec*类似的接口。 execle接口putenv函数替换当中的环境变量的变化
在 text 的 execl当中传入的各个参数如 -a 这些参数都是可以在 mycommand 当中的main函数的参数进行 接收同样环境变量也是可以接收的 输出 在 mycommand 的main函数当中把text 传入的 参数 -a 和 -b 都接受到了而且环境变量也收到了默认的。
环境变量在 text 当中没有传入到 mycommand 当中那 mycommand 是如何得到 环境变量的呢 首先我们要明确的时环境变量也是数据在进程地址空间当中也是有 虚拟地址的既然虚拟地址那么一般都会通过页表 来映射到 内存当中的物理空间也就是说环境变量 和 命令行参数实际上各个进程之间都有存储。 所以在text 当中刚开始创建子进程就已经从父进程当中复制好了 进程地址空间当中数据即子进程进程了父进程当中 数据包括环境变量父进程又是从哪里来的 环境变量数据呢当然是 bash啦。 就算再程序替换当中替换了 代码 和 数据就算替换了数据环境变量信息不会被替换。
如果想在代码当中 添加一个 环境变量的话可以使用 putenv函数 使用这样的方式就可以在当前进程之下添加一个 环境变量但是这个环境变量当前进程的父进程是不能接收的只能是这个进程的子进程可以接收你可以理解为 类似于 C/C 当中的继承关系子类有父类的属性但是子类独有的属性父类是没有的。 所以如果你想添加一个 环境变量让子进程接收到 话直接在父类当中 putenv就可以了。替换完的进程也是可以继承替换之前进程的环境变量的。 如果你是在像在 进程当中传入 当前进程的环境变量的话可以使用 带 e 字母的 exec*函数比如 execle函数 像上述传入的是 父进程当中 或者是当前进程 当中环境变量如果想自定义的话可以自己定义一个数据传入彻底替换环境变量列表 程序替换的各种接口介绍 上述所说的 excel 函数只是其中之一实现进程替换的 exce* 系列的函数还有很多 上述七个是在 3 号手册当中的在 2 号手册的当中还有一个 函数被单独拿出来了 这些函数都是以 exec 开头的。
execl接口
比如像上述的 excel这个函数最后是一个 l 这个字母l 代表的意思就是 list 的意思在我们上述传入在excel这个函数当中传入参数的时候你可以发现从第二个参数开始我们给 对应的可执行文件当中传入的参数是 一个一个传入的最后一个指向NULL看起来就就像是 list 链表一样。 在上述的七个函数当中函数名 带 l 的说明这个函数可以 像链表一样一个一个传。
在命令行当中我们如何给这个可执行程序 的 main 函数传入参数的就怎么样给这个 execl函数传入参数 返回来看第一个参数我们看到是上述传入的是一个绝对路径其实整个 exec*系列的 函数的第一个参数都是要传入 对应要替换的可执行程序的 路径函数通过这个 路径来找到 这个可执行程序。这个路径可以是 绝对路径 也可以是 相对路径。
总结 通过上诉的传入 excel函数的参数可以知道 这个 新的 可执行程序在哪 如何执行带不带参数 execlp接口 上述在 l list 的基础之上还带上了 p 这个字母p 这个字母代表的意思是 PATH 。
execlp这个函数除了在 可以像 list 一样指定新的程序的如何传入参数之外它会自己从 默认的 系统当中 PATH 环境变量 保存的默认目录当中去 查找 第一个参数传入的 文件名。
比如上述的 ls 这个命令我们使用 execl函数是带上了绝对路径的但是如果 你使用的是 execlp这个函数的话因为我当前被的 PATH 环境变量是保存了 ls 这个命令所在位置路径的。
所以execlp可以直接通过 PATH这个变量保存的 默认路径 找到 ls 这个可执行程序所以代码可以这样写注下述程序和上述的多进程例子一样只是改变了 execlp这个函数 输出 虽然传入的第一个参数不是绝对路径也不是相对路径但是只要是在PATH 环境变量当中存储的 默认路径 是存在 ls 这个可执行文件的那么execlp这个函数就可以找到发现和之前多进程这个例子的输出是一样的。 ececv接口 v vector可以理解为数组顺序表。 从参数当中你也可以发现带 v 字母的 exec*()系列函数第二个参数都是 一个 char* 数组也就是 字符串指针数组。
所以v 和 l 的区别就在于 带v 系列函数 在传入 新程序的调用方式的时候使用的是 数组的方式来传入的 如上定义一个 字符串指针数组 然后以传入这个数组方式传入这个 新程序的调用方式。
注 这个 字符串指针数组 的最后一个参数必须是 NULL。
exeve接口
之前说过这个接口是 没有在之前的 6 大接口当中这个 exeve接口被单路拎出来的 放在 2 号手册当中那么 这个 exeve接口 和 上述的 6大接口有什么关系呢 其实上述的 6 大接口是 C 库函数是 C 语言层面帮我们在操作系统之上封装的一个 库函数而 execve这个函数是 真正的 操作系统当中的系统调用函数。 可以说是上述的 6 大函数 就是用 execve函数来实现的所以在上述的 6 大函数当中不管是你调用那一个函数最终都是调用了 execve这个系统调用函数这 6 大函数本质上就是对 execve这个函数的 在语言层面上的封装。 比如你传入可能只是文件名可能是带有绝对路径 或者 相对路径又或者是以 list 方式传入 新程序的调用方式传入参数的方式也可能是使用 vector 数组的方式来传入 命令行参数。
这些不同的传入方式在底层都是在进行各自函数的处理然后调用 execve这个操作系统层面的 系统调用函数。 总结
其实 在 exec*系列的 函数当中l v p 这些后面的不同组合的名字就代表了不同的 使用方式一个字母代表的是一种使用方式。所以对于这七个函数的使用只需要按照这些不同的 字母 代表的意思来使用即可。 bash 当中 进程替换 系列函数的使用 我们知道我们在命令行当中运行的一个个进程都是bash 为我们创建的子进程而为什么bash 实现的代码当中创建的进程会执行我们所书写的程序其实就是使用了上述所示的 进程替换的原理。
在 bash 当中创建子进程操作系统先为这个 子进程 创建一个PCB 对象跟着这个PCB对象一起创建的还有进程地址空间等等 用于维护 这个子进程的 信息。然后此时 子进程和 bash 是共用一个 数据 和 代码的
但是在 bash 当中会使用 exec*系列的函数把 bash 创建的子进程当中的 代码 和 我们写的代码的进行替换也就发生了 子进程 存储 新代码的 写时拷贝。
此时在子进程当中原本的 bash 的代码就被替换为了 我们所书写的新代码。
所以exec*() 系列的函数它承担的是一个 加载器的效果。 exec*系列函数就是代码级别的 加载器把 参数当中指向的 新的 代码 加载到 当前进程的 代码 当中然后按照 exec*() 参数当中传入的 新程序的调用方式来调用这个新的程序。这个调用方式也相当于是 命令行参数了。