Linux进程控制(三)---进程替换+简易shell的实现

目录

execl()

execv()

execlp()

execvp()

如何利用execl执行自己写的C/C++可执行程序?

如何利用makefile同时编译两个文件

execle()

execvpe()

简单shell的编写


什么是进程替换?

我们之前fork之后,是父子进程各自执行代码的一部分,然后父子代码共享,数据写时拷贝各自一份.

但是如果子进程就想执行一个全新的程序呢?子进程想拥有自己的代码,这就用到了程序的替换.

程序替换,是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载调用进程的地址空间中.

比如只有一个进程,先不考虑父子进程.

先把可执行程序加载到内存中,然后进程通过地址空间及页表的映射找到代码地址,然后执行.

 此时如果发生了程序替换,要替换成磁盘中另一个可执行程序other.exe,此时便会将新的磁盘上的程序加载到内存,并和当前页表建立映射.而原本的程序myproc.exe几乎不发生变化.

而这些工作,可以使用exec系列的接口来实现的.

所以进程替换的原理是:

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

其中,exec系列函数的本质就是如何加载程序的函数.

我们来演示两个例子,一个是单独一个进程,一个是父子两个进程.

先来看一个进程的:

make编译然后运行:

正如我们的预期.

然后我们进行进程替换,当然需要用到上面所提到的exec系列函数.

execl()

我们man execl查看用法:

 

这里一共有6个相关的函数,但学会一个,后面的基本上也不是什么问题.

那我们先来第一个execl.

path是要新加载程序的路径。path:路径+目标文件名

第二个是传入一个字符串作为参数,等下会细说.

...这个叫做可变参数列表,即可以传入多个、不定个数的参数.最后一个参数必须是NULL结尾标识参数传递完毕.

我们在命令行上怎么写的,在这里参数就怎么填.什么意思呢,我们看下面的例子.

打开刚才那段代码,利用execl函数,然后我们先用一下系统的程序,例如填写ls的路径,然后参数的话,我们想执行ls,会在命令行上输入ls,所以参数写一个ls即可,如下:

 退出来之后,make编译执行.

首先,我们发现execl之前的代码正常运行了,而execl之后的代码并没有运行,这说明此时程序已经被ls替换了.它会将当前代码所有的代码和数据都进行替换,包括执行的和未执行的,一但替换成,后面的代码都不会被执行了.

其次我们发现ls的命令也被执行了,说明替换的程序也正常运行了.

这便是execl的一个用法.

当然还可以加更多的参数,例如:

然后运行:

 此时发现程序相当于执行了ls -a -l,显示了隐藏的文件及详细信息.

当然可以换成其它的命令,which,pwd,top...等等.只要填写其路径即可.

 知道了用法,然后返回值还没说

 说execl只有返回失败的时候才有返回值,返回-1.

也就是说,execl进程替换成功是不会有返回值的.

其实我们仔细想一下也是这样的,execl进程替换成功后,会连自己的execl这一行代码都替换掉,取而代之的一个全新的程序,所以返回值在这里也是没有意义的.

接下来我们演示一个父子两个进程的例子.

  然后我们预期的结果应该是子进程ls之后,父进程显示等待成功,并输出子进程的退出码.

可以发现,正如我们预期的所示.

为什么要创建子进程?

为什么不影响父进程,父进程聚焦在读取数据,解析数据,指派进程执行代码的功能!

父进程负责fork,管理这些子进程,子进程负责程序替换,完成自己的工作. 

execl之后,父子进程的关系?

加载新程序前,父进程和子进程的关系是代码共享,数据写时拷贝.

当子进程execl加载新程序后,它们父子间代码需要分离,代码需要进行写时拷贝,这样父子进程在代码和数据上就彻底分开了.

execv()

我们同样的先用man查看一下用法

 注意和execl的区别:

 execl的传参类似list的方式,

execv是指针数组,和execl没有本质的区别,只有传参上的区别,execv需要我们传入一个指针,这个指针指向的是这个argv数组.我们事先把选项写好到argv数组中,而execl需要我们把每个选项都当做参数传入.

