如何从零实现一个简单的命令行解释器(进程控制实战大运用)

目录

0.前言

2.代码书写

2.1 打印提示行

 2.2 用户输入

2.3 解析字符串

2.4创建子进程执行程序替换

2.5 第三方命令vs内建命令

2.6 框架完善

3. 结果展现


0.前言

我们在之前的博客中对进程控制(尤其是进程等待以及进程的程序替换做了超级十分噼里啪啦爆炸详细的讲解)下面是链接:

(3条消息) [入门篇]大二学生猛写300百字,用最生动的方法讲明白操作系统进程控制_yuyulovespicy的博客-CSDN博客icon-default.png?t=M85Bhttps://blog.csdn.net/qq_63992711/article/details/128070457?spm=1001.2014.3001.5502本文我们将在上篇博客的基础,从零开始敲,实现一个命令行解释器,至于什么是命令行解释器,我也用一个超级生动的方式进行了讲解:

(3条消息) 一个生动的例子让你理解Linux的Shell外壳_yuyulovespicy的博客-CSDN博客_linux shell和壳icon-default.png?t=M85Bhttps://blog.csdn.net/qq_63992711/article/details/126923186?spm=1001.2014.3001.5502本文代码以及代码说明已经上传至Gitee,可以自取:

practice13 · onlookerzy123456qwq/Linuxtwo - 码云 - 开源中国 (gitee.com)icon-default.png?t=M85Bhttps://gitee.com/onlookerzy123456qwq/linuxtwo/tree/master/practice13

1.思路构建

命令行解释器,其实就是我们的bash是所有在命令行上的跑起来的进程的父进程,比如我们的指令可执行程序,如ls,pwd等,这些跑起来进程的父进程都是bash,我们自己编译出的可执行程序a.out,myproc等,其父进程都是命令行解释器bash。

 图中的媒婆,其实就是shell,也就是我们的命令行释器bash,其实是当用户父进程bash传入指令字符串之后,由父进程bash,对指令,工具(可执行文件)指令字符串进行解析从系统的路径中寻找该可执行程序,然后父进程bash创建一个子进程,让子进程去执行如上的指令工具等可执行程序。

 那这里我们就有两个问题需要解决:

第一个问题:在命令行中执行可执行程序,我们知道带不同的命令行参数可执行程序所执行出的结果是不同的。如图中ls可执行程序。所以我们如何让bash知道这个可执行程序是如何在命令行执行的呢?

 所以用户传入给bash父进程的指令字符串,我们需要对这个指令进行解析。如“ls -a -l”,我们需要对之进行分成,“ls”,“-a”,“-l”三个字符串,对这个用户输入进行解析。然后就可以在系统路径中寻找该可执行程序,并知道如何运行该可执行程序

第二个问题:如何让bash创建的子进程来执行用户输入的可执行程序呢?

这就要使用我们的系统调用接口exec*。我们可以在bash创建的子进程中使用exec*使得子进程去执行相应的可执行程序。如用户输入了ls这个可执行程序,那就在bash的子进程中execvp("ls","ls",NULL);让子进程执行ls可执行程序,然后子进程退出。

 

2.代码书写

2.1 打印提示行

模仿命令行解释器,首先我们看到是在屏幕打印提示符,然后我们需要注意这个打印之后,光标还是在同一行,所以打印的时候不能带\n。

 但是我们printf采用的是行刷新策略,printf("[zy@VM-20-9-centos]");并不会立即刷新,而是必须等到\n或者是程序退出才会刷新到屏幕中,此时我们就要借助fflush接口,可以将printf刷新到的C语言级别的缓冲区的数据,立即刷新到屏幕上。

printf("[zy@VM-centos2022]$");
fflush(stdout);   

 2.2 用户输入

