进程替换出现的背景
我们可以用fork函数来创建子进程,使父子进程同时出现。正所谓子承父业,子进程在刚被创建时和父进程共享数据和代码,但是操作者想让子进程执行一个全新的程序,这个使子进程执行全新程序的操作叫做 进程程序替换。
- 替换原理
1.1替换的原理和底层
进程不变,仅仅替换当前 进程的 代码和数据的技术,叫做 进程程序替换。并 没有创建新的进程。
程序替换本质就是 把程序的代码+数据,加载到特定进程的上下文中。C/C++程序要运行,必须要先加载内存中,如何加载呢?是通过加载器,加载器的底层原理就是一系列的exec*程序替换函数。
用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。
1.2进程替换时创建新进程没?
进程程序替换之后,该进程对应的 PCB、 进程地址空间以及 页表等数据结构都没有发生改变,只是 进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有改变。
1.3对父进程的代码和数据的影响?
子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要 将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。
2.替换函数
提起进程替换,就不得不说到替换函数,替换函数有6种,他们均是以exec开头的,又把他们统称为exec函数。
exec把当前进程映像替换成新的程序文件,而且该新程序通常从main函数开始执行。进程ID并不改变。我们称调用exec的进程为 调用进程(calling process),称新执行的程序为新程序(new program)。
2.1六大函数介绍
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
函数参数:
path:用来替换的程序所在的路径
file:程序名
arg, …:列表的形式传参
arg[]:数组的形式传参
envp[]:自己维护的环境变量
特殊记忆法进行特殊记忆
替换函数接口 | 参数格式 | 采用方法 |
l | list | 参数采用列表方式 |
v | vector | 参数采用数组方式 |
p | path | 自动搜索环境变量PATH |
e | env | 自己维护环境变量,或者说自定义环境变量 |
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
execl | 列表(l) | 是(path) | 是 |
execv | 数组(v) | 是(path) | 是 |
execlp | 列表(l) | 否 | 是 |
execvp | 数组(v) | 否 | 是 |
execle | 列表(l) | 是(path) | 否,自己组装环境变量 |
execve | 数组(v) | 是(path) | 否,自己组装环境变量 |
接下来让我们一一介绍一下:
1.execl函数
int execl(const char *path, const char *arg, ...);
第一个参数是要执行程序的路径, 第二个参数是可变参数列表,表示你要如何执行这个程序, 并以NULL结尾。
假如要执行ls指令:
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
2.execv函数
int execv(const char *path, char *const argv[]);
第一个参数是要执行程序的路径, 第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序, 数组以NULL结尾。
假如要执行ls指令:
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
3.execlp函数
int execlp(const char *file, const char *arg, ...);
第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。
假如要执行的是ls程序。
execlp("ls", "ls", "-a", "-i", "-l", NULL);
4.execvp函数
int execvp(const char *file, char *const argv[]);
第一个参数是要执行程序的名字, 第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序, 数组以NULL结尾。
假如要执行ls指令:
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
5.execle函数
int execle(const char *path, const char *arg, ...,char *const envp[]);
第一个参数是要执行程序的路径, 第二个参数是可变参数列表,表示你要如何执行这个程序,并 以NULL结尾, 第三个参数是你自己设置的环境变量。
假如你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。
char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
6.execve函数
int execve(const char *path, char *const argv[], char *const envp[]);
第一个参数是要执行程序的路径, 第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序, 数组以NULL结尾, 第三个参数是你自己设置的环境变量。
假如设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量:
char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2023", NULL };
execve("./mycmd", myargv, myenvp);
2.2函数的返回值
这些函数如果调用成功,则加载指定的程序并从启动代码开始执行,不再返回。
如果调用出错,则返回-1。
大胆的理解:只要返回值了,那就意味着调用失败了。
2.21六大函数的关系
只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。
测试实例:
int main()
{
// 自己组装的环境变量
char* myenv[] = {"MYENV=you can see my", NULL};
// 列表形式传参
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
execp("ls", "ls", "-l", "-a", NULL);
exece("./mycmd", "mycmd", NULL, myenv);
// 数组形式传参
char* const argv[] = {"ls", "-l", "-a", NULL};
execv("/usr/bin/ls",argv);
execvp("ls", argv);
char* const agrv1[] = {"mycmd", NULL};
execve("./mycmd", agrv1, myenv);// 调用自己的程序
}
还有几点需要解释一下:
程序替换成功后,运行完新程序,则程序直接退出;程序替换成功后,原进程没有退出,使用原进程运行新程序