Shell
对Linux不是太陌生的读者都应该对Shell有一定的了解,就是这个程序在我们登陆后自动执行,打印出一个$符号,然后等待我们输入命令。Linux下最常用的Shell应用程序是Bash,绝大部分Linux发行版默认安装的都是它。下面我们也来亲手编写一个Shell程序,这个Shell远远不如Bash复杂,但也能满足我们一般的使用,下面,我们就开始。
首先,给这个Shell取一个名字,不妨就叫做Mini Shell。
Linux系统的命令分为内部命令和外部命令两种,内部命令由Shell程序实现,如cd、echo等,Linux的内部命令数量有限,而且绝大部分都很少用到。而每一个Linux外部命令都是一个单独的应用程序,我们非常熟悉的ls、cp等绝大多数命令都是外部命令,这些命令都以可执行文件的形式存在,绝大部分放在目录/bin和/sbin中。这样一来,我们编程的难度就可以大大下降了,我们只需要实现很有限的内部命令,对于其它的输入,统统当作应用程序来执行即可。
为了简单明了起见,Mini Shell只实现了2个内部命令:
1、cd 用于切换目录,和我们熟悉的命令cd类似,除了没有那么多的附加功能。
2、quit 用于退出Mini Shell。
下面是程序清单:
1: /* mshell.c */ 2: #include <sys/types.h> 1: #include <unistd.h> 3: #include <sys/wait.h> 4: #include <string.h> 5: #include <errno.h> 6: #include <stdio.h> 7: 9: void do_cd(char *argv[]); 10: void execute_new(char *argv[]); 11: 12: main() 13: { 14: char *cmd=(void *)malloc(256*sizeof(char)); 15: char *cmd_arg[10]; 16: int cmdlen,i,j,tag; 17: 18: do{ 19: /* 初始化cmd */ 20: for(i=0;i<255;i++) cmd[i]='\0'; 21: 22: printf("-=Mini Shell=-*| "); 23: fgets(cmd,256,stdin); 24: 25: cmdlen=strlen(cmd); 26: cmdlen--; 27: cmd[cmdlen]='\0'; 28: 29: /* 把命令行分解为指针数组cmd_arg */ 30: for(i=0;i<10;i++) cmd_arg[i]=NULL; 31: i=0; j=0; tag=0; 32: while(i<cmdlen && j<10){ 33: if(cmd[i]==' '){ 34: cmd[i]='\0'; 35: tag=0; 36: }else{ 37: if(tag==0) 38: cmd_arg[j++]=cmd+i; 39: tag=1; 40: } 41: i++; 42: } 43: 44: /* 如果参数超过10个,就打印错误,并忽略当前输入 */ 45: if(j>=10 && i<cmdlen){ 46: printf("TOO MANY ARGUMENTS\n"); 47: continue; 48: } 49: 50: /* 命令quit:退出Mini Shell */ 51: if(strcmp(cmd_arg[0],"quit")==0) 52: break; 53: 54: /* 命令cd */ 55: if(strcmp(cmd_arg[0],"cd")==0){ 56: do_cd(cmd_arg); 57: continue; 58: } 59: 60: /* 外部命令或应用程序 */ 61: execute_new(cmd_arg); 62: }while(1); 63: } 64: 65: /* 实现cd的功能 */ 66: void do_cd(char *argv[]) 67: { 68: if(argv[1]!=NULL){ 69: if(chdir(argv[1])<0) 70: switch(errno){ 71: case ENOENT: 72: printf("DIRECTORY NOT FOUND\n"); 73: break; 74: case ENOTDIR: 75: printf("NOT A DIRECTORY NAME\n"); 76: break; 77: case EACCES: 78: printf("YOU DO NOT HAVE RIGHT TO ACCESS\n"); 79: break; 80: default: 81: printf("SOME ERROR HAPPENED IN CHDIR\n"); 82: } 83: } 84: 85: } 86: 87: /* 执行外部命令或应用程序 */ 88: void execute_new(char *argv[]) 89: { 90: pid_t pid; 91: 92: pid=fork(); 93: if(pid<0){ 94: printf("SOME ERROR HAPPENED IN FORK\n"); 95: exit(2); 96: }else if(pid==0){ 97: if(execvp(argv[0],argv)<0) 98: switch(errno){ 99: case ENOENT: 100: printf("COMMAND OR FILENAME NOT FOUND\n"); 101: break; 102: case EACCES: 103: printf("YOU DO NOT HAVE RIGHT TO ACCESS\n"); 104: break; 105: default: 106: printf("SOME ERROR HAPPENED IN EXEC\n"); 107: } 108: exit(3); 109: }else 110: wait(NULL); 111: }
这个程序稍稍有点长,我们来对它作一下详细的解释:
函数main:
14行:定义字符串cmd,用于接收用户输入的命令行。
15行:定义指针数组cmd_arg,它的形式和作用都与我们熟悉的char *argv[]一样。
从以上2个定义可以看出Mini Shell对命令输入的2个限制:首先,用户输入的命令行必须在255个字符之内(除去字符串结束标志'\0');其次,命令行的参数个数不得超过10个(包括命令本身在内)。
18行:进入一个do-while循环,这个循环是本程序的主体部分,基本思想是"等待输入命令--处理已输入命令--等待输入命令"。
22行:打印输入提示信息。在Mini Shell中,你可以随意定自己喜欢的命令输入提示信息,本程序中使用了"-=Mini Shell=-*| ",是不是有点像一个CS高手?如果不喜欢,你可以用任意的字符替换它。
23行:接收用户输入。
25-27行:fgets接受输入时,会将输入字符串时末尾的换行符("\n")一起接受,这是我们不需要的,所以要把它去掉。本程序中简单的用字符串结束标志'\0'覆盖了字符串cmd的最后一个字符来实现这个目的。
30行:初始化指针数组cmd_arg。
32-42行:对输入进行分析,将cmd中参数间的空格用'\0'填充,并把各参数的起始地址分别赋与cmd_arg数组。这样就把cmd分解成了cmd_arg,但分解后的各命令参数仍然使用着cmd的内存空间,所以在命令执行结束前不宜对cmd另外赋值。
45行:如果还未分析到输入字符串的末尾(i<cmdlen),而分析出的参数已经达到或超过了10个(j>=10),就认为输入的命令行超出了10个参数的限制,打印错误并重新接收命令。
51-52行:内部命令quit:字符串cmd_arg[0]就是命令本身,如果命令是quit,则退出循环,也就等于退出该程序。
55-58行:内部命令cd:调用函数do_cd()完成cd命令的动作。
61行:对于其它的外部命令和应用程序,调用函数execute_new()执行。
函数do_cd:
68行:仅仅考虑紧跟在命令后面的参数argv[1],而不再考虑其它的参数。如果这个参数存在,就把它作为要转换的目录。
69行:调用系统调用chdir切换当前目录,参见附录1。
70-82行:对chdir可能出现的错误进行处理。
函数execute_new:
92行:调用系统调用fork产生新的子进程。
93行:如果返回负值,说明fork调用出现错误。
96行:如果返回0,说明当前进程是子进程。
97行:调用execvp执行新的应用程序,并检测调用是否出错(返回负值)。这里使用execvp的原因是它可以自动在各默认目录里寻找目标应用程序的位置,而不必我们自己编程实现。
98-107行:对execvp可能出现的错误进程处理。
108行:如果execvp的执行出现错误,子进程在这里终止。表面上看起来,这个exit是接着97行的错误判断的下一行语句,而非if语句的一部分,似乎无论调用execvp成功与否都会接着执行exit。但事实上,如果execvp调用成功的话,这个进程将会被新的程序代码填充,因而根本不可能执行到这一行。反之,如果执行到了这一行,说明前面的execvp调用一定出现了错误。这样的效果和exit被包含在if语句中的效果是完全一样的。
109行:如果fork返回其它值,说明当前进程是父进程。
110行:调用系统调用wait。wait在这里有两个作用:
- 使父进程在此暂停,等待子进程执行完毕。这样,就可以等子进程的所有信息全部输出完毕后才打印命令提示符,等待下一条命令的输入,从而避免了命令提示符和应用程序输出混杂在一起的现象。
- 收集子进程退出后留下的僵尸进程。可能有读者一直对这个问题存有疑问--"我们编程生成的子进程由我们自己设计的父进程负责收集,但我们手动执行的这个父进程由谁收集呢?"现在大家应该明白了,我们从命令行执行的所有进程最后都是由shell收集的。
关于Mini Shell的编译和运行,这里就不再敷述了,有兴趣的读者可以自行动手实验,或者对这个程序进行改进,使之更接近甚至超过我们正使用的Bash。