Chinaunix首页 | 论坛 | 博客
  • 博客访问: 712520
  • 博文数量: 165
  • 博客积分: 8218
  • 博客等级: 中将
  • 技术积分: 1749
  • 用 户 组: 普通用户
  • 注册时间: 2008-09-07 19:48
文章分类

全部博文(165)

文章存档

2014年(4)

2011年(3)

2010年(6)

2009年(43)

2008年(109)

分类:

2009-05-03 21:42:56

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 
1:  #include 
3:  #include 
4:  #include 
5:  #include 
6:  #include 
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=10 && i

这个程序稍稍有点长,我们来对它作一下详细的解释:

函数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=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在这里有两个作用:

  1. 使父进程在此暂停,等待子进程执行完毕。这样,就可以等子进程的所有信息全部输出完毕后才打印命令提示符,等待下一条命令的输入,从而避免了命令提示符和应用程序输出混杂在一起的现象。
  2. 收集子进程退出后留下的僵尸进程。可能有读者一直对这个问题存有疑问--"我们编程生成的子进程由我们自己设计的父进程负责收集,但我们手动执行的这个父进程由谁收集呢?"现在大家应该明白了,我们从命令行执行的所有进程最后都是由shell收集的。
阅读(5949) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~