0


【Linux】教你用进程替换制作一个简单的Shell解释器

本章的代码可以访问这里获取。

由于程序代码是一体的,本章在分开讲解各部分的实现时,代码可能有些跳跃,建议在讲解各部分实现后看一下源代码方便理解程序。

制作一个简单的Shell解释器


一、观察Shell的运行状态

我们想要制作一个简单的

Shell

解释器,需要先观察Shell是怎么运行的,根据

Shell

的运行状态我们再去进行模拟实现。

我们可以先考虑下面的指令与Shell的互动:

在这里插入图片描述

我们仔细进行分析可以发现,

Shell

执行上面的命令时,可以被理解为下面的过程。

在这里插入图片描述
当然上面的命令都是普通命令,所以

Shell

都是通过创建子进程的方式来执行的,对于一些内建命令(

Shell

自己去执行命令)我们现在还不考虑,在后面的部分我们再进行进一步的讨论内建命令应该怎么去处理。

二、简单的Shell解释器制作原理

通过观察

Shell

的运行状态,我们知道然后

Shell

读取新的一行输入,建立一个新的子进程,在这个子进程中运行程序并等待这个进程结束。

所以要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork
  4. 替换子进程(execvp),执行替换后的程序
  5. 父进程等待子进程退出(wait

1、获取命令行

我们在在

Shell

中输入的命令本质上就是输入一个字符串,因此我们想要获取命令行,可以先创建一个字符数组

commandstr

,然后使用C语言的

fgets

函数从键盘中进行读取数据到字符数组里面,这样我们就获取了一个命令行了。

注意:

  1. 这里不能使用scanf函数 ,这里的命令会包含空格,会导致scanf读取不到完整的数据。
  2. fgets函数会将我们输入的命令时的最后一个的\n符也给读取到字符数组内,我们需要特殊处理将\n进行用\0进行覆盖
//这里包含的头文件是我们整个程序需要用到的所有头文件#include<stdio.h>#include<unistd.h>#include<assert.h>#include<string.h>#include<sys/types.h>#include<sys/wait.h>#include<stdlib.h>//这里的N用于定义字符数组的大小#defineN128intmain(){//存储命令行的字符数组char commandstr[N]="";//Shell要一直运行接受命令,所以这里必须是死循环!while(1){//模拟Shell的提示符printf("[hong@machine MiniShell]# ");//从标准输入流中读取字符串char* s =fgets(commandstr,sizeof(commandstr),stdin);assert(s);//判断fgets是否读取成功//处理\n   示例字符串:ls -a -l\n\0
    commandstr[strlen(commandstr)-1]='\0';}

2、解析命令行

虽然我们通过前一步已经拿到命令行,但是我们还不能直接使用,因为我们拿到的字符串中间可能有许多空格以及一些其他的问题,我们还需要将命令行的字符进行切割提取出我们想要的子串,这样才符合程序替换函数的要求。例如:将

ls -a -l

提取成

ls

,

-a

,

-l

对于字符串的切割,我们可以使用C语言提供的

strtok

函数,由于切割以后我们的字符串从一个变成了多个,因此我们需要用一个字符串指针数组

argv

,存储每一部分切割后的首地址,同时这个

argv

也可以直接传递给

execvp

函数进行程序替换了。

//在全局域中 定义切割符#defineSEP" "//main函数的外部 定义一个命令行切割函数intsplit(char commandstr[],char* argv[]){assert(commandstr);assert(argv);//第一次切割
  argv[0]=strtok(commandstr, SEP);if(argv[0]==NULL){//返回 -1表示异常退出return-1;}//循环切割int i =1;while((argv[i++]=strtok(NULL, SEP)));return0;}//main函数内部,while循环上面定义切割后的字符指针数组char* argv[N]={NULL};//while循环内部//切割字符串  例如将"ls -a -l " 变为 "ls" "-a" "-l"int n =split(commandstr, argv);if(n ==-1){//切割失败就终止本次循环continue;}

在这里插入图片描述

3、创建子进程 进行程序替换 父进程等待

创建子进程而我们可以使用

fork

函数进行创建,创建完以后进程的执行流由一个变成了两个,我们在子进程中进行程序替换可以使用

execvp

命令,同时我们的

argv[0]

就是程序名,

argv

中存储的就是命令按照什么方式进行执行。

最后我们的父进程可以在外面进行阻塞等待,然后获取子进程的退出码和退出信息。

//main函数内部,while循环上面定义退出码变量int last_status =0;//while循环内部//创建子进程,进行命令处理pid_t id =fork();assert(id >=0);if(id ==0){//child processexecvp(argv[0], argv);//如果执行到这里说明程序替换失败  exit(-1);}//父进程等待子进程int status;int pid =waitpid(id,&status,0);//等待成功就提取退出码信息if(pid >=0){
      last_status =WEXITSTATUS(status);}}return0;

