标题:[Linux] Linux 进程程序替换
个人主页@水墨不写bug
(图片来源于网络)
目录
正文开始:
O、前言
在之前,我们已经学习了什么是进程的创建,退出,等待,以及有关进程地址空间的问题。接下来的进程程序替换依然是属于进程控制的范畴,但是在实际应用中,是非常重要的一种技术。
本文不直接像书本上一样,一开始就说一大堆概念,让人摸不清头脑,而是现观察进程程序替换的现象,然后再从现象中在进一步了解进程程序替换。
一、进程程序替换的直观现象(什么是进程程序替换?)
进程程序替换有一些专门(对口)的函数,我们可以通过man手册查到其中的一个函数:
接下来我们直接看一段代码,用一用这个函数:
cpp
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("process begin...\n");
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("process end...\n");
return 0;
}
两个打印语句分别标识进程开始与结束,话不多说,直接开始运行,结果如下:
我们惊奇的发现,执行的现象与使用ls的现象差不多!
通过进一步观察,可以发现:
1)在调用execl函数之前的代码(打印process begin 的语句)执行了;
2)调用execl函数就好像使用了 "ls" 命令;
3)调用execl函数之后的代码没有被执行。
为什么会发生这样的情况呢?别急,这就需要深入了解调用execl函数这一动作背后到底发生了什么------于是,我们需要谈一谈进程替换的原理。
二、进程程序替换的原理
我们知道,我们写好并编译好的文件是一个 独立的 可执行程序:
包括我们自己写的mytest,或者是系统上已经装好的ls等指令,都是一个独立的可执行程序。
execl 这类函数的功能就是,把一个新的独立的可执行程序覆盖式的加载到原来的运行起来的进程中,从而实现程序替换。
一个进程,包括内核数据结构 + 代码和数据;这里的程序替换,替换的不仅仅是数据,还要替换代码!这也就意味着原来的代码和数据就被覆盖了,于是进程会从执行execl函数这一行开始,直接执行新加载的代码和数据。原来的数据自然就丢失了:这也就解释了 执行 execl 就像执行力 ls指令 ,在最后执行结束之后并没有打印 "process end" 的原因:
1)执行execl之后,ls的代码和数据被加载的内存中,覆盖替换了原来的代码和数据;
2)原来的 打印 "process end" 的代码由于被覆盖而丢失,所以没有执行;
3)最终的返回值是ls指令的返回值,而不再是原来被覆盖的进程的返回值。
三、进程程序替换的函数(怎么使用进程程序替换?)
我们可以通过进程程序替换的函数名称来略知一二,但是在那样通过函数名称来快速记忆之前,我们还是需要先一个一个了解进程程序替换的函数们:
list(初始化列类型):
1.execl
就像我们之前演示的那样,其实就是execl函数使用方法,接下来需要对这个函数的传参细节做一些深入理解:
参数列表:
*pathname: 需要替换的可执行程序的位置,需要指明具体的位置,既可以使用绝对路径,也可以使用相对路径。
(比如想要替换ls命令,ls命令本质是一个可执行程序,位于/usr/bin/ls,于是我们第一个参数需要这样传递:"/usr/bin/ls")
*arg: 需要替换的可执行程序的名称
(需要替换的ls命令的名称就是ls,于是需要传递:"ls")
... : 参数列表
(类似于printf的参数列表不限制打印的参数的个数一样,我们在命令行上想要使用不同的命令,传递的参数的个数是不同的,于是通过参数列表,我们可以传递不同的参数个数,来达到正确执行不同指令的目的)
比如下面的这一个实例就很好的体现了上述的规则:实例一:
makefile:
当我们想要一次生成多个目标文件,那么可以定义一个伪目标:all
在伪目标后面 + " : "+ " 需要的依赖文件名称 "
在下文表明生成依赖文件的依赖方法即可:
bash.PHONY:all all:mytest mmtest mmtest:mytest.cc g++ -o $@ $^ mytest:pra_exec.c gcc -o $@ $^ .PHONY:clean clean: rm -r mytest mmtest
pra_exec.c:
cpp#include<stdio.h> #include<unistd.h> int main() { printf("process begin...\n"); execl("/usr/bin/ls","ls","-a","-l",NULL); printf("process end...\n"); return 0; }
cpp#include<iostream> #include<unistd.h> using namespace std; int main() { cout<<"C++进程开始运行"<<endl; execl("./mytest","./mytest",NULL); cout<<"C++进程结束运行"<<endl; return 0; }
由于我们已经编写了makefile,所以只需要make,就可以生成两个可执行程序。
运行结果:
在进程运行的过程中,我们发现进行了两次进程程序替换:
./mmytest (C++程序) ---> ./mytest(C程序) ---> /usr/bin/ls (系统命令C程序)
我们可以通过观察打印信息来观察。
到这里我们可以得出结论:
execl可以替换任何语言的可执行程序。(包括java,python,shell脚本语言等),唯一不同的是通过execl调用的时候传递的参数不同。
接下来的介绍只给出具体函数的使用实例,不再重复介绍同样性质的参数。
2.execlp
cpp
#include <unistd.h>
int main()
{
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
exit(0);
}
3.execle
cpp
#include <unistd.h>
int main()
{
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
exit(0);
}
vector(数组类型):
1.execv
cpp
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
//带v的需要传入数组形式的参数列表
execv("/bin/ps", argv);
exit(0);
}
2.execvp
cpp
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
exit(0);
}
3.execve
cpp
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
函数解释
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值。
命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
exec*函数簇函数之间的关系:
本质上,这些exec*函数都是对execve函数的封装,为什么?
因为execve是一个系统调用函数!!本质上底层调用的都是相同的形式调用的execve,只不过是上层的封装转换的参数传递的方式。
四、进程程序替换的实际应用场景(程序替换的意义?)
在我们的项目中,我们的父进程一直是运行的,不能说父进程执行了想要执行ls命令,在执行了之后自己却退出了。所以,我们在一般情况下,一定不能让父进程发生程序替换,不然父进程的代码和数据被子进程替换之后,代码和数据就丢失了!
所以我们需要fork创建子进程,让子进程来代替父进程来执行exec*这一类的函数,让子进程来发生程序替换,让子进程的代码和数据被替换掉,这样父进程就可以保留!
进程替换的常见场景:
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
完~
未经作者同意禁止转载