🏷️ 引入:
我们之前所创建的子进程,它执行的代码都是父进程的一部分。如果我们想要我们的子进程去执行全新的的代码,访问全新的数据,不要再和父进程有瓜葛我们应该怎么办呢?
这里我们就要使用:程序替换
🏷️ 见一见程序替换-单进程版的程序替换的代码(没有子进程)
代码如下:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("pid: %d, exec command begin\n", getpid());
// 用 execl 这个函数来调用系统的命令
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("pid: %d, exec command end\n", getpid());
return 0;
}
上面👆🏻代码的执行结果就和我们在命令行里输入的指令:ls -a -l
相同。
观察一下上面运行的结果你会发现。
下面我们简单介绍一下execl
这个函数
函数原型:
int execl(const char *path, const char *arg0, ..., (char *)NULL);
这里的参数意义如下:
path
:要执行的程序的路径。arg0
:传递给新程序的第一个参数,通常是程序的名称。...
:后续的参数列表,这些参数将作为新程序的argv
参数传递。参数列表必须以(char *)NULL
结尾,以表示参数列表的结束。
好了,execl
介绍完了,我们的问题是:我们的代码里面不是还有一条语句吗,它为什么没有去执行呢?
我们先把这个问题留着,讲解其他知识之后再做解答。
🏷️ 理解和掌握程序替换的原理,更改多进程版的程序替换的代码,扩展理解和掌握程序替换的原理多进程
📌 程序替换的原理一
好的,问题来了,那么这个过程有没有创建新的进程呢?
答:并没有。因为当我在替换新程序时,只是把我们目标程序的代码和数据替换到原来进程的壳子当中,也就是说整个进程的 pid 是不会反生任何改变的。
📌 我们把单进程的改成多进程的
我们之前单进程版本的程序替换的代码如下:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("pid: %d, exec command begin\n", getpid());
// 用 execl 这个函数来调用系统的命令
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("pid: %d, exec command end\n", getpid());
return 0;
}
我们现在来改成多进程版本的。
我们将代码修改成如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t id = fork(); // 创建子进程
if (id == 0)
{
// 这里是子进程
printf("pid: %d, exec command begin\n", getpid());
sleep(3);
// 用 execl 这个函数来调用系统的命令
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("pid: %d, exec command end\n", getpid());
exit(1); // execl 这个函数如果执行成功,就不会来到这下面的语句,如果执行失败才会来到这里,所以我们这里就直接退出
}
else
{
// 这里是父进程
pid_t rid = waitpid(-1, NULL, 0);
if (rid > 0)
{
printf("wait scuess, rid: %d\n", rid);
}
}
return 0;
}
代码的运行结果:
✏️如何来理解这个多进程发生程序替换的这个过程?
当我们今天创建子进程的时候,我们自己心里比较清楚的一点是:子进程也有自己的 PCB、地址空间、页表。
我们知道父进程和子进程通过页表的映射指向的同样的代码区和数据区,创建父子进程的时候,子进程要和父进程代码共享,数据以写时拷贝的方式各自私有一份
那么当我们程序替换的时候 实际上是把可执行程序它对应的代码和数据替换到子进程的代码和数据。但是问题来了,子进程的数据和代码是和父进程共享的,你程序替换把子进程的给替换了,那会不会影响到父进程呢?
首先,当你替换对应的数据的时候,因为父子进程的数据是以写时拷贝的方式各种私有一份的,所以并不会影响。
但是如果你替换对应的代码该怎么办呢?同样也是写时拷贝,在这种场景之下,我们也要把代码段进行写时拷贝。
程序替换的本质就是将新程序的代码和数据替换到我们之前进程(调用 execl的那个进程)的代码和数据,在替换的过程当中,如果是单进程就直接替换,如果是在子进程当中替换,我们要发生写时拷贝来保证父子进程的独立性
回答之前遗留的问题:我代码中最后一行还有个 printf
,为啥没给我执行这句代码呢?
因为当它调用exec
程序替换 ,只要程序替换成功了,其实 eip都被改掉了,所以你的子进程就转而去执行新的程序了, 如果是在单进程的场景下,你调用 execl
成功之后,你的子进程的代码和数据都被替换掉了,所以也无法执行那个printf
当我们执行 exec* 这样的函数,如果当前进程执行成功,则后续代码没有机会在执行了!因为被替换掉了!
exec*
只有失败的返回值,没有成功的返回值
🏷️大量使用其他的程序替换的方法——父子进程场景中
任何程序替换必须解决的两个问题:
- 必须找到这个可执行程序
- 必须告诉
exec*
怎么执行
✏️ 我们再来认识一下:execl
这个函数
✏️ execlp
函数
execlp("ls", "ls", "-a", "-l", NULL); // 注意,第一个“ls”表示的是文件名,第二个“ls”表示的是命令行里的参数
✏️ execv
函数
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t id = fork(); // 创建子进程
if (id == 0) // 这里是子进程
{
char *const argv[] = {
"ls",
"-a",
"-l",
NULL};
printf("pid: %d, exec command begin\n", getpid());
sleep(1);
execv("/usr/bin/ls", argv);
// execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
// execlp("ls", "ls", "-a", "-l", NULL); // 注意,第一个“ls”表示的是文件名,第二个“ls”表示的是命令行里的参数
printf("pid: %d, exec command end\n", getpid());
exit(1);
}
else
{
// 这里是父进程
pid_t rid = waitpid(-1, NULL, 0);
if (rid > 0)
{
printf("wait scuess, rid: %d\n", rid);
}
}
return 0;
}
✏️ execvp
函数
✏️ 问题:我们的程序替换,能替换系统指令程序,能替换我写的程序吗?
.cc
后缀的文件是 c++ 程序文件。
我们可以在这个mytest.cc
写下如下 c++ 代码:
#include <iostream>
int main()
{
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
return 0;
}
同时,我们也要相应的去修改我们的Makefile
文件,
.PHONY:all
all:mytest myprocess
// 这样我们才能在 make 的时候一次形成两个可执行程序
mytest:mytest.cc
g++ -o $@ $^ -std=c++11
myprocess:myprocess.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f myprocess mytest
看看我们今天写的程序:
我们现在的实验是:用上面的c程序替换掉我们的 c++程序。(用今天学的程序替换的方式)
在此之前,我们也要对我们写的 C 程序做出相应的修改:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t id = fork(); // 创建子进程
if (id == 0) // 这里是子进程
{
execl("./mytest", "mytest", NULL);
printf("pid: %d, exec command end\n", getpid());
exit(1);
}
else
{
// 这里是父进程
pid_t rid = waitpid(-1, NULL, 0);
if (rid > 0)
{
printf("wait scuess, rid: %d\n", rid);
}
}
return 0;
}
好,我们做好上面的准备工作之后,开始跑一下:
哎,也能行!
✏️ 一个程序是怎么加载到内存里的
通过今天的学习你会发现:exec*
的这些函数的调用的过程不就是一个程序加载到内存里的那个过程吗?
想想我们之前的那一张图:
这不就是把磁盘中的程序加载到内存中的过程吗,不就是
📌 解释一下:./mytest
这个程序从开始加载到调度运行到最后退出的整个过程
我们当前的系统识别到了对应的可执行程序,它要创建对应的进程,它会先创建进程的内核数据结构(什么 PCB,地址空间,页表之类的),然后把磁盘中的相应的代码和数据通过类似 exec* 这样的接口加载到物理内存之中。
🏷️接口学习:execle
假设我们现在有以下文件:
📄 myprocess.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t id = fork(); // 创建子进程
if (id == 0) // 这里是子进程
{
printf("pid: %d, exec command begin\n", getpid());
execl("./mytest", "mytest", NULL);
printf("pid: %d, exec command end\n", getpid());
exit(1);
}
else
{
// 这里是父进程
pid_t rid = waitpid(-1, NULL, 0);
if (rid > 0)
{
printf("wait sucess, rid: %d\n", rid);
}
}
return 0;
}
📄 test.cc
#include <iostream>
int main()
{
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
return 0;
}
上面的myprocess.c
文件中,我们是使用的execl
这个函数来进行程序替换的。
现在我们来修改一下上面👆🏻的代码,我想要mytest.cc
这个文件来实现一个打印环境变量的功能,我们可以做如下修改:
#include<iostream>
int main(int argc, char* argv[], char* env[])
{
for (int i = 0; env[i]; i++)
{
std::cout << i << ":" << env[i] << std::endl;
}
return 0;
}
对上面代码的解释
int argc, char* argv[], char* env[]
// `int argc`: 命令行参数的数量。
// `char* argv[]`: 一个字符串数组,包含了命令行参数。
// `char* env[]`: 一个字符串数组,包含了环境变量。
for (int i = 0; env[i]; i++)
//这是一个`for`循环,用于遍历环境变量数组。循环的条件是`env[i]`不为`NULL`,这意味着当前索引`i`处有环境变量。因为环境变量也是一张表,环境变量所对应的这张表是一个指针数组,这个指针数组最终以 NULL结尾,所以当我们在遍历的时候,退出循环的条件就是当 env[i]走到 NULL 的时候。
好了,修改之后,我们使用./myprocess
,运行上面的代码(运行之前记得重新 make一下),我们就可以看到打印出来的环境变量了。
通过观察打印出来的环境变量,我们可以得出这样的一个结论:
当我们进行程序替换的时候,子进程对应的环境变量是从父进程那里得来的,而父进程的环境变量是从当前对应的 shell 得来的。
我们可以验证一下:
使用export MYVAL=6666666666666666666666666666666666666666666
命令来自定义一个环境变量到当前 bash 这个 shell 中,名字叫:MYVAL。对应的内容是:6666666666666666666666666666666666666666666
我们可以使用 echo 命令查看一下:echo $MYVAL
;
然后重新再运行一下./myprocess
,就可以看到程序打印的环境变量中 有我们自定义的环境变量了。
我们还可以进行一次尝试,上面我们导入的环境变量是直接导入到 bash 中的,这次我们在父进程中,导入一个环境变量,看看子进程还拿不拿得到,这个时候我们要修改一下我们的代码。
❓ 我们如何让我们的父进程自己导入一个环境变量呢?
这个时候我们要引入一个新的函数:putenv
头文件是: stdlib.h
,这个函数的作用就是导入一个环境变量
# include <stdlib.h>
int putenv(char* string);
好,知道了上面的知识,我们把我们的myprocess.c
代码修改成如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
char *env_val = "MYVAL2=88888888888888888888888888888";
putenv(env_val);
pid_t id = fork(); // 创建子进程
if (id == 0) // 这里是子进程
{
printf("pid: %d, exec command begin\n", getpid());
execl("./mytest", "mytest", NULL);
printf("pid: %d, exec command end\n", getpid());
exit(1);
}
else
{
// 这里是父进程
pid_t rid = waitpid(-1, NULL, 0);
if (rid > 0)
{
printf("wait sucess, rid: %d\n", rid);
}
}
return 0;
}
make 之后,我们运行一下,也可以发现子进程中有父进程自己导入的环境变量:
在我们上面的代码之中,我们没有主动地传递过任何环境变量给子进程,但是子进程中会出现父进程的环境变量,这说明环境变量被子进程继承下去是一种默认行为,不受程序替换的影响,### 为什么呢?
通过地址空间可以让子进程继承父进程的环境变量数据。
但是环境变量和命令行参数也是数据呀,我们之前讲程序替换的时候不是说要把相应进程的数据段和代码段都替换掉吗?那为什么这里说环境变量被子进程继承下去不受到程序替换的影响 ?
答案很简单: 程序替换确实是替换掉之前程序的代码段和数据段,但是环境变量不会被替换掉。
环境变量具有全局属性
✏️子进程执行的时候,获得环境变量的方法
🧲execle
函数的相关知识:
execle
函数是Linux系统编程中exec函数族的一员,它用于在当前进程中执行一个新的程序,替换当前进程的映像。这个函数特别之处在于它允许你指定一个新的环境变量列表来代替当前进程的环境变量。
函数原型:
int execle(const char *path, const char *arg, ..., char *const envp[]);
参数:
path
:要执行的文件的路径。arg
:第一个参数是新程序的名称,后面跟着传递给新程序的参数,参数列表必须以NULL
结束。envp
:这是一个指向环境变量数组的指针数组,新程序的环境变量设置将由这个数组决定。
返回值:
- 如果执行成功,
execle
不会返回。 - 如果执行失败,返回-1,并设置
errno
以指示错误。
注意事项:
- 参数列表必须以
NULL
结束,这表示参数列表的结束。 envp
参数允许你为新程序定义一个全新的环境变量集合,这在执行需要特定环境配置的程序时非常有用。- 使用
execle
时,需要注意文件路径、权限等问题,确保可执行文件是存在的,并且有适当的执行权限。
🧲 方法一:将父进程的环境变量原封不动的传递给子进程
1.直接用
2.直接传
看例子🌰, 这个时候我们要修改一下我们的代码:
``myprocess.c``要改成这样,我们用的是``execle``函数
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
extern char **environ; // 在这里声明一下。
int main()
{
pid_t id = fork(); // 创建子进程
if (id == 0) // 这里是子进程
{
printf("pid: %d, exec command begin\n", getpid());
execle("./mytest", "mytest", "-a", "-b", NULL, environ); // 这里要报错,说 environ 未定义,但是 environ 是被包在了头文件 unistd.h 中,没办法,我们可以去上面声明一下
printf("pid: %d, exec command end\n", getpid());
exit(1);
}
else
{
// 这里是父进程
pid_t rid = waitpid(-1, NULL, 0);
if (rid > 0)
{
printf("wait sucess, rid: %d\n", rid);
}
}
return 0;
}
mytest.cc
,要修改为下面这样:
#include <iostream>
int main(int argc, char *argv[], char *env[])
{
for (int i = 0; i < argc; i++)
{
std::cout << i << " -> " << argv[i] << std::endl; // 打印参数
}
std::cout << "#####################################" << std::endl; // 分割线而已
for (int i = 0; env[i]; i++)
{
std::cout << i << ":" << env[i] << std::endl; // 打印环境变量
}
return 0;
}
🧲 方法二:传我们自己定义的环境变量—我们可以直接构造环境变量表给子进程传递
这个时候我们修改一下我们的代码,自定义环境变量。myprocess.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
extern char **environ; // 在这里声明一下。
int main()
{
// 自己定义一个环境变量
char *const myenv[] = {
"MYVAL1=11111111111111111111111111",
"MYVAL2=11111111111111111111111111",
"MYVAL3=11111111111111111111111111",
"MYVAL4=11111111111111111111111111", NULL // 注意不要忘了,以 NULL 结尾。
};
pid_t id = fork(); // 创建子进程
if (id == 0) // 这里是子进程
{
printf("pid: %d, exec command begin\n", getpid());
execle("./mytest", "mytest", "-a", "-b", NULL, myenv);
printf("pid: %d, exec command end\n", getpid());
exit(1);
}
else
{
// 这里是父进程
pid_t rid = waitpid(-1, NULL, 0);
if (rid > 0)
{
printf("wait sucess, rid: %d\n", rid);
}
}
return 0;
}
编译之后,我们运行一下来试试。(没说明的话 mytest.cc 就不做改变)
通过观察运行结果我们知道,子进程拿到的只有我们自己定义的环境变量表了。
注意:通过上面的运行结果我们可以知道,execle 函数来传递环境变量时,不是新增环境变量,而是覆盖掉之前的环境变量,同理可以推导出其他 exec 族函数中也一样,带
e
的,不是新增,而是覆盖