以上是execv的传参方式.我们看用代码是如何写的.

 注意和execl的区别,execv相当于是先在外面写好之后,再把写好的数组传进来.

 

运行之后,结果依然正确.

execlp()

我们依然man查看一下用法:

这个file和之前的path有什么区别呢?

我们上面说的,要寻找替换的文件,需要写它的路径。那么如果不带路径,可以找到程序吗?

当然是可以的,环境变量便是如此。例如我们平常运行ls的时候,不需要加路径即可运行,而我们运行我们自己的可执行程序时,需要加上路径./来运行.

所以execlp的file意思是它会自动在环境变量PATH中寻找,不用告诉它程序在哪里.

用代码这样写:

 首先它会自动在环境变量PATH中寻找"ls",然后执行ls -a -l.

同样地,照样结果正确.

同时你要分清以上两个ls的区别:第一个ls意思是你想执行谁(查找路径),第二个ls是你想怎么执行(匹配). 

execvp()

这个类似于execl和execp的区别,只是第二个参数不同,即除了传参方式不一样,别的本质都是一样的.

 注意和上一个execlp的区别:

只有第二个参数不一样,只是传参方式改变:

 

照样可以正常运行:

如何利用execl执行自己写的C/C++可执行程序?

我们可以在myproc.c文件中利用execl函数调用 由mycmd.c文件编译形成的mycmd可执行文件.

然后利用make编译两个文件,再执行myproc,便可以调用到自己写的C可执行程序(mycmd).

首先继续刚才的,再myproc.c文件中,先把mycmd的路径写入,为什么方便以后修改,我们可以直接#define一下,然后传入到execl函数中,执行-a选项.

 然后编写mycmd.c文件:

需要用到main函数里的命令行参数,如果输入的没有两个参数,就直接结束程序,若输入

mycmd -a ,则输出a.

mycmd -b,则输出b.

然后退出,make编译,那么有一个问题:

如何利用makefile同时编译两个文件

我们之前在make/makefile里讲过,如果直接make,便会从makefile中从上到下执行只执行第一条语句,这样只能编译一个文件,到时候在编译很多文件时,得一个一个编译,会很麻烦.

这个时候便用到了伪对象,也是那一章说过的.

定义一个伪对象all,比如我们最后要形成两个可执行文件mycmd和myproc.

然后我们只维护一个依赖关系,让伪对象all依赖于mycmd与myproc.

这样编译器遇到all时,会自动向下找到这两个语句,然后再分别编译得到它们,这样就成功了.

make编译好之后,我们运行myproc文件

可以发现已经成功运行了我们自己写的C程序了. 

execle()

 可以发现前两个参数和我们之前的那一套一模一样,所以我们只用看第三个参数即可.

第三个参数是一个环境变量,准确来说是来传递环境变量给新的程序的.

我们在原来的程序myproc.c文件中,写入一个环境变量:

然后再mycmd.c文件中获取环境变量

此时我们再次make编译并运行.

 

便成功获取到了环境变量.

当我们不传入环境变量时,即新的进程获取不到,便会返回null.

execvpe()

这个接口无非是前面几个接口参数的叠加,只要会前面的几个接口,这个接口也照样可以使用.

file是从环境变量里面查找,argv是要传入的参数选项,envp是要传递的环境变量,前面几个接口都有所提到,这里便不再做演示.

当然以上6个都不是严格意义上的系统接口,只是系统提供的基本封装.真正的系统接口是execve.

 filename需要把文件的全路径写上,argv同样也是同上,是参数和选项,最后一个参数也是传递环境变量.

到这里进程替换就讲完了,这些函数很多而且名字相近,各函数的作用也不相同,要记忆起来也是比较困难.

其实仔细观察这些函数名,也是有规律的.

 l(list) : 表示参数采用列表,即将参数全部传入到函数中,例如execl,execlp,execle.
v(vector) : 参数用数组,即先在外部将参数选项写入到数组里,再传入到函数中,例如execv,execvp.
p(path) : 有p自动搜索环境变量PATH,自带path,不需要写文件的全部路径,如execlp,execvp.
e(env) : 表示自己维护环境变量,可以传入环境变量,如execle.

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

