介绍:
进程程序替换是指将一个进程中正在运行的程序替换为另一个全新的程序的过程,但替换不是创建新进程,只是将对应程序的代码和数据进行替换。具体来说,这个替换过程涉及将磁盘中的新程序加载到内存结构中,并重新建立页表映射,然后更新一系列的组件,使得执行程序替换的进程(如子进程)与新程序关联起来。这样,该进程就不再执行原来的程序,而是开始执行新的程序。
进程替换的使用在很多情况下都是使用子进程来完成。当我们想让一个子进程执行与父进程不同的代码片段时,就可以通过进程程序替换来实现,这时父进程完成自己的程序,子进程进行替换。这里需说明一下,子进程的进程替换不会影响父进程,因为进程具有独立性。当刚开始创建子进程时,子进程内部的指针指向父进程的数据和代码,子进程一旦发生替换时(改动了原本的数据),要替换的部分将会进行写时拷贝,开辟一块空间。
替换原理
通常,用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支)。子进程进行替换往往要调用一种exec函数,以便在子进程中执行另一个程序,而父进程可以继续执行其原始任务。
这里说明一下exec函数,当该进程调用exec函数时,系统就会进行程序替换,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行,将新的代码和数据替换到原本的进程结构中。这里注重强调一下,调用exec进行进程替换并不创建新进程,替换的本质就是加载,将磁盘上的数据和代码加载到内存中,从而更新一系列数据重新建立起关系,所以调用exec前后该进程的 id 并未改变。
我们来研究一下exec替换函数。此函数共有以下六种形式,统称exec函数:
头文件
#include <unistd.h>
exec* 函数
int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);int execve(const char *path, char *const argv[], char *const envp[]);
exec* 函数的共性
1,程序一旦被 exec* 替换成功,exec* 后续的代码不在执行,因为此时的进程被替换掉了。若替换失败,此进程后面的代码才继续执行。
2,exec* 只有失败返回值-1,没有成功返回值,因为一旦成功此进程将被替换。
3,替换完成,不会创建新的进程,即PCB结构。
exec* 的各种类型函数中,虽说各有各的不同,但是这里只要明白里面的各种参数功能,就能够理解exec* 函数是如何进行替换的。
下面的演示为了方便,这里替换的进程统一用Linux系统下的指令。这里先说明一下,在Linux中,指令本质上是程序,各种命令实际上都是可执行程序。当进程执行时,它会加载程序到内存中,并通过虚拟地址空间与物理内存之间的映射关系来执行这些指令。
形式一:
int execl(const char* path, const char* arg, ...);
path: 要替换程序的路径。
arg:表示要替换的进程程序,这里参数可以有多个,用于指定程序的输入、选项或其他必要的信息,但最后必须以NULL结尾,以标记参数列表结束。
例:execl("/usr/bin/ls", "ls", "-l", "-a", NULL); 将此时进程替换成在路径 "/usr/bin/ls" 下的 ls -l -a 进程,若在此路径下不存在指定的进程,则替换失败,如:execl("/usr/bin/l", "ls", "-l", "-a", NULL); 没有此路径,替换失败。
形式二:
int execlp(const char* file, const char* arg, ...);
file:这是你要执行的程序的名称(即可执行文件名)。如果
file
中不包含路径信息(即只是程序名而不是完整的路径),则会在PATH
环境变量中定义的目录列表中查找该程序。arg:与execl中的arg一样,表示要替换的进程程序。
例:execlp("ls", "ls", "-a", "-l", NULL); 将此时进程替换成 ls -a -l 进程。注意,这里的两个参数 "ls" 不重复,各自表示的含义不一样。
在exec*的各种形式函数中,后面带 p 表示 PATH 环境变量,这时只用传达进程名称即可,不用告诉系统程序在哪里,系统在替换时会自动去PATH环境变量中查找。
形式三:
int execv(const char* path, char* const argv[]);
argv[]:用指针数组argv来表示替换的进程程序。
例:char* argv = { "ls", "-a", "-l" }; execv("/usr/bin/ls", argv); 效果与上相同。
int execvp(const char* file, char* const argv[]);
例:char* argv = { "ls", "-a", "-l" }; execvp("ls", argv); 效果类同
在exec*的各种形式函数中,后面带 v 的表示使用指针数组的形式表示要进行替换的进程程序。后面带 l 的表示以参数列表的形式表示要进行替换的进程程序。
形式四:
exec* 函数后面带 e 的表示环境变量。至于为什么引入此种功能我们先来了解当替换我们自己写的程序时的情况,这里以execl函数为例。说明一下,程序替换可替换在此系统下的所有高级语言程序,即包括python、C/C++、java等。因为所有的语言运行之后都是进程。这里以C/C++为例。
code2.cpp文件
#include <iostream>
#include <cstdio>
using namespace std;
int main(int argc, char* argv[], char* env[])
{
for (int i = 0; argv[i]; i++)
{
fprintf(stdout, "argv[%d]: %s\n", i, argv[i]);
fprintf(stdout, "env[%d]: %s\n", i, env[i]); //运行此时的环境变量
}
cout << "code2.exe option" << endl;
return 0;
}
code.cpp文件#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
int main()
{
pid_t id = fork();
if (id == 0) // 子进程进行进程替换
{
cout << "I am a child process, pid = " << getpid() << endl;
cout << "exec is begining" << endl;
execl("./code2.exe", "code2.exe", "-a", "-b", NULL); // 运行自己的程序code2.exe
cout << "exec end" << endl; // execl之后的代码不会运行,因为此时进程被exec替换
}
//父进程执行自己的程序
int w = wait(NULL);
if (w > 0)
cout << "wait success" << endl;
else
cout << "wait failure" << endl;
return 0;
}
运行code.exe后[zhu@VM-16-10-centos day2]$ ./code.exe
I am a child process, pid = 29117
exec is begining
argv[0]: code2.exe
env[0]: XDG_SESSION_ID=6732
argv[1]: -a
env[1]: HOSTNAME=VM-16-10-centos
argv[2]: -b
env[2]: TERM=xterm
code2.exe option
wait success
这里需要说明一下,进程在替换时是不会替换掉环境变量的数据,也就是说以上的程序code2.exe 默认可以通过地址空间继承的方式,让子进程拿到环境变量数据,所以,当我们调用子进程可以输出整个系统的环境变量,因为所有进程都是shell的子进程。但是若是子进程或孙子进程新增环境变量,父进程的进程地址空间中是没有存储的,若父进程想使用子进程新增的环境变量,这时就需要使用 execl* 函数后面带 e 类型的接口,这里以execle为例。
int execle(const char* path, const char* arg, ..., char* const envp[]);
envp[]:自定义存储环境变量的指针数组envp[],将此进程自定义的环境变量表覆盖替换程序的环境变量表。
exec* 后缀加上e的,表示需要传入环境变量表,此时将覆盖原本的环境变量数据。
exec*函数的演示与解说:
[zhu@VM-16-10-centos day2]$ cat code.cpp
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
int main()
{
char* env[] = { "AAA=aaa", "BBB=bbb", "CCC=ccc", "DDD=ddd" };
pid_t id = fork();
if (id == 0) // 子进程进行进程替换
{
cout << "exec is begining" << endl;
execle("./code2.exe", "code2x.exe", "-a", "-b", NULL, env); // 执行传入环境变量表的程序进程code2.exe
cout << "exec end" << endl; // execl之后的代码不会运行,因为此时进程被exec替换
}
//父进程执行自己的程序
int w = wait(NULL);
if (w > 0)
cout << "wait success" << endl;
else
cout << "wait failure" << endl;
return 0;
}
[zhu@VM-16-10-centos day2]$ cat code2.cpp
#include <iostream>
#include <cstdio>
using namespace std;
int main(int argc, char* argv[], char* env[])
{
for (int i = 0; env[i]; i++)
{
fprintf(stdout, "env[%d]: %s\n", i, env[i]); // 若这里没有传入环境变量表,将输出原有的环境变量,即系统下的环境变量
}
cout << "code2.exe option" << endl;
return 0;
}
[zhu@VM-16-10-centos day2]$ ./code.exe
exec is begining
env[0]: AAA=aaa
env[1]: BBB=bbb
env[2]: CCC=ccc
env[3]: DDD=ddd
env[4]:
code2.exe option
wait success
其它exec*接口的类型效果都是一样的,都是根据参数来实现具体的形式。