问题导入
前几篇博文我们知道了,一个进程可以创建一个子进程来执行父进程的部分代码,这个代码是在父进程里面的,属于父进程的一部分,那如果我们想让子进程来执行一个全新的代码任务,我们又该如何实现呢?下面我们来学习并解决这个问题。
进程替换引入
我们先来看看一段代码,带着疑问来思考答案。
#include<stdio.h>
#include<unistd.h> // execl函数的头文件
int main()
{
printf("父进程开始...\n");
printf("父进程开始...\n");
printf("父进程开始...\n");
execl("/bin/ls","ls","-al",NULL);
printf("父进程结束...\n");
printf("父进程结束...\n");
printf("父进程结束...\n");
return 0;
}
我们清楚地看到,调用完execl
函数后下面的“父进程结束…”并没有执行,这是为什么呢?
好的这里让我们先来学习一下execl
函数的用法和原理吧。
1. execl()
a. 用法
通过查看手册发现,execl函数的完整形式是int execl(const char *path, const char *arg, ...);
形参
path
表示路径参数,arg
表示执行命令,...
表示可变参数
上述代码中的path
是/bin/ls,表示执行文件的路径为/bin/ls。
arg
是"ls",表示指令ls。
...
是"-al",NULL,表示指令的可选项,还有execl最后都是以NULL结尾的,表示参数结束了。
execl的效果就是自调用execl函数
后,该进程后面的代码全被execl函数所调用的程序代码所覆盖。也就是说,后面的printf("父进程结束...\n");
全部被替换成了命令ls -al 所执行的代码
,这就解释了为什么看不到"父进程结束…",而看到当前目录的内容了。
- 返回值
execl既然是函数,那肯定有调用失败的时候,它返回的是一个int型,显然失败的时候返回-1,成功则什么都不返回。
b. 原理
当进程调用一种execl函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用execl并不创建新进程,所以调用exec前后该进程的id并未改变。
大致原理图:
好的,现在我们知道了如何执行新的程序任务的办法,其实关于execl函数的知识还有很多,下面就是相关的六个函数。
2. execl的相关函数及关系
通过查手册,我们观察到与其相关的函数还有五个,分别是execlp,execle, execv,execvp,execvpe。现在让我们简单介绍一下他们的用法。
以下五个函数的用法都一样,仅仅只是形参不同。这里主要介绍一下形参的区别。
下面代码均是问题导入代码中替换execl函数的部分代码。
execlp
int execlp(const char *file, const char *arg, ...);
execlp("ls", "ls", "-al", NULL);
- 讲解:
看到形参的名字是file的,这表示这个函数自己会去环境变量里面找路径了,形参file传文件名就行了,所以传个ls
execv
int execv(const char* path, char* const argv[])
char *const file[] = {"ls","-al", NULL};
execv("/bin/ls",file);
- 讲解
教大家一个便于理解的技巧,execl中的l是list的意思,而这里的v就代表vector的意思,所以后面是指针数组
execvp
int execvp(const char *file, char *const argv[]);
char* argv[] = {"ls","-al",NULL};
execvp("ls", argv);
- 讲解:
显然这个看函数名就大概能知道其形参的含义了,v表示以数组传入,p表示文件名,是的就是前面的缝合体。
从这里就有点不一样了,为了更好的理解execle的功能,所以我们来看改进后的代码
execle
int execle(const char *path, const char *arg, ..., char * const envp[]);
进程替换.c
#include<stdio.h>
#include<unistd.h> // execl函数的头文件
int main()
{
printf("父进程开始...,pid = %d\n",getpid());
printf("父进程开始...,pid = %d\n",getpid());
printf("父进程开始...,pid = %d\n",getpid());
char* myenv[] = {"myenv=hello, man", NULL };// 自定义的环境变量
execle("./son", "son", NULL, myenv);
printf("父进程结束...\n");
printf("父进程结束...\n");
printf("父进程结束...\n");
return 0;
}
另外一个程序 son.cc
#include<iostream>
using namespace std;
#include<unistd.h>
#include<stdlib.h>
int main()
{
cout << "新任务进行中...pid = " << getpid() <<
" myenvir : "<< getenv("myenv") << endl;
return 0;
}
执行效果:
- 讲解:
l表示列表,e表示自定义环境变量,所以前面的形参还是一样的,只是多了一个形参char * const envp[]
,这个形参就是我们自定义的环境变量,如代码“进程替换.c”中的指针数组myenv的第一个。
execvpe
int execvpe(const char *file, char *const argv[], char *const envp[]);
char* myenv[] = {"myenv=hello, man", NULL };
char* argv[] = {"son", NULL};
//execle("./son", "son", NULL, myenv);
execvpe("./son", argv, myenv);
小改动,效果和上面一样
- 讲解
v表示用数组来表示可选参数,p表示函数自己回去环境变量里面找程序文件,e表示自定义环境变量。
对于环境变量,有一点要注意:
自定义的环境变量会覆盖原有进程的环境变量
如图:
- 单独运行son.cc
- 运行myfile
如果不想覆盖当前的环境变量,而是在此基础上添加一个,则可以用函数
putenv()
如:putenv(“myenv=666”)
修改代码
联系
其实,上面我们所学的函数,都是这个函数execve
的封装,execve
是系统调用,c/c++为了便于我们开发者使用,所以对execve进行了封装,然后获得了上面所学的多个版本。