这张图也是对以上的一种总结,不用死记硬背,理解以上几个意思,看到函数便知道需要怎么传入参数了.

简单shell的编写

简单shell的编写基本上覆盖了之前所有进程控制的知识点,包括进程创建,进程终止,进程等待以及今天的进程替换,具体细节可以查看源代码,会有详细的注释.

这里唯一需要注意的是,当父进程bash创建子进程后,子进程执行的进程替换,即命令是无法完成内置命令的,例如cd这样的,子进程刚完成cd然后进程就退出了,这样并没有意义.

所以我们需要让父进程bash亲自完成,这里需要用到一个chdir函数,具体怎么使用可以man chdir来查看用法,总体代码如下:

    include<stdio.h>                                                                                                                                
    2 #include<stdlib.h>
    3 #include<string.h>
    4 #include<unistd.h>
    5 #include<sys/types.h>
    6 #include<sys/wait.h>
    7 
    8 #define NUM 1024
    9 #define SIZE 32
   10 #define SEP " "
   11 //保存打散之后的命令行字符串
   12 char* g_argv[SIZE];
   13 //保存完整的命令行字符串
   14 char cmd_line[NUM];
   15 
   16 //shell 运行原理: 通过让子进程执行命令,父进程等待 和 解析命令
   17 int main()
   18 {
   19   //0.命令行解释器,一定是一个常驻内存的进程,不退出
   20   while(1)
   21   {
   22     //1.打印出提示信息:[root@localhost myShell]$ 
   23     printf("[root@localhost myShell]# ");
   24     fflush(stdout);
   25     memset(cmd_line,'\0',sizeof(cmd_line));
   26     //2.获取用户的输入[输入的是各种指令和选项:"ls -a -l"]
   27     if(fgets(cmd_line,sizeof(cmd_line),stdin) == NULL)
   28     {
   29       continue;
   30     }
   31     cmd_line[strlen(cmd_line)-1] = '\0';
   32     //ls -a -l\n
   33     //printf("echo: %s\n",cmd_line);
   34     //3.命令行字符串解析:"ls -a -l" -> "ls" "-a" "-l"
   35 
   36     g_argv[0] = strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
   37     int index = 1;
   38     //这段代码等价于下面while(g_argv[index++] = strtok(NULL,SEP));
   39    // while(1)
   40    // {
   41    //   g_argvp[index] = strtok(NULL,SEP);//第二次,如果还要解析原始字符串,则传入NULL
   42    //   index++;
   43    // }
           //如果是ls命令,我们可以给它加上颜色
   44     if(strcmp(g_argv[0],"ls") == 0)
   45     {
   46       g_argv[index++] = "--color=auto";
   47     }
   48     while(g_argv[index++] = strtok(NULL,SEP));
   49     //for DEBUG
   50    // for(index = 0; g_argv[index]; index++)
   51    // {
   52    //   printf("g_argv[%d]:%s\n",index,g_argv[index]);
   53    // }
   54    //
   55     //4.TODO 内置命令:让父进程(shell)自己执行的命令,我们叫做内置命令(内建命令)
   56     //内置命令本质就是shell中的一个函数调用
   57     if(strcmp(g_argv[0],"cd") == 0)//不想让子进程执行,而是父进程执行
   58     {
   59       if(g_argv[1] != NULL)
   60       {
   61         chdir(g_argv[1]);
   62         continue;
   63       }
   64     }
   65     //5.fork()
   66     pid_t id = fork();
   67     if(id == 0)
   68     {
   69       //child process
   70       printf("功能让子进程执行\n");                                                                                                              
   71       execvp(g_argv[0],g_argv);//ls -a -l
   72       exit(1);
   73     }
   74        else
   75     {
   76       //father process
   77       int status = 0;
   78       pid_t ret = waitpid(id,&status,0);
   79       if(ret > 0)
   80       {
   81         printf("exit code:%d\n",WEXITSTATUS(status));
   82       }
   83     }
   84   }
   85   return 0;
   86 }                  

我们make编译好后然后运行:

发现功能都可以正常使用了. 

猜你喜欢

转载自blog.csdn.net/weixin_47257473/article/details/131827129