自主实现一个shell

我实现的shell具有以下功能:

1. 支持ls,touch,wc 等外部命令

2. 支持输入输出重定向符

3. 支持管道命令

4 .支持后台作业

5. 支持cd,jobs,kill,exit等内部命令(自己还写了一个about 命令 ^ _ ^)

6. 支持对ctrl+c 和ctrl +z 信号的处理


接下来我们按照编写的步骤一一来分析:

(一)命令的解析

输入命令的解析在本程序中占到了很大的比重,虽然像这种解析普通命令的程序(正则表达式太难了。。)的解释器难度不大,但是健壮性和全面性还是需要周全考虑的。
这里采用了分段解析,先除去起始空格,制表符等,并以此和一些‘|’,‘<’为分割界限来解析命令至COMMAND结构体。直接看代码吧,注释很详细!

  1. /* 
  2.  * 解析命令 
  3.  * 成功返回解析到的命令个数,失败返回-1 
  4.  */  
  5. int parse_command(void)  
  6. {  
  7.     /* cat < test.txt | grep -n public > test2.txt & */  
  8.     if (check("\n"))  
  9.         return 0;  
  10.   
  11.     /* 判断是否内部命令并执行它 */  
  12.     if (builtin())  
  13.         return 0;  
  14.   
  15.   
  16.     /* 1、解析第一条简单命令 */  
  17.          
  18.     get_command(0);  
  19.     /* 2、判定是否有输入重定向符 */  
  20.     if (check("<"))  
  21.         getname(infile);  
  22.     /* 3、判定是否有管道 */  
  23.     int i;  
  24.     for (i=1; i<PIPELINE; ++i)  
  25.     {  
  26.         if (check("|"))  
  27.             get_command(i);  
  28.         else  
  29.             break;  
  30.     }  
  31.     /* 4、判定是否有输出重定向符 */  
  32.     if (check(">"))  
  33.     {  
  34.         if (check(">"))  
  35.             append = 1;  
  36.         getname(outfile);  
  37.     }  
  38.     /* 5、判定是否后台作业 */  
  39.     if (check("&"))  
  40.         backgnd = 1;  
  41.     /* 6、判定命令结束‘\n’*/  
  42.     if (check("\n"))  
  43.     {  
  44.         cmd_count = i;  
  45.         return cmd_count;  
  46.     }  
  47.     else  
  48.     {  
  49.         fprintf(stderr, "Command line syntax error\n");  
  50.         return -1;  
  51.     }  
  52. }  
  53.   
  54. /* 
  55.  * 解析简单命令至cmd[i] 
  56.  * 提取cmdline中的命令参数到avline数组中, 
  57.  * 并且将COMMAND结构中的args[]中的每个指针指向这些字符串 
  58.  */  
  59. void get_command(int i)  
  60. {  
  61.     /*   cat < test.txt | grep -n public > test2.txt & */  
  62.   
  63.     int j = 0;  
  64.     int inword;  
  65.     while (*lineptr != '\0')  
  66.     {  
  67.         /* 去除空格 */  
  68.         while (*lineptr == ' ' || *lineptr == '\t')  
  69.             lineptr++;  
  70.   
  71.         /* 将第i条命令第j个参数指向avptr */  
  72.         cmd[i].args[j] = avptr;  
  73.         /* 提取参数 */  
  74.         while (*lineptr != '\0'  
  75.             && *lineptr != ' '  
  76.             && *lineptr != '\t'  
  77.             && *lineptr != '>'  
  78.             && *lineptr != '<'  
  79.             && *lineptr != '|'  
  80.             && *lineptr != '&'  
  81.             && *lineptr != '\n')  
  82.         {  
  83.                 /* 参数提取至avptr指针所向的数组avline */  
  84.                 *avptr++ = *lineptr++;  
  85.                 inword = 1;  
  86.         }  
  87.         *avptr++ = '\0';  
  88.         switch (*lineptr)  
  89.         {  
  90.         case ' ':  
  91.         case '\t':  
  92.             inword = 0;  
  93.             j++;  
  94.             break;  
  95.         case '<':  
  96.         case '>':  
  97.         case '|':  
  98.         case '&':  
  99.         case '\n':  
  100.             if (inword == 0)  
  101.                 cmd[i].args[j] = NULL;  
  102.             return;  
  103.         default/* for '\0' */  
  104.             return;  
  105.         }  
  106.     }  
  107. }  
  108.   
  109. /* 
  110.  * 将lineptr中的字符串与str进行匹配 
  111.  * 成功返回1,lineptr移过所匹配的字符串 
  112.  * 失败返回0,lineptr保持不变 
  113.  */  
  114. int check(const char *str)  
  115. {  
  116.     char *p;  
  117.     while (*lineptr == ' ' || *lineptr == '\t')  
  118.         lineptr++;  
  119.   
  120.     p = lineptr;  
  121.     while (*str != '\0' && *str == *p)  
  122.     {  
  123.         str++;  
  124.         p++;  
  125.     }  
  126.   
  127.     if (*str == '\0')  
  128.     {  
  129.         lineptr = p;    /* lineptr移过所匹配的字符串 */  
  130.         return 1;  
  131.     }  
  132.   
  133.     /* lineptr保持不变 */  
  134.     return 0;  
  135. }  
  136.   
  137. void getname(char *name)  
  138. {  
  139.     while (*lineptr == ' ' || *lineptr == '\t')  
  140.         lineptr++;  
  141.   
  142.     while (*lineptr != '\0'  
  143.             && *lineptr != ' '  
  144.             && *lineptr != '\t'  
  145.             && *lineptr != '>'  
  146.             && *lineptr != '<'  
  147.             && *lineptr != '|'  
  148.             && *lineptr != '&'  
  149.             && *lineptr != '\n')  
  150.     {  
  151.             *name++ = *lineptr++;  
  152.     }  
  153.     *name = '\0';  
  154. }  

