目录
什么是进程替换
替换原理
exec函数
exec* 函数的共性
什么是进程替换
进程程序替换是指将一个进程中正在运行的程序替换为另一个全新的程序的过程,但替换不是创建新进程,只是将对应程序的代码和数据进行替换。具体来说,这个替换过程涉及将磁盘中的新程序加载到内存结构中,并重新建立页表映射,然后更新一系列的组件,使得执行程序替换的进程(如子进程)与新程序关联起来。这样,该进程就不再执行原来的程序,而是开始执行新的程序。
试想我们创建一个进程的时候是先创建pcb,地址空间,页表等,还是先把程序加载到内存。答案是前者,而程序替换就是不再创建pcb,本质工作就是加载!!!
进程替换的使用在很多情况下都是使用子进程来完成。当我们想让一个子进程执行与父进程不同的代码片段时,就可以通过进程程序替换来实现,这时父进程完成自己的程序,子进程进行替换。这里需说明一下,子进程的进程替换不会影响父进程,因为进程具有独立性。当刚开始创建子进程时,子进程内部的指针指向父进程的数据和代码,子进程一旦发生替换时(改动了原本的数据),要替换的部分将会进行写时拷贝,开辟一块空间。
简而言之,进程程序替换就是在运行自己程序的时候可以替换成其他我想执行的程序,在进程程序替换后,我原来的程序就不存在了,进程中就只有我替换的程序了。
替换原理
一般来说用fork创建子进程去执行我想替换的程序,fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支)。想要完成程序的替换也就是进程属性的修改,这是操作系统内核才有的权限,必须要由操作系统来完成,所以我们必须要使用对应的系统调用。进程程序替换往往要调用一种exec函数以执行另一个程序,当进程调替换时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。这里注重强调一下,调用exec进行进程替换并不创建新进程,替换的本质就是加载,将磁盘上的数据和代码加载到内存中,从而更新一系列数据重新建立起关系,所以调用exec前后该进程的 id 并未改变。
exec函数
此函数共有六种以exec开头的函数,统称exec函数:
头文件:
#include <unistd.h>`
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[]);
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 的表示以参数列表的形式表示要进行替换的进程程序。
形式四:
int execle(const char* path, const char* arg, ..., char* const envp[]);
envp[]:自定义存储环境变量的指针数组envp[],将此进程自定义的环境变量表覆盖替换程序的环境变量表。
exec* 函数后面带 e 的表示环境变量。至于为什么引入此种功能我们先来了解当替换我们自己写的程序时的情况,这里以execl函数为例。说明一下,程序替换可替换在此系统下的所有高级语言程序,即包括python、C/C++、java等。因为所有的语言运行之后都是进程。这里以C/C++为例
code.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;}
run.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("./code.exe", "code.exe", "-a", "-b", NULL); // 运行自己的程序code.execout << "exec end" << endl; // execl之后的代码不会运行,因为此时进程被exec替换}//父进程执行自己的程序int w = wait(NULL);if (w > 0)cout << "wait success" << endl;elsecout << "wait failure" << endl;return 0;}
编译code.cpp为code.exe,run.cpp为run,运行run输出:
I am a child process, pid = 29117
exec is begining
argv[0]: code.exe
env[0]: XDG_SESSION_ID=6732
argv[1]: -a
env[1]: HOSTNAME=VM-16-10-centos
argv[2]: -b
env[2]: TERM=xterm
code.exe option
wait success
这里需要说明一下,进程在替换时是不会替换掉环境变量的数据,也就是说以上的程序run默认可以通过地址空间继承的方式,让子进程拿到环境变量数据,所以,当我们调用子进程可以输出整个系统的环境变量,因为所有进程都是shell的子进程。但是若是子进程或孙子进程新增环境变量,父进程的进程地址空间中是没有存储的,若父进程想使用子进程新增的环境变量,来执行其他程序,可以使用putenv来为子进程添加新的环境变量,但创建出来的环境变量不会影响前面的父进程,只会影响后面的替换来的程序,如果想新替换来的程序使用全新的环境变量,这时就需要使用 execl* 函数后面带 e 类型的接口。 exec* 后缀加上e的,表示需要传入环境变量表,此时将覆盖原本的环境变量数据。
可以看到我们成功调用了一个c++程序,并且传递使用了新的环境变量。
(最后再列出所有exec函数的联系与区别)
题外话:
查看进程语句:
while :; do ps ajx|head -1 && ps ajx |grep myprocess|grep -v grep;sleep 1;done
使用grep -v grep是为了排除grep自身的进程记录