之后我们就需要接收用户输入的指令(可执行程序名以及相关命令行参数),如“ls -a -l”等就需要我们进行接收,但是需要注意这个字符串是带空格的,如果我们使用scanf接口输入,则遇到空格就会停止接收。此时我们有两个方法,一个是使用C语言的接口fgets,可以接受到输入带空格的整个字符串。另一个则是使用系统调用接口read,也可以接受键盘输入带空格的整个字符串。

下面看一段代码就理解了(注意看注释):

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define NUM 129
int main()
{
  char reastr[NUM];
  //read遇到回车才会停止读入
  //而且是全部读入,空格会读入,读到的回车也会读入
  //尾部也自动添加‘\0’
  //这时候需要注意对\n处进行赋0值处理
  ssize_t rdnum = read(1,(void*)reastr,sizeof(reastr));
  reastr[rdnum-1]='\0';
  printf("%s\n",reastr); 
  
  char fgestr[NUM];
  //fgets遇到回车才会停止读入
  //而且是全部读入,空格会读入,读到的回车也会读入
  //尾部也自动添加‘\0’
  //这时候需要注意对\n处进行赋0值处理
  fgets(fgestr,NUM,stdin);
  fgestr[strlen(fgestr)-1]='\0';
  printf("%s\n",fgestr);
   
  char scfstr[NUM];    
  //scanf是遇到空格/回车都会停止读入,空格和回车都不会被读入,都会自动过滤    
  //且自动对尾部添加'\0’    
  scanf("%s",scfstr);    
  printf("%s\n",scfstr);         
  
  _exit(0);
}

 所以我们完成了第二步:

     char command[NUM] = {0};
     //1.输出提示语
     printf("[zy@VM-centos2022]$");
     fflush(stdout);
     //2.输入指令
     command[0] = '\0';
     ssize_t rnum = read(0,command,NUM); 
     command[rnum-1]='\0';

2.3 解析字符串

对于用户输入的字符串我们要进行处理,如“ls -a -l”,我们就要把这个字符串解析成“ls”,“-a”,“-l”,才能更方便供我们后面使用。比如系统调用接口exec*就需要使用这样分开的单字符串才可以。

我们观察“ls -a -l”,需要我们用空格对之进行分解,我们想到了C语言接口strtok,可以按照传入的分割字符,对原来的字符串进行划分,比如"ls -a -l",我们就可以对之赋为“ls\0-a\0-l”这个字符串,同时也可以获取三个字符串指针,分别指向“ls”,“-a”,“-l”

具体使用可以参考网站strtok - C++ Reference (cplusplus.com)icon-default.png?t=M85Bhttps://legacy.cplusplus.com/reference/cstring/strtok/?kw=strtok

     char command[NUM] = {0};
     //1.输出提示语
     printf("[zy@VM-centos2022]$");
     fflush(stdout);
     //2.输入指令
     command[0] = '\0';
     ssize_t rnum = read(0,command,NUM); 
     command[rnum-1]='\0';
     //3.分开指令
     char* commandarr[COM] = {NULL}; 
     const char* sep = " ";
     int i = 1;
     for(commandarr[0]=strtok(command,sep);(commandarr[i]=strtok(NULL,sep));++i)
     {;}

2.4创建子进程执行程序替换

在解析出指令之后,我们就可以让父进程创建子进程,让子进程找到存储在系统当中的该可执行程序,执行子进程的程序替换,就可以让子进程执行该程序(该程序的代码和数据)。