(二)命令的执行和实现

  1、程序框架:

   在对命令的解析完毕后,我们先考虑两个大的方向,即是外部命令还是内部命令?

   外部命令的话,我们只需要fork一个子进程,用execvp()来执行就可以了;对于内部命令则需要自己去实现。

   提出两个问题:第一个,为什么要使用execvp() ?第二个,为什么要fork一个子进程来实现,直接while循环不可以吗?

  解答:

 (1)我们之所以使用execvp(),是因为函数的原型是 int execvp(const char *file ,char * const argv []); 第一个参数是命令文件名,第二个是参数,执行命令非 常的方便。

(2)一旦执行execvp(),当前进程就会被execvp的进程所替代,执行完后就会结束程序,所以while循环是不可以的,必须要fork一个子进程来执行。

  1. while(1) {                  /* repeat forever */  
  2.   type_prompt();             /* display prompt on the screen */  
  3.   read_command(command,parameters);    /* read input from terminal */  
  4.   if(fork()!=0) {               /* fork off child process */  
  5.     /* Parent code */  
  6.     waitpid(-1,&status,0);         /* wait for child to exit */  
  7.    } else {  
  8.     /* Child code */  
  9.     execvp(command,parameters);    /* execute command */  
  10.   }  
  11.   
  12. }  

利用这个框架,外部命令(可执行文件)的功能基本实现(vi ,top ,ps等均可使用)。


2、输入输出重定向

当分析出来有输入输出重定向的符号时,我们要使用dup()函数来实现。函数详解请参考我的博客

对于输入的句法分析结果,我们使用一个结构体来保存:

  1. typedef struct command  
  2. {  
  3.     char *args[MAXARG+1];   /* 解析出的命令参数列表 */  
  4.     int infd;  
  5.     int outfd;  
  6. } COMMAND;  
基本流程:

  1. /* 子进程 */  
  2.     if (cmd[i].infd != 0)  
  3.     {  
  4.         close(0);  
  5.         dup(cmd[i].infd);  
  6.     }  
  7.     if (cmd[i].outfd != 1)  
  8.     {  
  9.         close(1);  
  10.         dup(cmd[i].outfd);  
  11.     }  
  12.                   
  13.            
  14.     int j;  
  15.     for (j=3; j<OPEN_MAX; ++j)  
  16.         close(j);  
其中cmd[i].infd和cmd[i].outfd是解析出来的重定向位置的全局变量。


3、管道命令

 管道命令是使用pipe()函数实现的。关于管道的详解请参考 我的博客

假如我们有  a | b | c 这样一个形式的命令,那么是需要创建两条管道的,依次类推。

  1. int i;  
  2. int fd;  
  3. int fds[2];  
  4. for (i=0; i<cmd_count; ++i)  
  5. {  
  6.     /* 如果不是最后一条命令,则需要创建管道 */  
  7.     if (i<cmd_count-1)  
  8.     {  
  9.         pipe(fds);  
  10.         cmd[i].outfd = fds[1];  
  11.         cmd[i+1].infd = fds[0];  
  12.     }  
  13.                  
  14.     forkexec(i);  
  15.   
  16.     if ((fd = cmd[i].infd) != 0)  
  17.         close(fd);  
  18.   
  19.     if ((fd = cmd[i].outfd) != 1)  
  20.         close(fd);  
  21. }  
  22.   
  23. if (backgnd == 0)  
  24. {  
  25.     /* 前台作业,需要等待管道中最后一个命令退出 */  
  26.     while (wait(NULL) != lastpid)  
  27.         ;  
  28. }  


