1.什么是进程程序替换?
在学习进程程序替换之前,我们知道当一个父进程创建一个子进程之后,父子进程的代码是共享的,子进程只能执行父进程的代码块
但是现在我们的需求增加了,我们不仅要让子进程能够执行父进程的代码块,也要能够让子进程能够做一些父进程不能做的事情,也就是能够执行一个全新的代码(程序),这样就能实现父子进程做的事情有所差异,大大提高了办事效率,同时也使父子进程的代码彻底分离,维护进程的独立性
进程程序替换:子进程在运行时指向一个全新的程序代码
所谓进程程序替换,顾名思义,就是使用一个新的程序替换原有的程序,进程将执行新程序的代码,而不再执行原有程序的代码,前面我们已经学习了如何创建一个进程,一般情况下,进程程序替换都不会使用父进程直接进行进程程序替换,而是让父进程调用fork()函数创建一个子进程,让子进程去执行一个新的程序即可
这个过程通常是由操作系统提供的 exec 系列函数来实现的:
- exec 函数族:exec 函数族是一组系统调用,用于执行程序替换操作。这些函数包括 execl, execv, execle, execve 等,它们允许以不同的方式传递参数给新程序,并执行地址空间替换。(我们要改变内存,那肯定是要调用系统调用接口的,这些函数会封装相应的接口)
我们先看简单版本
参数说明:
path
:要执行的程序的路径。arg
:新程序的参数列表的开始,通常这会是新程序的名称(尽管这不是强制的,但它通常用于错误消息和程序内部)。...
:一个可变参数列表(参数的数量不固定),新程序的参数列表,必须以NULL结尾。
1.1.单进程版本
我们先看一个单进程版的
#include<stdio.h> #include<unistd.h> int main() { printf("Before:I am a process,pid:%d,ppid:%d\n",getpid(),getppid()); execl("/usr/bin/ls","ls","-a","-l" ,NULL);//标准写法,最后一个必须使用NULL结尾 printf("After:I am a process,pid:%d,ppid:%d\n",getpid(),getppid()); }
上面这个执行了ls -al 命令,我们可以来验证一下
我们还发现,嗯?为什么After不见了? 我们先不谈
#include<stdio.h> #include<unistd.h> int main() { printf("Before:I am a process,pid:%d,ppid:%d\n",getpid(),getppid()); execl("/usr/bin/top","top" ,NULL);//标准写法,最后一个必须使用NULL结尾 printf("After:I am a process,pid:%d,ppid:%d\n",getpid(),getppid()); }
执行了之后直接进入top的界面
好了,上面这样子的就叫程序替换,我们发现替换之后的那段程序不跑啊
我们现在来研究一下这个过程啊
注意这里是单进程
首先在替换函数执行之前啊,进程加载好自己的PCB,mm_struct,页表了,但是当执行到替换程序的时候,进程将ls这个命令的代码和数据加载到内存里,替换了原来这个进程的代码和数据。这个时候后面的After就从进程中消失了
1.2.多进程版本
我们看看多进程版本的进程替换
#include<stdio.h> #include<unistd.h> int main() { pid_t id = fork(); if (id == 0)//子进程 { printf("Before:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(5); execl("/usr/bin/ls", "ls","-l", NULL);//标准写法,最后一个必须使用NULL结尾 printf("After:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); exit(1);//替换失败的时候,退出码是1 } pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success,father pid:%d,ret pid:%d\n", getpid(),ret);//ret是子进程的PID sleep(5); }
监视情况
现在我的问题是子进程执行excel会影响父进程吗?
答案是不会,因为子进程调用excel时加载外部可执行文件的数据和代码时,发生了写时拷贝,不影响父进程,只影响子进程excel后面的代码
我们来了解一下
- 进程替换前的效果图
当一个进程成功创建一个子进程之后,父子进程的情况如下图所示:
这个时候,我们这里先针对代码和数据进行分析,其他内容暂不做考虑,此时父子进程都没有修改代码和数据,因此,父子进程的代码和数据都是指向同一块内容的,也就是代码和数据是共享的,如果其中一方对数据进行修改,则这一方就会进行写时拷贝,如果想要执行不同的代码,则此时就要进行进程程序替换
- 进程替换之后的效果图
进程程序替换的原理:
假如刚开始父子进程都是执行a.exe的代码,后面,想要让子进程执行b.exe了,那么此时就要进行进程程序替换,替换的过程就是首先将b.exe从磁盘加载进内存,拿b.exe的代码和数据替换子进程的代码和数据,然后更新子进程的页表中的映射关系(注意,这里修改的是页表中的物理地址而不是虚拟地址,此时父子进程代码块中虚拟地址是一样的,但是通过页表映射出来的物理地址是不一样的),从而实现父子进程的代码彻底分离,此时父子进程的代码是互不干扰的,很好地满足了进程的独立性
1.3.进程替换的几个问题
1.程序替换有没有创建新进程?
进程替换不会创建新进程,因为进程替换只是将该进程的数据替换为指定的可执行程序。而进程PCB没有改变,所以不是新的进程,进程替换后不会发生进程pid改变。
从哪里看出来呢?
从上面我们多进程版本例子中发现子进程的PID一直没有变化
我们父进程等待时获得的pid和子进程本来的PID一模一样,就说明没有额外创建进程
进程替换就是用新的可执行程序的代码和数据取代掉 子进程的代码和数据(通过写实拷贝来实现),子进程的PCB,mm_struct,页表等数据结构都是原来那些(页表会更新物理内存的位置)
2.为什么子进程的打印After……没有执行?
进程替换后,如果替换成功后则替换函数下的代码不会执行,因为进程替换是覆盖式的替换,替换成功后进程原来的代码就消失了。同理在进程替换失败后会执行替换函数后的代码
3.我上面没有接受execl的返回值,那我怎么知道他失败了?
我们得知道,进程替换函数在进程替换成功后不返回,函数的返回值表示替换失败
很简单啊,替换失败了才会执行excel后面的代码,那么我就让excel后面的代码来做点事来告诉我们这个进程替换失败了,就像下面这样
4.进程替换成功后,子进程退出码是谁的?
进程替换成功后,退出码为替换后的进程的退出码
5.我们的CPU如何得知程序的入口在哪里
Linux中形成的可执行程序是有自己的格式的,ELF,可执行程序的表头。——表头里有可执行程序的入口 ,我们进程替换换了一个可执行文件,这个可执行文件也有表头,就能被cpu读取,也就能执行了
2.程序替换接口——exec族函数
3号手册里面有下面6种接口
我们来讲5个就够了
2.1.execl函数
execl函数是Linux系统中用于执行新程序的函数之一,它属于exec函数族的一部分。
这个函数的作用是在当前进程的上下文中启动一个新的程序,并替换当前进程的映像为新的程序映像。调用execl函数后,当前进程将停止执行,并由新的程序开始执行.
函数原型如下
参数说明:
path
:要执行的程序的路径。arg
:新程序的参数列表的开始,通常这会是新程序的名称(尽管这不是强制的,但它通常用于错误消息和程序内部)。...
:一个可变参数列表(参数的数量不固定),新程序的参数列表,必须以NULL结尾。
execl函数会根据提供的路径
path
找到并执行相应的程序,同时将arg0
及其后面的参数作为新程序的命令行参数传递。注意,参数列表必须以NULL结尾,这是告诉execl参数列表结束的标志。
我们可以把它看成exec+l,后面这个l我们可以看作是list,因为list和它的用法特别相关
我们在传参的时候,从第二个参数开始,我们是一个一个传进去的,结尾是NULL,这特别像链表
第一个参数是可执行文件存储的位置,如上
参数为什么是"ls" "-l"呢?
因为我们命令行里面的用法就是ls -l,他们是等效的
所以我们想使用ls -l的时候,就传"ls","-l",是不是特别简单?
命令行怎么写你就怎么传
例子看开头,我现在不想再讲了
但是有些东西我还是要强调
2.2.execlp
这个函数比exec多了一个lp,l是list的意思,那p呢?
- p是PATH的意思
该函数与 execl 类似,但是它自己会在系统的环境变量 PATH 指定的目录中查找可执行文件。它的原型如下:
int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
file 是要执行的可执行文件的文件名,arg0 是第一个参数,后续参数都是传递给可执行文件的命令行参数,以 NULL 结尾。
相比于execl函数,execlp函数的第一个参数能直接写文件名,系统会PATH环境变量里去查找
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<sys/types.h> int main() { pid_t id = fork(); if (id == 0)//子进程 { printf("Before:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(5); execlp("/usr/bin/ls", "ls", "-l", NULL);//可以带路径,也可以不带,标准写法,最后一个必须使用NULL结尾 printf("After:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); exit(1);//替换失败的时候,退出码是1 } pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success,father pid:%d,ret pid:%d\n", getpid(), ret);//ret是子进程的PID sleep(5); }
这个环境变量PATH里有ls的路径,而环境变量本来是bash进程的,但是环境变量具有全局属性,子进程会继承父进程的环境变量
2.3.execv:
- execv多出来的v是vector的意思,可以理解为数组
类似于 execl,但是允许传递一个参数数组给被执行的程序。它的原型如下:
int execv(const char *path, char *const argv[]);
- path 是要执行的可执行文件的路径,
- argv 是一个以 NULL 结尾的参数数组,其中每个元素都是一个字符串,表示命令行参数。
来看看用法吧
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<sys/types.h> int main() { pid_t id = fork(); if (id == 0)//子进程 { printf("Before:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(5); char* argv[] = { "ls","-a","-l",NULL}; execv("/usr/bin/ls",argv); printf("After:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); exit(1);//替换失败的时候,退出码是1 } pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success,father pid:%d,ret pid:%d\n", getpid(), ret);//ret是子进程的PID sleep(5); }
2.4.反思进程替换
我们来深入理解一下
ls是一个可执行程序,是由main函数的,而main函数接受命令行参数,那命令行参数哪里来?就是我们这里的“ls""-l""NULL”传进去的
在Linux中,所有的进程都是bash的子进程,所有子进程的启动都是使用exec族函数来启动的
于其说进程替换,不如说是进程加载
程序替换的本质就是加载 (可以看成一个加载器),有替换就是替换,没有就是程序加载
程序替换的本质是程序加载,因为在执行 exec 函数时,操作系统会加载新程序的可执行文件,并将其代码、数据和堆栈等部分加载到进程的地址空间中。这个过程涉及将新程序的内容从磁盘加载到内存中,为进程提供执行所需的资源。
因此,虽然我们常说是“程序替换”,但实际上更准确地说是将新程序加载到内存中,替换掉原有的程序,以实现进程的功能切换和更新。
2.4.execvp:
- p是PATH的意思,v是vector的意思
类似于 execv,但是它会在系统的环境变量 PATH 指定的目录中查找可执行文件。
它的原型如下:
int execvp(const char *file, char *const argv[]);
file 是要执行的可执行文件的文件名,argv 是一个以 NULL 结尾的参数数组,其中每个元素都是一个字符串,表示命令行参数。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<sys/types.h> int main() { pid_t id = fork(); if (id == 0)//子进程 { printf("Before:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(5); char* argv[] = { "ls","-a","-l",NULL}; execvp("ls",argv); printf("After:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); exit(1);//替换失败的时候,退出码是1 } pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success,father pid:%d,ret pid:%d\n", getpid(), ret);//ret是子进程的PID sleep(5); }
2.5.调用其他语言的程序
exec族函数能执行系统的命令,能执行我们的命令吗?
这个是完全没有问题的
我们下面用一个c语言的程序来调用一个c++程序
把它删了吧
接下来我们修改一下我们的makefile,让它一次性生成两个可执行程序
编译之后我们发现只能形成一个mytest,问题在哪里?
makefile默认执行前面那个,发现前面那个执行完了就没有依赖关系了,直接结束了,所以不能让它们两个放在前面
我们改成下面这个即可
要形成all就必须要mytest和otherexe,所以两个都会形成了
事实上这个all是makefile里的伪目标,是不会生成的
怎么样呢?
好了我们现在要用一个程序去调用另外一个程序
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<sys/types.h> int main() { pid_t id = fork(); if (id == 0)//子进程 { printf("Before:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(5); execl("./otherexe", "otherexe", NULL); printf("After:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); exit(1);//替换失败的时候,退出码是1 } pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success,father pid:%d,ret pid:%d\n", getpid(), ret);//ret是子进程的PID sleep(5); }
怎么样呢?
现在你肯定有个问题,明明我们调用otherexe的时候在命令行里是输入./otherexe,那为什么我们传参传的是otherexe而不是./otherexe?
其实你传 otherexe和./otherexe都行,都能运行
除此之外,我们可调用不同编程语言的可执行程序!!
我们下面再介绍一个
c语言调用脚本语言程序
有点神奇
来吧,用c语言调用一下这个脚本语言的程序
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<sys/types.h> int main() { pid_t id = fork(); if (id == 0)//子进程 { printf("Before:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(5); execl("/usr/bin/bash", "bash","test.sh", NULL); printf("After:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); exit(1);//替换失败的时候,退出码是1 } pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success,father pid:%d,ret pid:%d\n", getpid(), ret);//ret是子进程的PID sleep(5); }
太简单了
至于其他语言的程序,照样运行,不讲了,简单,太简单啦
为什么能跨语言调用?
所有的语言运行起来都是进程,所以就能调用!!!!就能拿c/c++调用
2.6.传命令行参数,环境变量
首先我们要知道环境变量具有全局性,进程程序替换是不会替换环境变量的
想要子进程继承全部的环境变量,不用管,直接就能拿到
说到环境变量,不急,我们先验证一下命令行参数能不能传递
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<sys/types.h> int main() { pid_t id = fork(); if (id == 0)//子进程 { printf("Before:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(5); const char* myargv[] = { "otherexe","-a","-b","-c",NULL }; execv("./otherexe", myargv); printf("After:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); exit(1);//替换失败的时候,退出码是1 } pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success,father pid:%d,ret pid:%d\n", getpid(), ret);//ret是子进程的PID sleep(5); }
我们就此验证命令行参数是可以传进去的
我们接下来验证环境变量
……
环境变量自动被子进程拿到了
1.我压根没有传环境变量啊??它怎么知道我的环境变量? 环境变量是上面时候给进程的?
环境变量也是数据!!!!!! 创建子进程的时候子进程就从父进程那里拿到环境变量了
2.我们程序替换不是换了子进程的代码和数据吗?
环境变量不会被替换啊!!!!
3.我如果想给子进程传递环境变量应该怎么办?
- 1.新增环境变量
- 2.删除之前的环境变量
我们得知道环境变量会从bash一路继承下来
4.我们怎么传递环境变量
单纯新增环境变量,在父进程里使用putenv()函数,会影响子进程
putenv 是 C 语言中的一个库函数,它定义在 <stdlib.h> 头文件中。这个函数用于将字符串添加到环境变量中,或者修改已经存在的环境变量的值。
int putenv(const char *string);
使用全新的环境变量,就使用execle()函数,那么替换后的代码切换后的环境变量就只是我们传入的表里的内容
我们来看一个例子
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<sys/types.h> int main() { putenv("PRIVATE_ENV=6666666"); pid_t id = fork(); if (id == 0)//子进程 { printf("Before:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(5); const char* myargv[] = { "otherexe","-a","-b","-c",NULL }; execv("./otherexe", myargv); printf("After:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); exit(1);//替换失败的时候,退出码是1 } pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success,father pid:%d,ret pid:%d\n", getpid(), ret);//ret是子进程的PID sleep(5); }
看到第25号了吗?就是我刚加上去的
我们看看bash里有没有
一点关系也没有
这好像继承啊,最顶上的很少,后面越来越多
5.强行传环境变量
2.5.1.execle:
- 多出来的e是enviroment的意思,l是list的意思
函数与 execl 函数类似,但允许在启动新程序时传递额外的环境变量。
它的原型如下:
int execle(const char *path, const char *arg, ..., char *const envp[]);
path 是要执行的可执行文件的路径,arg 是要传递给新程序的命令行参数,后面的参数是额外的环境变量,以 NULL 结尾。
我们先用这个函数来传递一下系统的环境变量
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<sys/types.h> int main() { extern char** environ; putenv("PRIVATE_ENV=6666666");//创建环境变量 pid_t id = fork(); if (id == 0)//子进程 { printf("Before:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(5); const char* myargv[] = { "otherexe","-a","-b","-c",NULL }; execle("./otherexe","otherexe","-a","-w","-v",NULL,environ ); printf("After:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); exit(1);//替换失败的时候,退出码是1 } pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success,father pid:%d,ret pid:%d\n", getpid(), ret);//ret是子进程的PID sleep(5); }
怎么样呢?
我们能不能自定义环境变量?
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<sys/types.h> int main() { pid_t id = fork(); if (id == 0)//子进程 { printf("Before:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(5); const char* myenv[] = {//自定义环境变量 "MY_VALUE1=1111", "MY_VALUE2=2222",NULL }; execle("./otherexe","otherexe","-a","-w","-v",NULL,myenv ); printf("After:I am a process,pid:%d,ppid:%d\n", getpid(), getppid()); exit(1);//替换失败的时候,退出码是1 } pid_t ret = waitpid(id, NULL, 0); if (ret > 0) printf("wait success,father pid:%d,ret pid:%d\n", getpid(), ret);//ret是子进程的PID sleep(5); }
我靠,系统的环境变量没了,只剩自己的了!!!!
注意:
- 1.导环境变量的数组最后以NULL结尾
- 2.导入自定义环境变量后原系统环境变量的值被清空,这种导入环境变量的方式为覆盖式导入
2.7.用法总结
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
3. 系统调用——execve
我们知道上面那些都是3号手册的库函数,那么exec族还有2号手册的!!!!
2号手册下面只有一个
execve是系统调用
那么这个系统调用和上面的库函数的关系是什么?
- 3号手册的那些库函数最终都是调用2号手册execve这个系统调用
4.总结
1,程序替换一旦成功,exec后面的代码不在执行。因为被替换掉了,这也是什么代码没有输出execl end的原因了
2,exec函数调用成功,那么它实际上不会有返回值;调用失败,它会返回-1
3,exec函数不会创建新的进程。它们只是在当前进程的上下文中启动另一个程序
4,创建一个进程。我们是先创建PCB、地址空间、页表等再先把程序加载到内存
如果先加载的话,页表都没办法映射的
5,程序替换的本质就是加载 (可以看成一个加载器),有替换就是替换,没有就是程序加载
程序替换的本质是程序加载,因为在执行 exec 函数时,操作系统会加载新程序的可执行文件,并将其代码、数据和堆栈等部分加载到进程的地址空间中。这个过程涉及将新程序的内容从磁盘加载到内存中,为进程提供执行所需的资源。
因此,虽然我们常说是“程序替换”,但实际上更准确地说是将新程序加载到内存中,替换掉原有的程序,以实现进程的功能切换和更新。
6,程序运行要加载到内存;为什么?
冯诺依曼体系规定;
如何加载的呢?
就是程序替换:程序替换是操作系统的接口,所谓的把磁盘里的数据加载到内存就是把磁盘设备的数据拷贝到内存里。把数据从一个硬件搬到另一个硬件,只有操作系统能做