欢迎来到博主的专栏——从0开始linux
博主ID:代码小豪
文章目录
进程程序替换的主要的函数为execl系列,注意这个execl可不是windows的办公软件,而是c标准库中的函数,由于其运行原理与命令行参数和环境变量相关,因此读者在观看这篇博客时,请先补充一下命令行参数与环境变量的知识。当然博主在之前也写了一篇相关的内容。
进程程序替换
进程替换的需要用到以下的函数
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 execvpe(const char *file, char *const argv[],char *const envp[]);
excel函数
excel函数原型如下:
int execl(const char *path, const char *arg, ...);
其中const char *arg, ...
是可变参数列表,如果你对C标准库中的函数原型比较了解,那么这个这边参数列表你一定见过,那就是printf函数的参数,也有一个可变参数列表。
int printf(const char *format, ...);
可变参数列表的意思就是,在这个函数当中,你可以传递非定数的参数,比如可以传递一个,也可以传递多个。
博主先不将execl函数的参数到底有什么用,而是先写出使用execl的代码,再讲解它的原理。
execl函数是定义在标准库中的函数,在linux系统中,其定义在<unistd.h>头文件中,而在windows系统,其定义在<process.h>头文件中。
#include<unistd.h>
int main()
{
execl("/usr/bin/ls","ls","-a","-l",nullptr);
return 0;
}
我们编译并运行该程序,查看结果。
嘶,这个进程的运行结果怎么有点眼熟啊,这不是和我们在命令行中输入ls -a -l
的结果一样吗?
其实这就是为什么将execl函数叫做进程程序替换,因为ls本身就是一个程序,因此运行ls -a -l时,它也就成为了一个进程,执行execl("/usr/bin/ls","ls","-a","-l",nullptr);
就相当于将ls进程,替换了test_exec进程。
那么它的参数该如何理解呢?其中
- path代表执行的程序所在的路径,而ls的路径为/usr/bin/ls,因此传递的参数为
"/usr/bin/ls"
- arg是一个可变参数列表,实际上该参数传递的是给替换的新进程的命令行参数
因此我们想要将ls -a -l
指令切换到test_exec进程当中,首先我们要告诉系统我们要切换的进程的路径,即/usr/bin/ls
,接着告诉进程,我们运行你时,你的命令行参数应该为“ls”,“-a”,“-l”,“nullptr”.
现在我们已经清楚execl函数该如何调用了,接下来我们要搞清楚execl函数的原理,首先当一个二进制文件变成进程,第一步是被系统加载到内存中,接着是创建该进程对应的pcb,最后是将进程放入cpu中处理。
而pcb与内存之间还存在一个页表,以划分进程的地址空间。
也就是说,cpu处理进程时,会根据页表当中的内容,来运行进程,比如该执行什么代码,什么数据要进行计算,以及数据处理的结果都会根据页表的内容来进行。因此execl函数想要达到替换进程的效果,只需要将页表中的数据内容,代码内容替换成目标进程的内容即可。
好了,现在问题有两个,一个就是excel函数的返回值代表什么,另一个则是替换出来的进程是不是新进程。
为了验证第一个问题,我们先来写如下的代码。
#include<unistd.h>
#include<iostream>
int main()
{
int n=execl("/usr/bin/ls","ls","-a","-l",nullptr);
std::cout<<"execl 函数的返回值为:"<<n<<std::endl;//当execl函数结束后,输出它的返回值
return 0;
}
接着我们运行该程序。
我们惊奇的发现,当excel函数成功将ls进程替换掉test_exec进程后,它竟然不打印后续的内容了?我们明明在execl函数后面写了别的代码啊,为什么它不执行呢?
这是因为,execl函数的原理,是将原进程的代码,数据,替换成目标进程的代码,既然原进程的代码都被替换了,cpu在页表中都找不到对应的代码了,那么肯定是无法执行excel函数的后续代码的。
所以我们得出结论,execl函数如果成功执行,那么它的返回值没有任何的意义,因为保存返回值的数据,和后续的代码,都被目标进程替换了,即使它给出了返回值,我们也没有任何办法可以看到了啊。
但是执行失败的话,excel函数的返回值为-1,由于目标进程替换原进程失败了,所以页表中的内容还是原进程的内容,cpu会继续执行后续代码。要让excel函数出错也很简单,将路径写错就行了。
接着运行该代码。
这说明,excel函数的返回值其实不重要,因为想要对excel函数的返回值进行判断,首先我们需要给excel函数写上后续的判断代码,但是excel只有失败了才能执行后续的代码,因此我们也就不需要判断了,只需要在excel函数的后续写上输出信息,或者补救代码。因为只要执行了后续的函数,那么excel函数必然是失败的。
现在我们来看第二个问题,那就是替换的进程,是不是新进程?
首先,我们先写一个自己的进程,这里就不用系统的进程了。取名为exec
#include<iostream>
#include<unistd.h>
int main()
{
std::cout<<"我是替换的进程,我的pid为:"<<getpid()<<std::endl;
return 0;
}
接着我们将使用execl让exec进程替换掉原进程。
#include<unistd.h>
#include<iostream>
int main()
{
std::cout<<"我是被替换的进程,我的pid:"<<getpid()<<std::endl;
execl("./exec","exec",nullptr);
std::cout<<"execl函数执行失败"<<n<<std::endl;
return 0;
}
接着我们运行该进程,结果如下:
可以发现,被替换的进程,与替换的目标进程的pid一致,这是因为。其实所谓的替换进程,其实不仅仅只替换页表,其中也包括pcb,即将原进程的pcb也一起拿来用了,因此实际上替换的情况并非如上图所示,而是这样更加贴切。
因此替换进程的本质不是创建新进程,而是将原来的进程的东西,换成exec的东西,使用的pcb其实是同一个。
其他的替换函数
exec系列的函数很多,但是原理都和execl一样,只是参数不同,因此博主在此只展示用法,不再讲解原理了。
execv
int execv(const char *path, char *const argv[]);
execv和execl切换进程的原理一致,实际上exec系列函数切换进程的原理都一致,只是参数传递的方法不同罢了。
execl当中的l其实就是list(链表),即将命令行参数像链表一样,一个一个的传递给execl,而execv当中的v是vector(顺序表),即传递命令行参数的方式,是将命令行参数写成数组传递给execv函数。
#include<unistd.h>
#include<iostream>
int main()
{
std::cout<<"我是被替换的进程,我的pid:"<<getpid()<<std::endl;
char* const argv[]={//将命令行参数写成一个数组
"ls",
"-a",
"-l",
nullptr
};
execv("/usr/bin/ls",argv);
return 0;
}
execvp
int execvp(const char *file, char *const argv[]);
execvp与execv相比,其多了一个p,这个p其实是PATH的意思,PATH是环境变量之一,如果我们执行的程序在PATH保存的路径当中,那么我们可以省略掉路径。
int main()//execvp
{
std::cout<<"我是被替换的进程,我的pid:"<<getpid()<<std::endl;
char* const argv[]={//将命令行参数写成一个数组
"ls",
"-a",
"-l",
nullptr
};
//execvp("ls",argv);//ok
//execvp(argv[0],argv);//ok,argv[0]就是"ls"
//execvp("exec",argv);//error,exec不在PATH指定的路径下
//execvp("./exec",argv);//ok,将路径写全也能找到对应文件
return 0;
}
execvpe
int execvpe(const char *file, char *const argv[],char *const envp[]);
execvpe的e代表环境变量,即该程序允许我们给切换的进程传递函数变量,这里我们在同一路径下写上一个新的源文件other.cc。我们需要让其打印出它具有的所有环境变量。代码如下:
#include<unistd.h>
#include<iostream>
extern char** environ;
int main()
{
for(int i=0;environ[i]!=nullptr;i++)
{
std::cout<<"environ["<<i<<"]:"<<environ[i]<<std::endl;
}
return 0;
}
other进程的运行结果如下:
接着我们使用execvpe切换成other程序,且为其传入新的环境变量。
#include<unistd.h>
#include<iostream>
char* env[]={//环境变量
"A=helloworld",
"B=hellobit",
"C=114514",
"D=hahahahaha",
nullptr
};
int main()//execvpe
{
std::cout<<"我是被替换的进程,我的pid:"<<getpid()<<std::endl;
char* const argv[]={//将命令行参数写成一个数组
"other",
nullptr
};
execvpe("./other",argv,env);
printf("切换失败\n");
return 0;
}
运行该程序,可以发现other的环境变量被切换了。
那么到此,我们关于exec系列的函数就介绍完了,但是有人可能会说,exec系列的函数不是还有execlp和execlpe没讲吗?实际上exec系列的函数,其实都是exec,v,l,p,e的排列组合罢了,其中v表示以将命令行参数以数组的方式传递,而l表示将命令行参数一个一个的传递,p表示将可以不带路径,前提是文件要在环境变量PATH记录的路径下,e则表示我们可以传递环境变量。只要搞清楚了这些,exec系列函数我们也就掌握了。
但是要注意,有一个函数是特殊的,即execvpe
,为什么这么说呢,因为execl,execlp,execlpe,execv,execvp其实都是c标准库中的函数,而execvp却是实打实的系统调用,实际上,exec系列的函数,其底层都是execvp。