首先,我们要认识到,我们之前fork()所创建的子进程,执行的代码,都是父进程的一部分(用if-else分流或者执行同样的代码)!
如果我们想让子进程执行新的程序呢?执行全新的代码和访问全新的数据,不再和父进程有瓜葛,这种技术就叫做程序替换,下面我们就来学习一下:
首先我们先写一份单进程版的程序替换的代码(没有子进程),先来见见!
linux下有execl这样的一批接口,大家先来看看:
#include
#include
#include
int main()
{
printf("pid: %d, exec command begin\n", getpid());
// 因为可变参数列表,所以以NULL结尾,表示参数传完了
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
printf("pid: %d, exec command end\n", getpid());
return 0;
}
我们直接用“语言”将其他进程执行了! 但是我们预期还有end呢,没有打印出来,怎么回事?这个我们下面回答!
熟悉了使用后,我们再来看看另外的代码:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
printf("pid: %d, exec command begin\n", getpid());
sleep(1);
execl("/usr/bin/ls", "ls", "-l", "-a", NULL); // 因为可变参数列表,所以以NULL结尾,表示参数传完了
printf("pid: %d, exec command end\n", getpid());
}
else
{
// father
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
return 0;
}
我们在等成等待的时候,没有指定pid,给了-1,让随机等待,返回的进程pid与子进程的pid一样,由此我们可以得出一个结论:进程替换没有创建新的进程!
在上面的代码中,子进程进行了程序替换,是需要在物理内存中重新开辟空间,并将子进程的页表进行修改,代码也是数据,替换时子进程对代码做出了修改,又由于进程具有独立性,父进程与子进程互不影响,他们各自执行自己的代码,就实现了解耦,互不影响!
问题:
子进程怎么知道,要从新的程序的最开始执行?它怎么知道最开始的地方在哪?
在Linux中,可执行程序有一定的格式叫做ELF,二进制文件的头部有一张表,表中有一个字段,里面记录了程序的入口地址(entry),CPU内有一批寄存器,有的时程序计数器(eip,pc)它们记录了代码执行的上下文,eip内存的是当前所执行代码的下一条,所以我们想要执行新的程序,只需要将entry中的地址填到eip,它就会进入新的程序开始执行!
至此,我们就知道了,为什么上面第一段代码中替换后,输出没有打印出来了。单进程中,替换后原本的代码和数据都被替换了,原本的后续代码就没有了!多进程中,因为我们将eip的地址填为了替换后进程的入口地址,这样就不会再给eip中更新原本语句的地址了,因此也就不可能再执行后面的代码了!
exec这样的函数,如果当前进程执行成功,则后续代码没有机会在执行了!因为被替换掉了!如果失败,就会继续执行本来代码的后续!exec只有失败的返回值,没有成功的返回值,所以使用时不用判断!
程序替换 原理是,将物理内存中的数据和代码替换为我们想要替换的进程的代码和数据(虚拟内存会根据新加载的代码和数据做相应的调整,但是各个区的范围是不变的),这就做到了谁调用exec系列函数就可以实现程序替换!!!
因此这也说明 进程替换并没有创建新的程序,只是替换了数据和代码!
总结一个思想:
a. 必须先找到这个可执行程序
b. 必须告诉exec,怎么执行*
此函数最开始就使用了,使用样例直接跳转到通篇开始去看即可!
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
printf("pid: %d, exec command begin\n", getpid());
sleep(1);
execlp("ls", "ls", "-a", "-l", NULL);
printf("pid: %d, exec command end\n", getpid());
}
else
{
// father
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
return 0;
}
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
char* const argv[] = {"ls", "-a", "-l", NULL};
printf("pid: %d, exec command begin\n", getpid());
sleep(1);
execv("/usr/bin/ls", argv);
printf("pid: %d, exec command end\n", getpid());
}
else
{
// father
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
return 0;
}
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
char* const argv[] = {"ls", "-a", "-l", NULL};
printf("pid: %d, exec command begin\n", getpid());
sleep(1);
execvp("ls", argv);
printf("pid: %d, exec command end\n", getpid());
}
else
{
// father
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
return 0;
}
我们一直都替换的是库中的程序,我们自己写的程序可以替换吗?答案是可以,我们来试一下:
先写一份C语言测试代码:
#include
using namespace std;
int main()
{
cout << "hello linux!" << endl;
cout << "hello linux!" << endl;
cout << "hello linux!" << endl;
return 0;
}
再写程序替换代码:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
printf("pid: %d, exec command begin\n", getpid());
execl("./mytest.cc", "mytest", NULL);
printf("pid: %d, exec command end\n", getpid());
}
else
{
// father
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
return 0;
}
问题:
之前不是说execl函数第一个参数是路径,第二个以及后面的命令行怎么写就怎么传么,这里execl函数第二个参数怎么没有带./呢?
我们在命令行中执行自己的可执行程序时,先要找到可执行程序,再调用execl函数时,我们第一个参数传了路径了,后面的参数就不用了带./了。
我们还可以写一份脚本语言,并进行程序替换:
#!/usr/bin/bash
echo "hello world!"
touch file1 file2 file3
echo "hell done"
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(0 == id)
{
// child
printf("pid: %d, exec command begin\n", getpid());
execl("/usr/bin/bash", "bash", "test.sh", NULL);
printf("pid: %d, exec command end\n", getpid());
}
else
{
// father
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
return 0;
}
所以exec系列函数进行程序替换,不就是加载的过程么,这不就是加载器的重要功能么!
我们先来铺垫两点,铺垫完之后就更容易理解execle函数了!
1、当进行程序替换的时候,子进程对应的环境变量,是可以直接从父进程来的!下面我们进行验证:
这里介绍一个函数,putenv()函数,添加环境变量的。
我们写两套代码,mytest.cc和procReplace.c,mytest.cc为替换的程序!
#include
using namespace std;
int main(int argc, char* argv[], char* env[])
{
for(int i = 0; env[i]; i++)
{
cout << i << " : " << env[i];
}
return 0;
}
#include
#include
#include
#include
#include
int main()
{
char* my_val = "MYENV=11111111111";
putenv(my_val);
pid_t id = fork();
if(0 == id)
{
// child
printf("pid: %d, exec command begin\n", getpid());
execl("./mytest", "mytest", NULL);
printf("pid: %d, exec command end\n", getpid());
}
else
{
// father
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
return 0;
}
我们之前在环境变量中就讲过,子进程会继承父进程的环境变量表,这里的父进程是procReplace,mytest继承了它,而procReplace是bash的子进程,所以除了MYENV这个环境变量是procReplace的,其他的环境变量都是procReplace继承bash来的。所以我们得出的结论就是:
2、环境变量被子进程继承下去是一种默认行为,不受程序替换的影响!
为什么?
子进程创建时,拷贝了父进程的PCB,进程地址空间,页表。命令行参数和环境变量是数据,在物理内存中开辟了空间,因此通过地址空间可以让子进程继承父进程的环境变量数据!
程序替换,只替换新程序的代码和数据,环境变量不会被替换!
3、让子进程执行的时候,获得的环境变量,以下面两种方式传递
a、将父进程的环境变量原封不动传递给子进程
经过上面的演示,我们发现:
还是两个代码,一个父一个子:
#include
#include
#include
#include
#include
int main()
{
extern char** environ;
pid_t id = fork();
if(0 == id)
{
// child
printf("pid: %d, exec command begin\n", getpid());
execle("./mytest", "mytest", "-a", "-l", NULL, environ);
printf("pid: %d, exec command end\n", getpid());
}
else
{
// father
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
return 0;
}
#include
using namespace std;
int main(int argc, char* argv[], char* env[])
{
for(int i = 0; i < argc; i++)
{
cout << i << " -> " << argv[i] << endl;
}
cout << "##################################" << endl;
for(int i = 0; env[i]; i++)
{
cout << i << " : " << env[i] << endl;
}
return 0;
}
b、我们想传我们自己的环境变量!
我们自己写一批环境变量,再使用execle函数进行传参即可:
#include
#include
#include
#include
#include
int main()
{
char* const myenv[] = // 自定义环境变量表
{
"MYENV1 = 11111111111111111",
"MYENV2 = 22222222222222222",
"MYENV3 = 33333333333333333",
"MYENV4 = 44444444444444444",
NULL
};
pid_t id = fork();
if(0 == id)
{
// child
printf("pid: %d, exec command begin\n", getpid());
execle("./mytest", "mytest", "-a", "-l", NULL, myenv);
printf("pid: %d, exec command end\n", getpid());
}
else
{
// father
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
return 0;
}
#include
using namespace std;
int main(int argc, char* argv[], char* env[])
{
for(int i = 0; i < argc; i++)
{
cout << i << " -> " << argv[i] << endl;
}
cout << "##################################" << endl;
for(int i = 0; env[i]; i++)
{
cout << i << " : " << env[i] << endl;
}
return 0;
}
我们发现,我们使用execle函数将定义的环境变量传过去之后,并不是在原来的基础上新增,而是而是覆盖式传递!
c、如果我想新增呢?
其实我们已经做过了,我们在这个函数讲解开始的时候说过一个函数,putenv函数,如果我们想要新增,直接在父进程中使用putenv添加环境变量,然后再fork创建子进程,子进程继承父进程环境变量表后,就实现了在原来的基础上新增!
总结:程序替换(exec系列函数)可以将命令行参数和环境变量(覆盖式传递),通过自己的参数传递给被替换的程序的main函数中!
这里就不再多讲了,结合execvp函数与execle函数很好理解!!!
左边的6个函数底层其实都是封装了右边execve函数,只有execve是系统调用,封装6个是为了满足不同的需求。