文章目录
为什么要使用进程程序替换?
一般情况下,我们写程序的时候,只能调用同一语言的接口。但是如果我们使用的C++语言,但是想要调用python或者java的程序接口,该怎么办?这时,进程程序替换就起到了作用。
什么是进程程序替换
进程程序替换:将可执行程序加载到内存,并且重新调整子进程的页表映射,使之指向新的进程的代码和数据段,这种过程就叫做程序替换。
一般情况下,会使用fork()函数创建子进程,然后让子进程进行进程程序替换,父进程继续执行父进程的逻辑。
进程程序替换的原理
观察现象
我们写了这样一个程序,这里的execl()函数就是进程程序替换函数,先就这样用着,后文会详细讲解这类函数。
本程序很简单,就是最开始打印了一下"before,进程的pid,ppid",再进行程序替换,最后再打印一下"after,进程的pid,ppid"。
运行之后,观察结果:
运行之后,发现两个很奇怪的情况:
- 只打印了before,没有打印after
- 打印之后执行了ls命令,可是我们知道,我们并没有输入任何指令
从这个简单的例子中,我们认识了进程程序替换。
可以观察到,我们只用的execl()函数中,里面有ls命令,所以是execl函数直接进行了程序替换。导致程序运行到execl之后,跑去执行ls命令了,不再执行本来的命令。
同样的情况,我们可以将execl()函数中的ls命令改为top命令,观察是不是直接执行了top函数。
根据上图所示可以看到确实执行了top执行,所以确实是我们所说那样。
原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
程序替换的原理:
- 将磁盘中的程序,加载入内存结构。
- 重新建立页表映射,如果谁执行程序替换,就重新建立子进程的页表映射。
父进程的映射关系
程序替换之后,父子进程的映射关系
调整子进程的页表,让其不再与父进程代码和数据有任何关系,而是指向自己的代码和自己的数据区。
注意:这个过程中没有创建新的进程!
总结:
- 让fork创建子进程,不让子进程执行父进程的程序,而是执行磁盘中新的程序
- 执行磁盘中新的程序,但是不创建新的进程
- 子进程的内核数据结构没有改变,改变的是,页表右边的部分 — 也就是虚拟内存和物理内存的映射关系
进程程序替换的接口 – exec*家族
输入man execl
查看手册
可以发现所有函数都有相同的开头,就是exec,所以后面的后缀才能区分他们。
提前提出总结:
- exec + l这样的后面加上l的函数,这里的l表示list(列表),就表示参数中会有…,如果不使用就要在…位置传上NULL (…是可变参数列表)
- exec + v这样的后面加上v的函数,这里的v表示vector(数组),就表示参数的第二个函数是argv[],要传递命令行参数
- exec + p这样的后面加上p的函数,这里的p表示path,就表示函数的第一个参数不用像execl这样没p的函数一样传path(路径名),传递file(文件名)就行
- exec + e这样的后面加上e的函数,这里的e表示environment,就表示函数的最后一个参数需要传递环境变量
execl()
int execl(const char *path, const char *arg, …);
- 这是exec + l,说明是list,并且最后一个参数是可变参数。
上面图片大概解释了函数的使用逻辑。
path
- 可执行程序的路径。
比如:如果是执行“ls”命令,该命令在"/usr/bin/ls"路径下,所以在该参数位置,填下这个路径。
arg
- 命令怎么写,这里的命令行参数就如何填写
- 解决如何执行程序的问题
例如:你写的命令是"ls -a -l",那么在命令行参数填写*“ls”, “-a”,“-l”*就行
…
- 可变参数,可以传多个参数
如果你不使用,就填上NULL就行
** 代码演示**
运行结果:
程序替换没有返回值
----程序替换是没有返回值的,如果有返回值就代表程序替换失败了!
execv()
int execv(const char *path, const char *argv[]);
execv函数是exec + v,说明是vector,那么第二个参数就是argv[],并且没有p,说明第一个参数需要传path。
参数:
path
- 可执行程序的路径。
比如:如果是执行“ls”命令,该命令在"/usr/bin/ls"路径下,所以在该参数位置,填下这个路径。
argv[]
- 和execl一样,但是传参的方式略有区别
- 这里需要注意的是,最后一个参数必须是NULL
- 如果想不报警告,将参数强转成(char*)类型就行
代码演示
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
9
10 printf("开始前:\n");
11
12 pid_t id = fork();
13 if(id == 0)
14 {
15 //child
16 char *const myargv[] = {
17 (char*)"ls",
18 (char*)"-l",
19 (char*)"-a",
20 NULL
21 };
22 execv("/usr/bin/ls", myargv);
23 }
24 else
25 {
26 pid_t ret = waitpid(id, NULL,0);
27 if (ret == id) printf("进程等待成功\n");
28 }
29 return 0;
30 }
运行结果:
execlp()
int execlp(cosnt char *file, const char *argv, …);
- exec + l + p,从而得知是list和path,,得到:
- 第一个参数是file,而不是path
- 第二个参数是argv而不是argv[]
- 第三个参数是…
参数:
file
- 填入想要执行的程序的文件名即可
argv
- 命令怎么写,这里的命令行参数就如何填写
- 解决如何执行程序的问题
例如:你写的命令是"ls -a -l",那么在命令行参数填写*“ls”, “-a”,“-l”*就行
…
- 可变参数,可以传多个参数
如果你不使用,就填上NULL就行
代码演示
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
9
10 printf("开始前:\n");
11
12 pid_t id = fork();
13 if(id == 0)
14 {
15 execlp("ls", "ls","-l","-a", NULL);
16 }
17 else
18 {
19 pid_t ret = waitpid(id, NULL,0);
20 if (ret == id) printf("进程等待成功\n");
21 }
22 return 0;
23 }
运行结果:
execvp()
int execvp(const char *file, const char *argv[]);
- exec + v + p,从而得知是vector 和path:
- vector,表示第二个参数是argv[]
- path,表示第一个参数是file
代码演示
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8 {
9
10 printf("开始前:\n");
11
12 pid_t id = fork();
13 if(id == 0)
14 {
15 //child
16 char *const myargv[] = {
17 (char*)"ls",
18 (char*)"-l",
19 (char*)"-a",
20 NULL
21 };
22 // execv("/usr/bin/ls", myargv);
23 // execlp("ls", "ls","-l","-a", NULL);
24 execvp("ls", myargv);
25 }
26 else
27 {
28 pid_t ret = waitpid(id, NULL,0);
29 if (ret == id) printf("进程等待成功\n");
30 }
31 return 0;
32 }
运行结果:
execle()
int execle(const char *path, const char *argv, …, char *const envp[]);
- exec + l + e,是list和environment,得到:
- 没有p,说明第一个参数是const char* path,需要填写路径
- 有l,说明第二个参数传的argv不是argv[]
- 有l,说明第三个参数是…
- 有e,说明第四个参数是envp[]环境变量
参数
envp
- 环境变量
在进行代码演示之前,需要明确:
子进程可以继承父进程的环境变量!程序替换的时候不替换环境变量
如果我们想给子进程传递环境变量,该怎么做呢?
- 新增环境变量
- 彻底替换环境变量
新增环境变量需要一个函数putenv
代码演示
如果没有手动导入,而是使用下面的environ,也就是系统的环境变量,如下图
这是mycmd.c文件
那么得到的结果是这样的。
可以观察到PATH是可以打印出来的
如果使用我们自己手动导入的环境变量,例如下图这样
这是mycmd.c文件
运行结果如下图所示,可以发现,PATH的值为空
这是otherExc.cpp文件
这是mycmd.c文件
调用自己导入的环境变量
运行结果
从上面的演示可以得出:添加环境变量给目标进程,是覆盖式的!!!所以最后的环境变量只剩下MYPATH
子进程会继承父进程的环境变量!!!
- 子进程会继承父进程的环境变量,当父进程调用fork()创建子进程时,子进程会继承父进程的所有环境变量。
- 当子进程调用execlp()等函数执行其他程序时,子进程也会继承父进程的环境变量。
- 如果需要在子进程中更改环境变量,可以使用setenv()或putenv()等函数进行更改。
- 但是,更改的环境变量只会影响当前进程和它的子进程,并不会影响父进程或其他进程的环境变量。