4.后台作业和信号处理

判断后台,我们只需要解析命令看是否存在 “&”,若存在则backgnd = 1,不再对后台进程进行wait。为了避免僵尸进程,我们可是选择使用signal()处理SIGCHLD,将其忽略,同时忽略SIGINT和SIGQUIT信号(后台不响应ctrl+c,ctrl+z)。但是注意backgnd=0的时候要将这两个信号再设置成默认处理,否则前台也不能响应信号了。


5.内部命令

1、 cd命令的实现 
cd命令的实现主要依赖于系统调用chdir()。我们通过将第一个参数传入chdir就可以进行一次成功的cd调用。通过判断chdir()不同的返回值可以判断出更改目录成功与否,并能输出错误原因。

  1. void do_cd(void)  
  2. {  
  3.     get_command(0);  
  4.     int fd;  
  5.     fd=open(*(cmd[0].args),O_RDONLY);  
  6.     fchdir(fd);  
  7.     close(fd);  
  8. }  
 

2、 jobs命令的实现 
jobs命令我们维护一个链表,每次当有一个后台进程运行的时候,都要向这个链表中添加一个数据。并当子进程结束的时候会向父进程发送SIGCHLD信号,父进程也就是Shell要处理这个信号,并且将后台进程链表中相应的进程进行处理,也就是将其移除。

  1. /* 父进程 */  
  2.     if (backgnd == 1)  
  3.        {  
  4.           /*添加入jobs的链表*/  
  5.               NODE *p=(NODE*)malloc(sizeof(NODE));  
  6.               p->npid=pid;  
  7.               printf("%s",cmd[0].args[0]);  
  8.               strcpy(p->backcn,cmd[0].args[0]);  
  9.               // printf("%s",p->backcn);  
  10.   
  11.               NODE* tmp=head->next;  
  12.               head->next=p;  
  13.               p->next=tmp;  
  14.        }  

 3、 exit命令的实现 
exit命令分两部分实现。第一,当词法分析到exit的时候直接调用系统调用exit()就可以了。第二,退出之前要判断一下后台进程链表中是否还有未执行完的任务,如果有未执行完的任务,要提示用户,等待用户选择。

  1. void do_exit(void)  
  2. {  
  3.     int Pgnum=0;  
  4.     NODE* tmp=head->next;  
  5.     while(tmp!=NULL)  
  6.     {  
  7.         Pgnum++;  
  8.         tmp=tmp->next;  
  9.     }  
  10.     if(Pgnum!=0)  
  11.     {  
  12.        printf("There are programs in the background,are you sure to exit?y/N\n");  
  13.        char c= getchar();  
  14.        if(c=='N')  
  15.            return ;  
  16.        else  
  17.            goto loop;  
  18.     }  
  19.    loop:  
  20.     printf("exit\n");  
  21.     exit(EXIT_SUCCESS);  
  22. }  
4、 kill命令的实现 
kill命令的实现是通过信号来实现的,我们使用kill -9 +pid来强制结束后台进程,用kill系统调用向相应的进程发送SIGQUIT信号来使进程强制退出。

  1. void do_kill(void)  
  2. {  
  3.     get_command(0);  
  4.     int num=atoi(cmd[0].args[1]);  
  5.     signal(SIGQUIT,SIG_DFL);  
  6.     kill(num,SIGQUIT);  
  7.     signal(SIGQUIT,SIG_IGN);  
  8.     NODE *bng=head->next;  
  9.     NODE *pre=head;  
  10.     while(bng!=NULL)  
  11.     {  
  12.         if(bng->npid==num)  
  13.         {  
  14.             NODE* nxt=bng->next;  
  15.             pre->next=nxt;  
  16.             break;  
  17.         }  
  18.         pre=bng;  
  19.         bng=bng->next;  
  20.     }  
  21. }  

到这里,本程序的功能已经基本实现,效果还算不错。  

注:本程序的具体源码托管至Github   ,欢迎大家关注!

然而依然存在一些不足之处:

1.因为时间和测试不足的关系,肯定存在着bug

2.没能支持正则表达式等复杂的命令解析

3.不能执行shell脚本。

4.没有实现上下键查看历史命令的功能。

猜你喜欢

转载自blog.csdn.net/zy20150613/article/details/79946979
今日推荐