在前几个月对Linux的学习过程中,一直在与shell进行交互,感觉shell充满了神秘感。偶然看到一篇文章讲解了shell的实现,感觉也不是很难的样子,于是自己也开始开发自己的minishell,顺便也巩固了前一段时间学习的linux系统编程的知识。
先来展示一下我的这个minishell实现的功能:
1. 支持ls,touch,wc 等外部命令
2. 支持输入输出重定向符
3. 支持管道命令
4 .支持后台作业
5. 支持cd,jobs,kill,exit等内部命令(自己还写了一个about 命令 ^ _ ^)
6. 支持对ctrl+c 和ctrl +z 信号的处理
接下来我们按照编写的步骤一一来分析:
(一)命令的解析
输入命令的解析在本程序中占到了很大的比重,虽然像这种解析普通命令的程序(正则表达式太难了。。)的解释器难度不大,但是健壮性和全面性还是需要周全考虑的。
这里采用了分段解析,先除去起始空格,制表符等,并以此和一些‘|’,‘<’为分割界限来解析命令至COMMAND结构体。直接看代码吧,注释很详细!
/* * 解析命令 * 成功返回解析到的命令个数,失败返回-1 */ int parse_command(void) { /* cat < test.txt | grep -n public > test2.txt & */ if (check("\n")) return 0; /* 判断是否内部命令并执行它 */ if (builtin()) return 0; /* 1、解析第一条简单命令 */ get_command(0); /* 2、判定是否有输入重定向符 */ if (check("<")) getname(infile); /* 3、判定是否有管道 */ int i; for (i=1; i<PIPELINE; ++i) { if (check("|")) get_command(i); else break; } /* 4、判定是否有输出重定向符 */ if (check(">")) { if (check(">")) append = 1; getname(outfile); } /* 5、判定是否后台作业 */ if (check("&")) backgnd = 1; /* 6、判定命令结束‘\n’*/ if (check("\n")) { cmd_count = i; return cmd_count; } else { fprintf(stderr, "Command line syntax error\n"); return -1; } } /* * 解析简单命令至cmd[i] * 提取cmdline中的命令参数到avline数组中, * 并且将COMMAND结构中的args[]中的每个指针指向这些字符串 */ void get_command(int i) { /* cat < test.txt | grep -n public > test2.txt & */ int j = 0; int inword; while (*lineptr != '\0') { /* 去除空格 */ while (*lineptr == ' ' || *lineptr == '\t') lineptr++; /* 将第i条命令第j个参数指向avptr */ cmd[i].args[j] = avptr; /* 提取参数 */ while (*lineptr != '\0' && *lineptr != ' ' && *lineptr != '\t' && *lineptr != '>' && *lineptr != '<' && *lineptr != '|' && *lineptr != '&' && *lineptr != '\n') { /* 参数提取至avptr指针所向的数组avline */ *avptr++ = *lineptr++; inword = 1; } *avptr++ = '\0'; switch (*lineptr) { case ' ': case '\t': inword = 0; j++; break; case '<': case '>': case '|': case '&': case '\n': if (inword == 0) cmd[i].args[j] = NULL; return; default: /* for '\0' */ return; } } } /* * 将lineptr中的字符串与str进行匹配 * 成功返回1,lineptr移过所匹配的字符串 * 失败返回0,lineptr保持不变 */ int check(const char *str) { char *p; while (*lineptr == ' ' || *lineptr == '\t') lineptr++; p = lineptr; while (*str != '\0' && *str == *p) { str++; p++; } if (*str == '\0') { lineptr = p; /* lineptr移过所匹配的字符串 */ return 1; } /* lineptr保持不变 */ return 0; } void getname(char *name) { while (*lineptr == ' ' || *lineptr == '\t') lineptr++; while (*lineptr != '\0' && *lineptr != ' ' && *lineptr != '\t' && *lineptr != '>' && *lineptr != '<' && *lineptr != '|' && *lineptr != '&' && *lineptr != '\n') { *name++ = *lineptr++; } *name = '\0'; }
(二)命令的执行和实现
1、程序框架:
在对命令的解析完毕后,我们先考虑两个大的方向,即是外部命令还是内部命令?
外部命令的话,我们只需要fork一个子进程,用execvp()来执行就可以了;对于内部命令则需要自己去实现。
提出两个问题:第一个,为什么要使用execvp() ?第二个,为什么要fork一个子进程来实现,直接while循环不可以吗?
解答:
(1)我们之所以使用execvp(),是因为函数的原型是 int execvp(const char *file ,char * const argv []); 第一个参数是命令文件名,第二个是参数,执行命令非 常的方便。
(2)一旦执行execvp(),当前进程就会被execvp的进程所替代,执行完后就会结束程序,所以while循环是不可以的,必须要fork一个子进程来执行。
while(1) { /* repeat forever */ type_prompt(); /* display prompt on the screen */ read_command(command,parameters); /* read input from terminal */ if(fork()!=0) { /* fork off child process */ /* Parent code */ waitpid(-1,&status,0); /* wait for child to exit */ } else { /* Child code */ execvp(command,parameters); /* execute command */ } }
利用这个框架,外部命令(可执行文件)的功能基本实现(vi ,top ,ps等均可使用)。
2、输入输出重定向
当分析出来有输入输出重定向的符号时,我们要使用dup()函数来实现。函数详解请参考 我的博客
对于输入的句法分析结果,我们使用一个结构体来保存:
typedef struct command { char *args[MAXARG+1]; /* 解析出的命令参数列表 */ int infd; int outfd; } COMMAND;基本流程:
/* 子进程 */ if (cmd[i].infd != 0) { close(0); dup(cmd[i].infd); } if (cmd[i].outfd != 1) { close(1); dup(cmd[i].outfd); } int j; for (j=3; j<OPEN_MAX; ++j) close(j);其中cmd[i].infd和cmd[i].outfd是解析出来的重定向位置的全局变量。
3、管道命令
管道命令是使用pipe()函数实现的。关于管道的详解请参考 我的博客
假如我们有 a | b | c 这样一个形式的命令,那么是需要创建两条管道的,依次类推。
int i; int fd; int fds[2]; for (i=0; i<cmd_count; ++i) { /* 如果不是最后一条命令,则需要创建管道 */ if (i<cmd_count-1) { pipe(fds); cmd[i].outfd = fds[1]; cmd[i+1].infd = fds[0]; } forkexec(i); if ((fd = cmd[i].infd) != 0) close(fd); if ((fd = cmd[i].outfd) != 1) close(fd); } if (backgnd == 0) { /* 前台作业,需要等待管道中最后一个命令退出 */ while (wait(NULL) != lastpid) ; }4.后台作业和信号处理
判断后台,我们只需要解析命令看是否存在 “&”,若存在则backgnd = 1,不再对后台进程进行wait。为了避免僵尸进程,我们可是选择使用signal()处理SIGCHLD,将其忽略,同时忽略SIGINT和SIGQUIT信号(后台不响应ctrl+c,ctrl+z)。但是注意backgnd=0的时候要将这两个信号再设置成默认处理,否则前台也不能响应信号了。
1、 cd命令的实现
cd命令的实现主要依赖于系统调用chdir()。我们通过将第一个参数传入chdir就可以进行一次成功的cd调用。通过判断chdir()不同的返回值可以判断出更改目录成功与否,并能输出错误原因。
void do_cd(void) { get_command(0); int fd; fd=open(*(cmd[0].args),O_RDONLY); fchdir(fd); close(fd); }
2、 jobs命令的实现
jobs命令我们维护一个链表,每次当有一个后台进程运行的时候,都要向这个链表中添加一个数据。并当子进程结束的时候会向父进程发送SIGCHLD信号,父进程也就是Shell要处理这个信号,并且将后台进程链表中相应的进程进行处理,也就是将其移除。
/* 父进程 */ if (backgnd == 1) { /*添加入jobs的链表*/ NODE *p=(NODE*)malloc(sizeof(NODE)); p->npid=pid; printf("%s",cmd[0].args[0]); strcpy(p->backcn,cmd[0].args[0]); // printf("%s",p->backcn); NODE* tmp=head->next; head->next=p; p->next=tmp; }
void do_exit(void) { int Pgnum=0; NODE* tmp=head->next; while(tmp!=NULL) { Pgnum++; tmp=tmp->next; } if(Pgnum!=0) { printf("There are programs in the background,are you sure to exit?y/N\n"); char c= getchar(); if(c=='N') return ; else goto loop; } loop: printf("exit\n"); exit(EXIT_SUCCESS); }4、 kill命令的实现
void do_kill(void) { get_command(0); int num=atoi(cmd[0].args[1]); signal(SIGQUIT,SIG_DFL); kill(num,SIGQUIT); signal(SIGQUIT,SIG_IGN); NODE *bng=head->next; NODE *pre=head; while(bng!=NULL) { if(bng->npid==num) { NODE* nxt=bng->next; pre->next=nxt; break; } pre=bng; bng=bng->next; } }
注:本程序的具体源码托管至 Github ,欢迎大家关注!
然而依然存在一些不足之处:
1.因为时间和测试不足的关系,肯定存在着bug
2.没能支持正则表达式等复杂的命令解析
3.不能执行shell脚本。
4.没有实现上下键查看历史命令的功能。
总的来说,自己收获很大,也希望可以帮助到大家!