本文将详解进程替换以及实现进程替换的七个函数,并通过进程替换实现一个简单的shell。
进程替换就是指将当前程序替换成一个新的程序,让当前进程执行这个新程序,如果替换成功了,原来的程序就不会被执行,也不会返回原来的返回值。
进程替换通常是在子进程中完成的,在学习进程替换之前,我们都是通过条件判断fork()的返回值使用子进程来执行父进程的一部分的代码,其实意义并不大。学习了进程替换可以使子进程执行一个全新的程序。
一个进程包含PCB,mm_struct(虚拟内存)等结构体,用来联系虚拟内存与物理内存的页表,以及程序的代码和数据。当进程中发生进程替换时,只有代码和数据发生替换,其他的内容不变。也就是说进程替换没有创建新的进程。
进程运行起来时,它的代码和数据是要被加载到内存中的,是通过exec系列的函数来完成加载的。exec系列函数一共有七个,我们可以通过man手册来进行查询:
通过观察这几个函数我们发现它们都是在exec后面加入了几种后缀名。
可以通过后缀名的含义来进行区分记忆。
l:表示参数采用列表(以列表的方式一个一个传入进去)。
v:参数用数组。
p:有p自动搜索环境变量PATH(只要说名字就可以了,不用指明路径)。
e(env):表示自己维护的环境变量。(不用默认的环境变量)。
以l为后缀,说明以列表的形式传入参数。
int execl(const char *path, const char *arg, ...);
其中*path表示的是路径,arg表示的是要执行的程序,“…”表示的就是可变参数列表,即命令行上怎么执行这里就写入什么参数。必须以NULL作为参数列表的结束。
#include<unistd.h>
#include<stdio.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
if(fork()==0)
{
printf("command begin\n");
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("command fail\n");
exit(1);
}
waitpid(-1,NULL,0);
printf("wait child success\n");
return 0;
}
当子进程执行完打印command begin的语句的时候,进行进程的替换。其中替换的是/usr/bin/ls,在命令行要输入的是ls -a -l,将程序运行起来:
我们发现子进程被替换为了ls进程,并添加了-a -l等选项。
execv的后缀是v,指的是以数组的形式输出参数。
只需要将上面的execl替换为如下即可。
char* argv[]={"ls","-l","-a",NULL};
execv("/usr/bin/ls",argv);
其本质就是将参数列表放在了一个数组argv中。
后缀为l和p,l表示的是以参数列表的形式,p表示的是不用指明具体的路径,会根据环境变量去查找文件。
execlp("ls","ls","-l","-a",NULL);
就是将本来要指定的路径改成了通过环境变量进行查找。
后缀为v和p,v表示以数组的形式,p表示的是不指明具体的路径去查找文件:
char* argv[]={"ls","-l","-a",NULL};
execvp("ls",argv);
后缀为l和e,e表示自己维护环境变量,不使用默认的环境变量。即向要执行的程序中导入环境变量:
建立两个程序一个程序完成进程替换,另一个程序为被替换的程序,负责打印自己的环境变量:
//mytest.c用来打印自身的环境变量
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
extern char** environ;
int i;
for(i=0;environ[i];i++)
{
printf("%d:%s\n",i,environ[i]);
}
}
//在myload.c中进行进程替换
char* env[]={"MYENV1=hahaha","MYENV2=hehehe",NULL};
execle("./mytest","mytest",NULL,env);
这里要注意的是已经指明了路径,在可变参数列表中可以不写入./
当我们不使用进程替换的时候,直接运行mytest,生成的是系统默认的环境变量:
如果通过进程替换执行mytest的话,它的环境变量就被定义成了env[],运行的结果是:
execve的代码同理:
char* argv[]={"mytest",NULL};
char* env[]={"MYENV1=hahaha","MYENV2=hehehe",NULL};
execve("./mytest",argv,env);
char* argv[]={"mytest",NULL};
char* env[]={"MYENV1=hahaha","MYENV2=hehehe",NULL};
execvpe("mytest",argv,env);
由于p是在环境变量中找文件,那么就需要将我们新创建的文件添加到环境变量中,添加的方法为:
export PATH=$PATH:/home/用户名
此时再来执行myload得到我们希望获得的结果:
其实所有的接口本质上无差别就是传递的参数不同而已,是为了满足不同的应用场景进行的封装。
操作系统实际上只提供了一个接口那就是:execve,其他的函数都是对该接口封装而成的库函数。它们的底层都是使用execve来进行实现的。
既然我们已经了解了进程替换,那么我们就可以通过进程替换来调用命令行进程从而实现一个DIY的shell。
首先它一定是一个死循环的程序,因为shell在不停地等待我们输入内容。
char command[NUM];
command[0]=0; //定义commmand接收命令行信息
printf("[lhb@myhostname mydir]# ");
fgets(command,NUM,stdin);//在标准输入流(键盘)读取输入的信息存入command中
command[strlen(command)-1]='\0'; //由于将回车读入了进去,因此需要将回车部分置为'\0'
}
此时可以测试一下效果:
命令和每一个选项之间是用" "隔开的。因此通过查找空格的位置可以切割command字符串,从而找到每一个命令和选项。
char* argv[CMDNUM]={NULL};
argv[0]=strtok(command," ");
int i=1;
while(argv[i]=strtok(NULL," "))
{
i++;
}
这里涉及到C语言函数分割字符串strtok的使用,忘记的小伙伴可以查一查呀。
通过execvp函数进行进程替换即可:
if(fork()==0)
{
execvp(argv[0],argv);
exit(1);
}
waitpid(-1,NULL,0);
可以看到效果:
当我们执行ls时,是可以正常运行的,但是当我们执行cd命令的时候会发生错误:
我们发现当执行cd命令的时候,路径并没有发生改变。
这是因为cd是子进程执行的,当该子进程执行完cd路径退回后,子进程执行结束了,但是没有改变父进程的路径。所以当父进程再次创建子进程执行pwd时,路径还是之前的路径。C语言提供了chdir函数来改变当前工作路径。
当输入的命令是cd的时候,切换路径,并直接执行下一次循环。
if(strcmp(argv[0],"cd")==0)
{
if(argv[1]!=NULL) //当argv[1]不为空时切换路径为argv[1]
chdir(argv[1]);
continue;
}
可以添加一些功能比如获取子进程的pid等等:
int status=0;
waitpid(-1,&status,0);
printf("exit code:%d\n",(status>>8&0xFF));
进程替换一般应用在子进程,这样可以使子进程执行完全不同于父进程的代码,而自己实现shell的本质就是将shell作为父进程而命令行执行的程序作为子进程,对子进程的命令进行替换从而实现一个简单的shell。