1. 替换概念
- 为什么需要程序替换
父进程一般需要子进程能够做其他的事,那么也有可能做的是全新的、不同的事,这就需要进程替换。
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换, 从新程序的启动例程开始执行。
调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
C/C++程序要运行,就要先把程序加载到内存中,那么这个过程需要的加载器其实就是exec*程序替换函数。
- 本质
程序替换的本质就是把程序的进程代码+数据,加载到特定进程的上下文中。
也就是说每个可执行程序需要运行,都需要加载到内存中,这是因为冯诺依曼体系结构中规定外设不能直接和CPU打交道,而是要通过存储器;那么这个“加载”过程的实现就是通过exec*函数,所以它也被称为加载器。
程序替换的前提是建立了一个完整的进程,这包括:PCB,虚拟地址空间,页表物理内存等。
2. 替换解释
2.1 替换函数
当子进程替换另一个代码时,父进程不受影响,因为每个进程是独立的;但是它们(父子进程)共享着一份代码,为什么可以不受影响?
这是因为程序替换的过程,对这个“共享”的代码进行了写入,会引发写时拷贝,此时它们执行的已经不是同一份代码了,也就互不影响。
头文件:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
- 替换函数execl
int execl(const char *path, const char *arg, ...);
path:代表要执行的目标程序的全路径,即路径和文件名;
arg:要执行的目标程序,在命令行上怎么执行,这里的参数就一个个传递;
并且参数必须以NULL结尾。
如果执行ls -a -l -n -i命令:
printf("hello one\n");
execl("/usr/bin/ls","ls","-a","-l","-n","-i",NULL);
printf("hello two\n");
return 0;
执行后,将不会打印hello two,因为进行了程序替换,程序都被代替了。
那么返回值也会被替换掉:
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
所以,可以实现一个程序调用另一个程序的功能,这其实也是一种程序替换,只需要把参数按照对应格式修改。
上面的例子演示了运行该可执行文件的时候不再执行原代码的程序,而是执行替换后的ls -a -l -n -i命令,实现了程序替换。
2.1.1 替换对于父进程的影响
对于父子进程来说,子进程的替换不会影响到父进程,因为进程间具有独立性。
但是之前说的代码共享呢?是否矛盾?
这里其实并不矛盾,因为这里的“共享”是建立在代码不发生改变的前提下,而此时的进程替换很明已经更改了代码区的代码,那么此时自然就会发生写时拷贝。所以最终的结果应该是:子进程执行新的代码,父进程继续执行旧的代码。
观察下列代码:
int main(){
pid_t id = fork();
if(id == 0){
//子进程
printf("i am a child!!pid:%d\n",getpid());
sleep(3);
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("aaaaaaaaaaaaaaaaa\n");
exit(0);//子进程执行完以后返回正常退出码
}
while(1){
printf("i am a father\n");
sleep(1);
}
waitpid(id,NULL,0);
printf("wait success!!");
}
该程序建立了子进程,并且通过execl函数把子进程本该执行的打印aaaaa的代码替换成了打印"ls -a -l",那么程序的运行结果就是:
此时可以理解进程程序替换的本质:程序员在命令行当中启动一个程序, 本质上是bash程序启动了一个子进程, 子进程程序替换成为程序员启动的程序
2.2 替换成功与否
- 程序替换不会创建新进程,本质是由于task_struct等数据未改变;
- 进程程序替换一经替换,决不返回,后续代码不会执行,也就是说该进程的代码全部被替换了。
但是也存在替换失败的情况,函数会返回,但后续程序不受影响。也就是说。exec*系列函数只要返回了,就说明替换失败。
2.3 其他替换函数
2.3.1 替换系统指令
- 其他函数
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[]);
//execve是独特的
int execve(const char *path, char *const argv[], char *const envp[]);
乍一看复杂的很,好像都一样又好像都不一样……但其实它们有分类依据:
根据带有的单词不同,有不同的分类依据:
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
就比如最开始的execl,它的参数就是采用列表,也就是一个个罗列出来。
- 解释
- 对于execv,其实和execl没有太大差别,只不过execv是先把参数打包在数组argv中,再一起传参。
int execv(const char *path, char *const argv[]);
//程序替换
//execv--对比execl传参是用数组形式
int main()
{
if(0 == fork()){
//child
printf("command begin.......\n");
char* argv[] = {
"ls",
"-a",
"-l",
NULL
};
execv("/usr/bin/ls",argv);//数组形式传参
printf("command end.......\n");
}
waitpid(-1,NULL,0);
return 0;
}
运行结果:
- 对于execlp,它的file参数意思是你要执行谁,传参后PATH环境变量会自动帮你找到
int execlp(const char *file, const char *arg, ...);
例如:
execlp("ls","ls","-a","-l","-n","-i",NULL);
第一个ls代表你要执行谁,后面的参数都代表你想执行的程序,所以execlp对比execl就是可以利用环境变量PATH自动搜索程序。
int main()
{
if(0 == fork()){
//child
printf("command begin.......\n");
execlp("ls","ls","-a","-l",NULL);
printf("command end.......\n");
}
waitpid(-1,NULL,0);
printf("waitpid success!\n");
return 0;
}
- 以此类推,对于execvp就说明是用数组传参,并且采用环境变量查找指令:
int execvp(const char *file, char *const argv[]);
execvp("ls",argv);//自定义数组传参
2.3.2 替换自定义可执行程序
也可以用来替换自定义的可执行程序,如execl:
先在myexe.c实现:
int main(){
printf("hahaahaahhaha!!i am your exec!\n");
return 0;
}
在myload.c中实现(makefile中实现的可执行程序就是对应的程序名,例如myexe.c—>myexe):
//在myload.c函数中写入此代码
execl("./myexe","myexe",NULL);
//其他代码省略
运行结果如下,可以看到此时运行的是可执行程序myload,但是执行的却是myexe.c的代码:
- 对于execle,与execl对比区别就是需要多传一个env,也就是可以自定义环境变量:
所以只要我在myload.c中自定义环境变量,通过execle是可以导入到myexe.c中并且由它输出的,就像前面替换系统指令ls一样:
在myexe.c中实现:
int main(){
//打印环境变量
extern char** environ;
for(int i = 0;environ[i];i++)
{
printf("%s\n",environ[i]);
}
printf("hahaahaahhaha!!i am your exec!\n");
return 0;
}
在myload.c中实现:
int main()
{
if(0 == fork())
{
//child
char* myenv[] = {
"MY_ENV1 = AAAAAAA",
"MY_ENV2 = AAAAAAA",
"MY_ENV3 = AAAAAAA",
"MY_ENV4 = AAAAAAA",
"MY_ENV5 = AAAAAAA",
};
execle("./myexe","myexe",NULL,myenv);
}
//其他代码省略
}
此时运行myload.c得出:
如果运行myexe.c,得出结果是系统默认的很多环境变量:
- 结论
原本直接运行myexe中打印的是系统的环境变量,而运行myload以后,由于调用了execle函数并且把自己想要查找的环境变量给到了myexe,这时候myexe找的不再是系统的环境变量,而是被替换后,myload自定义的环境变量env。
但是如果直接运行myexe.c,得出的就是系统默认的环境变量。就是上面输出的一堆环境变量。
下面是makefile的实现:
- 对于独特的execve:这其实就是execle的另一种版本,单独罗列出来是因为这个接口在Linux中是单独一个手册的,而其它六个是封装在了一起。
3. 总结
有这么多的接口,是为了满足不同的需求。(带l的参数都需要列表,带p的都可以使用环境变量PATH,带e的需要自己组装环境变量)
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。
那么程序替换运行程序,和直接运行程序有什么区别?
区别就是直接运行还需要创建新的进程,而程序替换不需要创建新进程。