环境:centos7.6,腾讯云服务器 Linux文章都放在了专栏:【Linux】欢迎支持订阅 相关文章推荐: 【Linux】冯.诺依曼体系结构与操作系统 【Linux】进程理解与学习Ⅰ-进程概念 【Linux】进程理解与学习Ⅱ-进程状态 【Linux】进程理解与学习Ⅲ-环境变量 【Linux】进程理解与学习Ⅳ-进程地址空间 【Linux】进程控制(创建、终止、等待)undefined 浅谈Linux下的shell--BASH 【Linux】进程优先级&前后台理解
在前文,我们学习了fork函数创建子进程,而创建子进程主要就是为了让它帮我们执行特定的任务。而我们之前所学的都只是为了让子进程帮我们执行父进程的部分代码(通过执行流分流的方式),并没有执行一个全新的程序。但实际上子进程也是可以执行一个新的程序。子进程可以通过程序替换的方式将父进程的代码与数据替换成新的程序对应的代码与数据。本文将对此进行探讨。
进程可以通过程序替换的方式来执行一个全新的程序,具体的做法则是通过对应的程序替换的几个系统调用函数来实现,下面先来看一下程序替换的现象,根据这个现象来分析程序替换实现的原理。
接下来我们通过该现象对此进行分析,现象如下:
我们可以看到,我们最终的执行结果并不是像我们想象的那样,而是将本该打印的end给替换成了执行ls这个指令。那么具体的原理是怎样的呢?如下图所示:
通过上图,也解释了为什么在执行我们的进程时,execl函数后面的end的打印并没有执行,因为在我们执行完打印begin后,开始调用系统调用函数execl,将新的程序(ls)的代码与数据加载到内存对应的位置,将老进程的代码与数据给替换掉,所以就执行不了后面的打印end指令了。(这里注意的是,程序替换是实现的代码与数据的整体替换)
那么这里不仅有一个问题:在进程替换时,有没有产生新的进程呢?
答案是没有的,因为我们仅仅只是将老进程里面的代码与数据,替换成新程序的代码与数据,实现程序替换。并没有产生新的进程。正如上图所示,原进程对应的pcb并没有发生改变,也没有产生新的pcb。
当然我们也可以通过代码来验证一下:
当然,假如我们使用fork创建子进程,让子进程完成程序替换,子进程的程序替换并不会影响父进程,这是因为父子进程都有各自独立的PCB,并且由于写时拷贝机制的存在,使得父子进程互相独立,互不影响。
接下来我们讲一下几个程序替换函数。总体一共有7个,其中这七个里的六个实际上底层都是调用第七个。总体如下:
exec函数家族关系
对于这些exec函数,它们都只具有失败时候的返回值,当程序替换失败时,会返回-1,同时继续往后执行exec后面的指令,当替换成功时会直接执行替换后的新程序。接下来逐一介绍。
首先介绍的是execl函数,我们在上面的演示中用到的就是该函数。
int execl(const char *path, const char *arg, ...);
对于该函数来说:
举例:
int execv(const char *path, char *const argv[]);
该函数我们发现,之前的l变成了v,实际上其实就是用了一个函数指针数组,将之前的"ls","-a","-l"的地址放进数组里,数组最后一位元素为NULL,然后将该指针数组的起始地址(数组名),当作execv的第二个参数。
对于该函数来说:
举例:
int execlp(const char *file, const char *arg, ...);
对于该函数,我们发现之前的path参数更换成了file,然后函数名中加了个p,其实代表的意思就是,会在PATH环境变量中根据file名查找file的路径,后面的参数代表的与execl一样。
对于该函数:
举例:
不过这里需要注意的是,自动搜索匹配路径是指在PATH环境变量中搜寻,假如一个新程序的路径并不在PATH中,则会匹配不上,就导致替换失败。如下:
程序替换失败
我们发现,该函数变成了vp结尾,v表示数组(指针数组)的形式,p表示自动搜索匹配环境变量PATH中的路径。
int execvp(const char *file, char *const argv[]);
对于该函数:
举例:
同样,这里自动匹配路径指的是在PATH中搜索,假如我们想要执行替换自己写的程序,就要将我们写的程序的路径用export导入环境变量即可。
这里的e,表示environ,即表示环境变量表。也就是说,我们可以将当前程序的替换成新程序,同时将老的环境变量表也传给新程序。
int execle(const char *path, const char *arg,
..., char * const envp[]);
对于该函数:
举例:
我们发现,这里v(数组)、p(PATH)、e(环境变量表),三者都集齐了。
int execvpe(const char *file, char *const argv[],
char *const envp[]);
对于该函数:
举例:
newtest程序:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
extern char** environ;
printf("我是新程序,我的环境变量表中的前三个环境变量是:\n");
for(int i=0;i<3; ++i)
{
printf("%d:%s\n",i,environ[i]);
}
return 0;
}
运行newtest:
mytest程序:实现程序替换(在此之前已经将newtest的路径导入了PATH)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<fcntl.h>
int main()
{
pid_t id=fork();//创建子进程
if(id == 0)
{
//child
printf("我是子进程,pid:%d\n",getpid());
char*const envp[]={"myval=520",NULL};//自定义环境变量表
const char* argv[]={"newtest",NULL};//指针数组
execvpe("newtest",argv,envp);//注意:我已经将newtest的路径导进了PATH
printf("程序替换失败\n");
exit(1);
}
//father
int status=0;
waitpid(id,&status,0);//进程等待
if(WEXITSTATUS(status)!=1)
{
printf("进程替换成功\n");
}
return 0;
}
已经将新程序路径导入环境变量
execve为最正宗的系统调用函数,我们这里讲解的其它的系统调用其实底层都是调用了该函数。
int execve(const char *filename, char *const argv[],
char *const envp[]);
对于该函数:
举例:
对于以上的这么多系统调用函数,可能看了都头大,但是仔细看,其实会有很多共性,可以利用这 个特点来更加巧妙地记住这些函数的用法。
end.
生活原本沉闷,但跑起来就会有风!🌹