我们使用fork接口来创建子进程,之后我们让子进程去执行exec*程序替换接口,这是我们注意到,我们已经获取到的是指针数组(所以我们选择使用数组vector传参),还有一件事 ,通常都是系统级别的指令程序,也即都是存储在系统级别的默认路径之下的(所以我们让其在PATH下自动搜寻路径即可),为了方便我们使用execvp。

     char command[NUM] = {0};
     //1.输出提示语
     printf("[zy@VM-centos2022]$");
     fflush(stdout);
     //2.输入指令
     command[0] = '\0';
     ssize_t rnum = read(0,command,NUM); 
     command[rnum-1]='\0';
     //3.分开指令
     char* commandarr[COM] = {NULL}; 
     const char* sep = " ";
     int i = 1;
     for(commandarr[0]=strtok(command,sep);(commandarr[i]=strtok(NULL,sep));++i)
     {;} 
     //4创建子进程进行程序替换执行
     if(fork()==0){
     execvp(commandarr[0],commandarr);
     perror("execvp");
     _exit(1);
     }

2.5 第三方命令vs内建命令

我们知道每次用户输入指令,父进程bash都要创建子进程去执行,但是所有指令都需要子进程去执行吗?是不是有些进程必须来由父进程去亲自去执行?答案是肯定的。举个例子来说明。

首先我们先明白两个事情,进程之间具有独立性,父进程bash和fork出的子进程也是一样的。第二件事是进程所处的路径其实是进程的一个重要的属性,如我们看到进程的cwd项就记录着该进程的路径。

所以如果我们让子进程执行如cd ..,此时是子进程去到了上级路径,是子进程的cwd发生了改变,而父进程bash的cwd并没有改变。ls,pwd这类的指令让fork的子进程执行还好,如果cd这种指令还让子进程执行的话,那移动路径就是马上要退出的子进程,再度回到父进程后,我们就会发现bash的路径并没有发生改变。

 所以我们需要单拎出来这些指令,进行单独的处理,如当检测出是cd指令的时候,我们就需要单独处理,让父进程去执行,但是我们不能让父进程执行程序替换,因为这样父进程就会退出了,所以我们让父进程执行相应的系统调用接口,如chdir可以和cd起到相同的作用。

char command[NUM] = {0};
     //1.输出提示语
     printf("[zy@VM-centos2022]$");
     fflush(stdout);
     //2.输入指令
     command[0] = '\0';
     ssize_t rnum = read(0,command,NUM); 
     command[rnum-1]='\0';
     //3.分开指令
     char* commandarr[COM] = {NULL}; 
     const char* sep = " ";
     int i = 1;
     for(commandarr[0]=strtok(command,sep);(commandarr[i]=strtok(NULL,sep));++i)
     {;}
     //4.1主进程执行的指令
     if(strcmp(commandarr[0],"cd")==0)
     {
       chdir(commandarr[1]);
     }
  
     //4.2创建子进程进行程序替换执行
     if(fork()==0){
     execvp(commandarr[0],commandarr);
     perror("execvp");
     _exit(1);
     }

2.6 框架完善

父进程调用完子进程去执行完相应的指令之后,需要等待回收子进程执行完毕退出之后的僵尸资源。在tinybash父进程等待成功之后,tinybash就要开始下一次接收用户指令->程序替换执行....... 所以我们可以看到命令行解释器的实现其实就是一个无限的循环

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#define NUM 257
#define COM 128
int main()
{
  while(1){
     char command[NUM] = {0};
     //1.输出提示语
     printf("[zy@VM-centos2022]$");
     fflush(stdout);
     //2.输入指令
     command[0] = '\0';
     ssize_t rnum = read(0,command,NUM); 
     command[rnum-1]='\0';
     //3.分开指令
     char* commandarr[COM] = {NULL}; 
     const char* sep = " ";
     int i = 1;
     for(commandarr[0]=strtok(command,sep);(commandarr[i]=strtok(NULL,sep));++i)
     {;}
     //4.1主进程执行的指令
     if(strcmp(commandarr[0],"cd")==0)
     {
       chdir(commandarr[1]);
     }
  
     //4.2创建子进程进行程序替换执行
     if(fork()==0){
     execvp(commandarr[0],commandarr);
     perror("execvp");
     _exit(1);
     }
     waitpid(-1,NULL,0);
  }
  return 0;
}

3. 结果展现

  • 13
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值