熟悉Linux的小伙伴们都知道,shell作是用户使用系统的桥梁,那么今天我们便来看看shell是如何实现的;
shell是一种命令解释器,也是用户操作接口,Linux用户通过我们通过输入一系列的指令,被shell解释后调用需要的系统接口,从而操纵系统内核,完成期望的动作
所以,任何一个shell都必须要具备以下几点:
- 抓取分析信息:能够读入用户操作的命令,并将其解析为我们需要的参数
- 创建进程为了保证shell稳定运行,需要创建一个子进程,让用户期望的动作在子进程上完成,这样即使子进程崩溃,shell依然可以稳定运行
- 识别并实现特殊符号:诸如cd,>,>>这些符号,是需要shell或子进程自行实现的,所以我们需要对那些自行实现的符号进行识别
- 程序替换:shell是一个命令解释器,但它创建的子进程,是需要完成预期功能的,所以我们要使用程序替换,改变子进程的内存指针;
- 错误提示:当用户操作非法,或者进程异常时,我们要及时的反馈或记录这些错误信息,方便用户调试;
解析&实现
抓取用户输入
- Shell工作时,用户每次输入的是一个字符串,这串字符包含了指定程序名(命令名)—附加操作–目标文件或路径:
- 我使用了常用的scanf来抓取这些字符串,但scanf—%s一旦识别到空格变回停止抓取,所以需要用到正则表达式:
- scanf("%[^\n]%*c",buf):"%[^\n]"表示在遇到\n之前,scanf不会停止抓取字符,%*c表示抓取一个字符但不储存(扔掉),这是针对\n的,若是不这样做,scanf会到\n停止,但\n依然存在,scanf下次便会一直在这里等待,导致阻塞;
参考代码:
char buff[1024] = {0};//抓来的字符串就放这里;
int getInput(){
memset(buff,0x00,1024);//每次调用都要清空一次
printf("[minishell@coolsuperman]$");//为了和Linux的shell长得一样,所以每次都要先打印这个界面;
fflush(stdout);
if(scanf("%[^\n]%*c",buff)!=1){//如果只是\n scanf阻塞
getchar();
return -1;
}
return 0;
}
- 在这里我们需要考虑一个特殊情况,当用户什么都没做只输入了回车(\n)时,scanf会一直无法通过%[^\n],也就无法执行%*c的操作,\n将一直存在,所以我们要在此种情况下,手动使用getchar()扔掉\n;同时再次为情况下,用户不进行任何操作,我们需要返回一个异常值,方便后面处理;
分析字符串
- 在得到字符串后,我们需要将这些指定程序,路径,附加操作符都分别存储,(程序替换要用)在这之中,还隐藏了诸多如空格,制表符等等的"空白符号",这些符号是是我们所不需要但真实存在的,所以也得识别并剔除出去;
- 指定程序,路径等参数也是通过”空白字符“来间隔的,我们通过这里下手,使用ctype.h里的isspace()来识别它们,将有效字符后的空白字符置为‘\0’这样便将它们分成了一个个更短的有结尾的字符串
- 因为我已经将他们分成了很多个字符串,所以我使用字符串指针数组argv[]来存储他们,并记录这些字符串的个数argc(也是替换时的重要参数)
参考代码:
int argc = 0;
char *argv[32];
int AnalysisData(){
argc = 0;//每次清空初始化
char* go = buff;
while(*go!='\0'){
if(!isspace(*go)){
argv[argc] = go;
argc++;
while((*go!='\0')&&(!isspace(*go))){
go++;
}
*go = '\0';
}
go++;
}
argv[argc]=NULL;//告诉程序替换函数参数已经完了,详见程序替换函数原型需求
return 0;
}
创建进程执行操作
- 当上述两个动作都正常完成时,我们已经完成了准备工作,下面就要开始创建子进程,并进行程序替换了;
- 在创建之前,我们需要对一些需要shell自行调用接口的命令如"cd",进行判断,在这里,我只实现了cd,但基本形式便是如此,如果要实现更多,可以封装一个函数进行处理;
- 创建子进程时,如果创建失败,我们需要返回一下错误让用户知道原因方便调试,如果创建正常,我们便进入子进程操作;
- 在子进程上,也有一些特殊命令">,>>,…"是系统命令里没有的,我们需要自行实现,之后便可以使用之前储存好的参数进行程序替换了,(如果这里你不太清楚,可以看我的另一篇博文:Linux:程序替换)
- 如果替换失败,别忘了友好的返回错误信息,这事也要关闭掉这个没用的子进程(exit(-1))当成功替换时,要运行的程序已经变化了,也就不会运行到这里啦!?
- 子进程的事情忙完了,但shell作为父进程,一定要进行进程等待,否则子进程退出时shell在忙其他的事不太关注,就会产生僵尸进程!
int main(){
while(1){
if(getInput()<0)
continue;
AnalysisData();
if(!strcmp(argv[0],"cd")){
chdir(argv[1]);
continue;
}
int pid = fork();
if(pid<0){
perror("fork error");
return -1;
}else if(pid==0){//返回0代表子进程
int i = 0;
for(;i<argc;i++){
if(!strcmp(argv[i],">")){
int fd = open(argv[i+1],O_WRONLY|O_CREAT|O_TRUNC);//只写,没有创新,清空再写入
dup2(fd,1);
argv[i] = NULL;
}else if(!strcmp(argv[i],">>")){
int fd = open(argv[i+1],O_WRONLY|O_CREAT|O_APPEND);//只写,没有创新,在末尾续;
dup2(fd,1);
argv[i] = NULL;
}
}
execvp(argv[0],argv);//在PATH下找命令(argv[0]);
perror("execvp error");
exit(-1);
}
wait(NULL); //阻塞式等待,防止僵尸进程;
}
return 0;
}
需要用到的库
- 这里是我当前环境使用这些自带函数需要用到的库,但我发现有的情况下,不同系统,配置,版本,要用的库文件不尽相同,如果在Linux下你引用库文件出现了问题,不妨去问问那个男人吧!(man+你要问的函数名)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>
#include<ctype.h>
效果展示:
- 我的完整代码放在github上,有需要的小伙伴可以
点击下载
yo~
yo~
如果觉得不错~
请别忘了点个赞~
Bro你的鼓励~
给我坚持的勇气~
Peace out~