4、实际运行

我们可以执行

ls
pwd
ps -axj

命令 看一看效果。
在这里插入图片描述

二、对简单的内建命令进行处理

我们知道内建命令是让

Shell

自己执行的命令,而不是让子进程执行的命令,例如

cd

命令就是内建命令,因为我们要改变的是

Shell

自己的工作目录,而不是子进程的工作目录,类似的命令还有

export
env
echo

命令。

由于上面我们写的程序执行命令时都是交给子进程去做的,所以我们上面写的程序是没有办法执行内建命令的,或者说能执行内建命令,但不是我们想要的结果或目的。

所以接下来我们要对这个简单的

Shell

进行改造,让它能够执行一些简单的内建命令,还有刚刚我们的

ls

命令没有色彩,我们也要进行一些修改。

1、给ls命令加上色彩

在真正的

Shell

中我们执行的

ls

命令其实是

ls --color=auto

ls

被我们真正的

Shell

进行了起别名。

在这里插入图片描述
我们在运行我们自己制作的

Shell

时也可以加上

--color=auto

//此段代码应该在切割字符串之后//argv[0]就是我们的命令名if(strcmp(argv[0],"ls")==0){int pos =0;//寻找指针数组的结尾while(argv[pos++]);//在NULL位置加上 --color=auto
      argv[pos -1]="--color=auto";//将后一个位置置空
      argv[pos]=NULL;}

这样以后我们在我们自己制作的

Shell

中执行

ls

命令时也会由颜色了!

2、支持cd命令

对于

cd

命令如果让父进程进行执行,我们可以调用系统调用

chdir

我们只需要传递一个参数:路径字符串,当执行成功时会返回0,执行失败会返回-1,并设置错误码。

在这里插入图片描述

//此段代码应该在ls添加颜色之后elseif(strcmp(argv[0],"cd")==0){//argv[1]里面存放的是路径字符串if(argv[1]==NULL){printf("没有正确的路径!\n");//设置错误码
        last_status =-1;continue;}//执行系统调用改变父进程的工作目录chdir(argv[1]);continue;}

3、支持export命令

export

命令可以将一个本地变量加入到环境变量表中,我们让我们自己制作的

Shell

完成

expoprt

命令可以用C语言提供的函数

putenv

函数,但是在向环境变量表加入新的环境变量时,我们要维护好我们加入到环境变量,这个环境变量不能够被轻易的覆盖,否则环境变量表在找我们的环境变量时就会找不到,所以我们还要创建一个我们自己维护的二维数组。

//在全局域中定义// 自己维护的二维数组最多能向环境变量表几个自定义的环境变量#defineMAX64//main函数内部,while循环上面定义//指向下一个要添加的环境变量的位置int env_index =0;//要维护的二维数组char envstr[MAX][N];//此段代码应该在ls添加颜色之后elseif(strcmp(argv[0],"export")==0){//声明putenv函数否则会编译器会有警告externintputenv(char*string);//argv[1]位置应该是环境变量if(argv[1]==NULL){printf("没有输入变量!\n");
        last_status =-1;continue;}//将argv[1]位置的环境变量,拷贝到env_str中,否则下一次解析的命令会覆盖环境变量strcpy(envstr[env_index], argv[1]);//将环境变量导入环境变量表putenv(envstr[env_index++]);}

4、支持env命令

对于

env

命令我们只需要写一个打印环境变量表的函数就能完成此命令了。

//main函数的外部 定义一个打印环境变量表的函数voidshowEnv(){externchar** environ;int i =0;while(environ[i]){printf("%d : %s\n", i, environ[i++]);}}//此段代码应该在ls添加颜色之后elseif(strcmp(argv[0],"env")==0){showEnv();continue;}

5、支持echo命令

echo

命令可以用于打印环境变量,也可以打印退出码,这取决于

$

后面是不是

?

?

我们就可以打印

last_status

,不是我们就用

getenv

命令拿到环境变量的内容。

//此段代码应该在ls添加颜色之后elseif(strcmp(argv[0],"echo")==0){if(*argv[1]=='$'){if(*(argv[1]+1)=='?'){printf("process exit code %d\n", last_status);continue;}else{char* str =getenv(argv[1]+1);printf("%s\n",str);continue;}}}
标签: linux 运维 服务器

本文转载自: https://blog.csdn.net/qq_65207641/article/details/130367045
版权归原作者 看到我请叫我滚去学习Orz 所有, 如有侵权,请联系我们删除。

“【Linux】教你用进程替换制作一个简单的Shell解释器”的评论:

还没有评论