一,前言
创建子进程的目的之一就是为了代劳父进程执行父进程的部分代码,也就是说本质上来说父子进程都是执行的同一个代码段的数据,在子进程修改数据的时候进行写时拷贝修改数据段的部分数据。
但是还有一个目的——将子进程在运行时指向一个全新的程序代码,也就是我们的进程程序替换。
二、什么是进程程序替换?
所谓进程程序替换,顾名思义,就是使用一个新的程序替换原有的程序,进程将执行新程序的代码,而不再执行原有程序的代码,前面我们已经学习了如何创建一个进程,一般情况下,进程程序替换都不会使用父进程直接进行进程程序替换,而是让父进程调用fork()函数创建一个子进程,让子进程去执行一个新的程序即可
三,进程程序替换的原理
- 进程替换前的效果图
当一个进程成功创建一个子进程之后,父子进程的情况如下图所示:
这个时候,我们这里先针对代码和数据进行分析,其他内容暂不做考虑,此时父子进程都没有修改代码和数据,因此,父子进程的代码和数据都是指向同一块内容的,也就是代码和数据是共享的,如果其中一方对数据进行修改,则这一方就会进行写时拷贝,如果想要执行不同的代码,则此时就要进行进程程序替换
- 进程替换之后的效果图
四,为什么要进行进程程序替换
在学习进程程序替换之前,我们知道当一个父进程创建一个子进程之后,父子进程的代码是共享的,子进程只能执行父进程的代码块
但是现在我们的需求增加了,我们不仅要让子进程能够执行父进程的代码块,也要能够让子进程能够做一些父进程不能做的事情,也就是能够执行一个全新的代码(程序),这样就能实现父子进程做的事情有所差异,大大提高了办事效率,同时也使父子进程的代码彻底分离,维护进程的独立性
五,怎么实现进程程序替换
进程程序替换是指在运行过程中将一个进程的地址空间中的代码、数据和堆栈等内容完全替换为另一个程序的代码、数据和堆栈的过程。
这个过程通常是由操作系统提供的 exec 系列函数来实现的:
- 地址空间替换:进程的地址空间是指进程可以访问的内存范围。通过地址空间替换,进程可以在运行时动态地加载并执行不同的程序,从而实现灵活的程序执行和管理。
- exec 函数族:exec 函数族是一组系统调用,用于执行程序替换操作。这些函数包括 execl, execv, execle, execve 等,它们允许以不同的方式传递参数给新程序,并执行地址空间替换。(我们要改变内存,那肯定是要调用系统调用接口的,这些函数会封装相应的接口)
- 程序入口点:新程序的入口点是程序中的起始执行位置,通常是 main 函数或其他指定的入口函数。替换完成后,控制权将转移到程序入口点,开始执行新程序的代码。
5.1.原理
- 当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换
- 替换完成后,控制权将转移到新程序的入口点,开始执行新程序的代码。
5.2.使用execl()函数
execl函数是Linux系统中用于执行新程序的函数之一,它属于exec函数族的一部分。
这个函数的作用是在当前进程的上下文中启动一个新的程序,并替换当前进程的映像为新的程序映像。调用execl函数后,当前进程将停止执行,并由新的程序开始执行.
函数原型如下
参数说明:
path
:要执行的程序的路径。arg0
:新程序的参数列表的开始,通常这会是新程序的名称(尽管这不是强制的,但它通常用于错误消息和程序内部)。...
:一个可变参数列表(参数的数量不固定),新程序的参数列表,必须以NULL结尾。
只看上面的解释,我相信小伙伴们对于这一块还是不太理解,那么上代码:
我们来看个例子
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("I'm a process, pid: %d\n", getpid());
printf("execl begin...\n");
int a=execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("execl end...\n");
return 0;
}
如果execl函数调用成功,那么它实际上不会返回,因为当前进程的映像已经被新程序替换。如果调用失败,它会返回-1,并设置全局变量errno以指示错误原因。常见的错误原因可能包括文件未找到、权限不足等。
execl函数和其他exec函数一样,不会创建新的进程。它们只是在当前进程的上下文中启动另一个程序。因此,调用execl前后,进程的ID(PID)不会改变。
同时,由于execl会替换整个进程映像,所以在调用execl之前,通常需要确保当前进程的所有打开的文件描述符、内存分配等都被适当地处理或释放,因为这些资源不会被新程序继承。
5.2.1.结论与细节
5.3.多进程实现使用ls
上面的实验是没有子进程的,是一个纯单进程的实验,下面将演示一个多进程的例子执行ls指令
上面的实验思路就是父进程创建一个子进程,然后本来子进程是要执行父进程的代码块和父进程进行代码共享的,但是我们在子进程中调用execl函数接口,因此,在子进程中会进行程序替换
6.exec系列函数
由我们库封装的exec函数常用的有上面的几种,这些函数都有下面的特性,当该函数成功执行,那么进程替换成功,代码不再返回,如果函数调用失败,例如不正确的地址,不正确的文件等等,函数会返回一个-1,并且exec函数之后在函数调用失败时才有返回值,成功没有返回值。
只有失败时有返回值其实很好理解,因为函数调用成功那就表示程序替换成功,原来的代码都被替换了,我返回之后给谁?没了,之后的代码就是新代码了
1.execl:
该函数允许通过提供可变数量的参数来执行指定的可执行文件。它的原型如下:
int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
path 是要执行的可执行文件的路径,arg0 是第一个参数,后续参数都是传递给可执行文件的命令行参数,以 NULL 结尾。
观察上图发现进程替换成功后,替换函数下的打印没有执行,原因与注意事项的第二条相同
如果替换失败:
2.execlp:
该函数与 execl 类似,但是它会在系统的环境变量 PATH 指定的目录中查找可执行文件。它的原型如下:
int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
file 是要执行的可执行文件的文件名,arg0 是第一个参数,后续参数都是传递给可执行文件的命令行参数,以 NULL 结尾。
3.execv:
类似于 execl,但是允许传递一个参数数组给被执行的程序。它的原型如下:
int execv(const char *path, char *const argv[]);
path 是要执行的可执行文件的路径,argv 是一个以 NULL 结尾的参数数组,其中每个元素都是一个字符串,表示命令行参数。
4.execvp:
类似于 execv,但是它会在系统的环境变量 PATH 指定的目录中查找可执行文件。它的原型如下:
int execvp(const char *file, char *const argv[]);
file 是要执行的可执行文件的文件名,argv 是一个以 NULL 结尾的参数数组,其中每个元素都是一个字符串,表示命令行参数。
5.execle:
函数与 execl 函数类似,但允许在启动新程序时传递额外的环境变量。它的原型如下:
int execle(const char *path, const char *arg, ..., char *const envp[]);
path 是要执行的可执行文件的路径,arg 是要传递给新程序的命令行参数,后面的参数是额外的环境变量,以 NULL 结尾。
因为此时我们没有环境变量MYSTR所以第一行打印为空
这里在myProc子进程中用execle函数来导入环境变量MYSTR
6.使用方法总结
有人就说了上面那些函数,我怎么记得住他们的用法啊?
使用这些函数其实简单,先将函数名的exec提取出来看后面的几个字母。
其中/bin/ls表示需要执行的文件是谁,ls表示执行方式,而-a和-l表示这个执行的参数列表。
可以看到我们用过指针数组的方式将我们的执行和参数列表存到了一起,然后将这个指针数组作为参数传递给我们的execv函数就行。
7.也可以调用其他语言的程序
code.c里:
int main()
{
char* const env[] = {
(char*)"first",
(char*)"second",
NULL };
pid_t id = fork();
if (id == 0)
{
printf("I'm a process, pid: %d\n", getpid());
printf("execl begin...\n");
execle("./mytest", "mytest", NULL, env)
printf("execl end...\n");
exit(1);
}
pid_t rid = waitpid(id, NULL, 0);
if (rid > 0)
{
printf("wait successfully\n");
}
return 0;
}
test.cpp里:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
for (int i = 0; environ[i]; i++)
{
printf("env[%d]: %s\n", i, environ[i]);
}
cout << "This is C++" << endl;
return 0;
}