zoukankan      html  css  js  c++  java
  • Tinyshell: 一个简易的shell命令解释器

    这是自己最近学习Linux系统编程之后写的一个练手的小程序,能很好地复习系统编程中的进程管理、信号、管道、文件等内容。

    通过回顾写的过程中遇到的问题的形式记录程序的关键点,最后给出完整程序代码。

    0. Tinyshell的功能

    这个简易的shell解释器可以解析磁盘命令,支持管道和输入输出重定向,内置命令只实现了exit,可以判定后台执行命令(&),但未实现bg功能(后台命令直接返回)。

    1. shell是如何运行程序的

    基本的模式就是主进程从键盘获取命令、解析命令,并fork出子进程执行相应的命令,最后主进程在子进程结束后回收(避免僵尸进程)。

    这里执行命令可以采用exec家族中的execvp

    int execvp(const char *file, char *constargv[]); 

    两个参数分别传递程序名(如ls)和命令行参数(如 -l)即可。

    2. 怎么解析命令?

    由于命令行解析要实现管道功能和I/O重定向功能,所以解析命令也稍微有些复杂。

    首先用cmdline读取完整的一行命令;

    avline解析命令,去除空格,不同字符串之间以间隔。

    定义一个COMMAND数据结构,包含一个字符串指针数组和infd,outfd两个文件描述符变量。

    typedef struct command
    {
        char *args[MAXARG+1];    /* 解析出的命令参数列表 */
        int infd;
        int outfd;
    } COMMAND;

    每个COMMAND存储一个指令,其中args中的每个指针指向解析好的命令行参数字符串,infd,outfd存这个命令的输入输出对应的文件描述符。

    COMMAND之间以< > |符号间隔,每个COMMAND中空格间隔出命令和不同的参数。大致结构如下图所示:(注:命令行处理方法和图片均学习自[2])

                                                                          

    3. 输入输出重定向怎么处理?

    理解I/O重定向首先要理解最低可用文件描述符的概念。即每个进程都有其打开的一组文件,这些打开的文件被保持在一个数组中,文件描述符即为某文件在此数组中的索引。

    所以当打开文件时,为文件安排的总是此数组中最低可用位置的索引

    同时stdin, stdout, stderr分别对应文件描述符0,1,2被打开。

    文件描述符集通过exec调用传递,不会被改变。

    所以shell可以通过fork产生子进程与子进程调用exec之间的时间间隔来重定向标准输入输出到文件。

    利用的函数是dup / dup2

    #include <unistd.h>
    int dup(int oldfd);
    int dup2(int oldfd, int newfd)

    以输入重定向为例。 open(file)  -> close(0) -> dup(fd) -> close(fd)

    open(file)打开将要重定向的文件,close(0)使得文件描述符0空闲,dup(fd)对fd进行复制,利用最低文件描述符0,此时该文件与文件描述符0连接在一起。

    close(fd)来关闭文件的原始连接,只留下文件描述符0的连接。 或直接利用dp2将文件描述符pld复制到文件描述符new(open -> dup2 -> close)

    同时利用append变量记录输出重定向是否是追加模式(“>>”)来决定打开文件的方式。

    4. 管道怎么处理?

    管道就是利用linux的管道创建函数并将管道的读写端分别绑定即可。

    #include <unistd.h>
    int pipe(int pipefd[2]);

    pipefd[0]为管道读端,pipefd[1]为管道写端。

    前后进程利用管道,采用如下逻辑:(以ls | wc为例)

    前一个进程(ls) : close(p[0]) -> dup2(p[1], 1) -> close(p[1]) -> exec(ls)

    后一个进程 (wc):close(p[1]) -> dup(p[0], 0) -> close(p[0])  -> exec(wc)

    注意,有N个COMMAND意味着要建立N-1个管道,所以可以用变量cmd_count记录命令个数。

        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);
        }  
    
    //forkexec中相关代码
            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<1024; ++j)
                close(j);                

    5.信号处理

    分析整个流程中不同阶段的信号处理问题。

    5.1 首先是shell主循环运行阶段,显然shell是不会被Ctrl + C停止的,所以初始化要忽略SIG_INT,SIGQUIT

    5.2 当前台运行其他程序时,是可以用Ctrl + C来终止程序运行的,所以这时要恢复SIG_INT, SIGQUIT。

    但注意Ctrl + C会导致内核向当前进程组的所有进程发送SIGINT信号。所以当fork出子进程处理前台命令时,应该让第一个简单命令作为进程组的组长。

    这样接收到信号时,不会对shell进程产生影响。

    设置进程组采用setpgid函数。setpgid(0,0)表示用当前进程的PID作为进程组ID。

    #include <unistd.h>
    int setpgid(pid_t pid, pid_t pgid);

    5.3 后台运行程序,不会调用wait等待子进程退出,所以采用linux下特有的处理方式,忽略SIGCHLD,避免僵尸进程(在linux下交给init处理)

    但在前台运行时需要再把SIGCHLD回复,显示等待子进程退出。

    if (backgnd == 1)
        signal(SIGCHLD, SIG_IGN);
    else
        signal(SIGCHLD, SIG_DFL);

    5.4 前台任务如何回收子进程? 在看到的参考中有的方案提到while循环只到回收到最后一个子进程为止。

    while (wait(NULL) != lastpid)
        ;

    但此方法应该有bug,fork出的子进程的顺序与子进程结束的顺序不一定相同,所以还是采用计数的方式,等待所以子进程被回收。

    int cnt = 0;
    while (wait(NULL) != -1 && cnt != cmd_count) {
        cnt++;
    }

    6.其他开始没注意到的小bug和补充

    6.1 cmd_count == 0时,不执行任何操作,直接返回。不加这一句判断会出错。

    6.2 因为没有实现bg功能,所以后台作业将第一条简单命令的infd重定向至/dev/null, 当第一条命令试图从标准输入获取数据的时候立即返回EOF。

    6.3 内置命令只实现了exit退出。

    7. 还有什么可以优化?

    7.1 这里主进程中采用的是阻塞等待回收子进程的策略,一个更好的方案应该是利用SIGCHLD信号来处理。但这里便存在很多容易出错的地方。

    比如子进程可能结束了,父进程还没有获得执行的机会,父进程再执行后再也收不到SIGCHLD信号。

    所以需要通过显示的阻塞SIGCHLD信号来对其进行同步(利用sigprocmask函数)

    其次,信号的接收是不排队的,所以对于同时到来的子进程结束信号,一些信号可能被丢弃。所以一个未处理的信号表明至少一个信号到达了。要小心处理。

    关于这部分内容,可以参考CSAPP【3】。

    7.2 可以考虑加入更多的内置命令,同时实现shell的流程控制和变量设置。

    8. 完整代码

    为了好在博客中上传,没有采取头文件形式,所有内容在一个.c文件中

      1 #include <stdlib.h>
      2 #include <stdio.h>
      3 #include <unistd.h>
      4 #include <sys/types.h>
      5 #include <sys/wait.h>
      6 #include <linux/limits.h>
      7 #include <fcntl.h>
      8 #include <signal.h>
      9 #include <string.h>
     10 
     11 
     12 #define MAXLINE 1024    /* 输入行的最大长度 */
     13 #define MAXARG 20        /* 每个简单命令的参数最多个数 */
     14 #define PIPELINE 5        /* 一个管道行中简单命令的最多个数 */
     15 #define MAXNAME 100        /* IO重定向文件名的最大长度 */
     16 
     17 
     18 typedef struct command
     19 {
     20     char *args[MAXARG+1];    /* 解析出的命令参数列表 */
     21     int infd;
     22     int outfd;
     23 } COMMAND;
     24 
     25 typedef void (*CMD_HANDLER)(void);  /*内置命令函数指针*/
     26 
     27 typedef struct builtin_cmd
     28 {
     29     char *name;
     30     CMD_HANDLER handler;
     31 
     32 } BUILTIN_CMD;
     33 
     34 void do_exit(void);
     35 void do_cd(void);
     36 void do_type(void);
     37 BUILTIN_CMD builtins[] = 
     38 {
     39     {"exit", do_exit},
     40     {"cd", do_cd},
     41     {"type", do_type},
     42     {NULL, NULL}
     43 };
     44 
     45 char cmdline[MAXLINE+1];        /*读到的一行命令*/
     46 char avline[MAXLINE+1];            /*解析过添加好的命令*/
     47 char *lineptr;
     48 char *avptr;
     49 char infile[MAXNAME+1];         /*输入重定向文件*/
     50 char outfile[MAXNAME+1];        /*输出重定向文件*/
     51 COMMAND cmd[PIPELINE];            /*解析好的命令数组*/
     52 
     53 int cmd_count;            /*有多少个命令*/
     54 int backgnd;           /*是否后台作业*/
     55 int append;                /*输出重定向是否是append模式*/
     56 int lastpid;            /*回收最后一个子进程的pid*/
     57 
     58 #define ERR_EXIT(m) 
     59   do 
     60   { 
     61     perror(m); 
     62     exit(EXIT_FAILURE); 
     63   } 
     64   while (0)
     65 
     66 
     67 void setup(void);
     68 void init(void);
     69 void shell_loop(void);
     70 int read_command(void);
     71 int parse_command(void);
     72 int execute_command(void);
     73 void forkexec(int i);
     74 int check(const char *str);
     75 int execute_disk_command(void);
     76 int builtin(void);
     77 void get_command(int i);
     78 void getname(char *name);
     79 
     80 
     81 int main()
     82 {
     83     /* 安装信号 */
     84     setup();
     85     /* 进入shell循环 */
     86     shell_loop();
     87     return 0;
     88 }
     89 
     90 
     91 void sigint_handler(int sig)
     92 {
     93     printf("
    [minishell]$ ");
     94     fflush(stdout);
     95 }
     96 
     97 
     98 void setup(void)
     99 {
    100     signal(SIGINT, sigint_handler);
    101     signal(SIGQUIT, SIG_IGN);
    102 }
    103 
    104 void init(void)
    105 {
    106     memset(cmd, 0, sizeof(cmd));
    107     int i;
    108     for (i=0; i<PIPELINE; ++i)
    109     {
    110         cmd[i].infd = 0;
    111         cmd[i].outfd = 1;
    112     }
    113     memset(cmdline, 0, sizeof(cmdline));
    114     memset(avline, 0, sizeof(avline));
    115     lineptr = cmdline;
    116     avptr = avline;
    117     memset(infile, 0, sizeof(infile));
    118     memset(outfile, 0, sizeof(outfile));
    119     cmd_count = 0;
    120     backgnd = 0;
    121     append = 0;
    122     lastpid = 0;
    123 
    124     printf("[minishell]$ ");
    125         fflush(stdout);
    126 }
    127 
    128 
    129 /**主循环**/
    130 void shell_loop(void)
    131 {
    132     while (1)
    133     {
    134         /* 初始化环境 */
    135         init();
    136         /* 获取命令 */
    137         if (read_command() == -1)
    138             break;
    139         /* 解析命令 */
    140         parse_command();
    141         /*print_command();*/
    142         /* 执行命令 */
    143         execute_command();
    144     }
    145     
    146     printf("
    exit
    ");
    147 }
    148 
    149 
    150 /*
    151  * 读取命令
    152  * 成功返回0,失败或者读取到文件结束符(EOF)返回-1
    153  */
    154 int read_command(void)
    155 {
    156     /* 按行读取命令,cmdline中包含
    字符 */
    157     if (fgets(cmdline, MAXLINE, stdin) == NULL)
    158         return -1;
    159     return 0;
    160 }
    161 
    162 /*
    163  * 解析命令
    164  * 成功返回解析到的命令个数,失败返回-1
    165  */
    166 int parse_command(void)
    167 {
    168     /* cat < test.txt | grep -n public > test2.txt & */
    169     if (check("
    "))
    170         return 0;
    171 
    172     /* 判断是否内部命令并执行它 */
    173     if (builtin())
    174         return 0;
    175 
    176 
    177     /* 1、解析第一条简单命令 */
    178     get_command(0);
    179     /* 2、判定是否有输入重定向符 */
    180     if (check("<"))
    181         getname(infile);
    182     /* 3、判定是否有管道 */
    183     int i;
    184     for (i=1; i<PIPELINE; ++i)
    185     {
    186         if (check("|"))
    187             get_command(i);
    188         else
    189             break;
    190     }
    191     /* 4、判定是否有输出重定向符 */
    192     if (check(">"))
    193     {
    194         if (check(">"))
    195             append = 1;
    196         getname(outfile);
    197     }
    198     /* 5、判定是否后台作业 */
    199     if (check("&"))
    200         backgnd = 1;
    201     /* 6、判定命令结束‘
    ’*/
    202     if (check("
    "))
    203     {
    204         cmd_count = i;
    205         return cmd_count;
    206     }
    207     else
    208     {
    209         fprintf(stderr, "Command line syntax error
    ");
    210         return -1;
    211     }
    212 }
    213 
    214 
    215 /*
    216  * 解析简单命令至cmd[i]
    217  * 提取cmdline中的命令参数到avline数组中,
    218  * 并且将COMMAND结构中的args[]中的每个指针指向这些字符串
    219  */
    220 void get_command(int i)
    221 {
    222     /*   cat < test.txt | grep -n public > test2.txt & */
    223 
    224     int j = 0;
    225     int inword;
    226     while (*lineptr != '')
    227     {
    228         /* 去除空格 */
    229         while (*lineptr == ' ' || *lineptr == '	')
    230             *lineptr++;
    231 
    232         /* 将第i条命令第j个参数指向avptr */
    233         cmd[i].args[j] = avptr;
    234         /* 提取参数 */
    235         while (*lineptr != ''
    236             && *lineptr != ' '
    237             && *lineptr != '	'
    238             && *lineptr != '>'
    239             && *lineptr != '<'
    240             && *lineptr != '|'
    241             && *lineptr != '&'
    242             && *lineptr != '
    ')
    243         {
    244                 /* 参数提取至avptr指针所向的数组avline */
    245                 *avptr++ = *lineptr++;
    246                 inword = 1;
    247         }
    248         *avptr++ = '';
    249         switch (*lineptr)
    250         {
    251         case ' ':
    252         case '	':
    253             inword = 0;
    254             j++;
    255             break;
    256         case '<':
    257         case '>':
    258         case '|':
    259         case '&':
    260         case '
    ':
    261             if (inword == 0)
    262                 cmd[i].args[j] = NULL;
    263             return;
    264         default: /* for '' */
    265             return;
    266         }
    267     }
    268 }
    269 
    270 /*
    271  * 将lineptr中的字符串与str进行匹配
    272  * 成功返回1,lineptr移过所匹配的字符串
    273  * 失败返回0,lineptr保持不变
    274  */
    275 int check(const char *str)
    276 {
    277     char *p;
    278     while (*lineptr == ' ' || *lineptr == '	')
    279         lineptr++;
    280 
    281     p = lineptr;
    282     while (*str != '' && *str == *p)
    283     {
    284         str++;
    285         p++;
    286     }
    287 
    288     if (*str == '')
    289     {
    290         lineptr = p;    /* lineptr移过所匹配的字符串 */
    291         return 1;
    292     }
    293 
    294     /* lineptr保持不变 */
    295     return 0;
    296 }
    297 
    298 void getname(char *name)
    299 {
    300     while (*lineptr == ' ' || *lineptr == '	')
    301         lineptr++;
    302 
    303     while (*lineptr != ''
    304             && *lineptr != ' '
    305             && *lineptr != '	'
    306             && *lineptr != '>'
    307             && *lineptr != '<'
    308             && *lineptr != '|'
    309             && *lineptr != '&'
    310             && *lineptr != '
    ')
    311     {
    312             *name++ = *lineptr++;
    313     }
    314     *name = '';
    315 }
    316 
    317 /*
    318  * 执行命令
    319  * 成功返回0,失败返回-1
    320  */
    321 int execute_command(void)
    322 {
    323     execute_disk_command();
    324     return 0;
    325 }
    326 
    327 /*执行命令 fork + exec */
    328 void forkexec(int i)
    329 {
    330     pid_t pid;
    331     pid = fork();
    332     if (pid == -1)
    333         ERR_EXIT("fork");
    334 
    335     if (pid > 0)
    336     {
    337         /* 父进程 */
    338         if (backgnd == 1)
    339             printf("%d
    ", pid);
    340         lastpid = pid;
    341     }
    342     else if (pid == 0)
    343     {
    344         /* backgnd=1时,将第一条简单命令的infd重定向至/dev/null */
    345         /* 当第一条命令试图从标准输入获取数据的时候立即返回EOF */
    346 
    347         if (cmd[i].infd == 0 && backgnd == 1)
    348             cmd[i].infd = open("/dev/null", O_RDONLY);
    349 
    350         /* 将第一个简单命令进程作为进程组组长 */
    351         if (i == 0)
    352             setpgid(0, 0);
    353         /* 子进程 */
    354         if (cmd[i].infd != 0)
    355         {
    356             close(0);
    357             dup(cmd[i].infd);
    358         }
    359         if (cmd[i].outfd != 1)
    360         {
    361             close(1);
    362             dup(cmd[i].outfd);
    363         }
    364 
    365         int j;
    366         for (j=3; j<1024; ++j)
    367             close(j);
    368 
    369         /* 前台作业能够接收SIGINT、SIGQUIT信号 */
    370         /* 这两个信号要恢复为默认操作 */
    371         if (backgnd == 0)
    372         {
    373             signal(SIGINT, SIG_DFL);
    374             signal(SIGQUIT, SIG_DFL);
    375         }
    376         execvp(cmd[i].args[0], cmd[i].args);
    377         exit(EXIT_FAILURE);
    378     }
    379 }
    380 
    381 /*执行非内置的命令*/
    382 int execute_disk_command(void)
    383 {
    384     if (cmd_count == 0)
    385         return 0;
    386 
    387     if (infile[0] != '')
    388         cmd[0].infd = open(infile, O_RDONLY);
    389 
    390     if (outfile[0] != '')
    391     {
    392         if (append)
    393             cmd[cmd_count-1].outfd = open(outfile, O_WRONLY | O_CREAT
    394                 | O_APPEND, 0666);
    395         else
    396             cmd[cmd_count-1].outfd = open(outfile, O_WRONLY | O_CREAT
    397                 | O_TRUNC, 0666);
    398     }
    399 
    400     /* 因为后台作不会调用wait等待子进程退出 */
    401     /* 为避免僵死进程,可以忽略SIGCHLD信号 */
    402     if (backgnd == 1)
    403         signal(SIGCHLD, SIG_IGN);
    404     else
    405         signal(SIGCHLD, SIG_DFL);
    406 
    407     int i;
    408     int fd;
    409     int fds[2];
    410     for (i=0; i<cmd_count; ++i)
    411     {
    412         /* 如果不是最后一条命令,则需要创建管道 */
    413         if (i<cmd_count-1)
    414         {
    415             pipe(fds);
    416             cmd[i].outfd = fds[1];
    417             cmd[i+1].infd = fds[0];
    418         }
    419 
    420         forkexec(i);
    421 
    422         if ((fd = cmd[i].infd) != 0)
    423             close(fd);
    424 
    425         if ((fd = cmd[i].outfd) != 1)
    426             close(fd);
    427     }
    428 
    429     if (backgnd == 0)
    430     {
    431         /* 前台作业,需要等待管道中最后一个命令退出 */
    432         int cnt = 0;
    433         while (wait(NULL) != -1 && cnt != cmd_count) {
    434             cnt++;
    435         }
    436     //    while (wait(NULL) != lastpid)
    437     //        ;
    438     }
    439 
    440     return 0;
    441 }
    442 
    443 
    444 /*
    445  * 内部命令解析
    446  * 返回1表示为内部命令,0表示不是内部命令
    447  */
    448 int builtin(void)
    449 {
    450     /*
    451     if (check("exit"))
    452         do_exit();
    453     else if (check("cd"))
    454         do_cd();
    455     else
    456         return 0;
    457 
    458     return 1;
    459     */
    460 
    461     int i = 0;
    462     int found = 0;
    463     while (builtins[i].name != NULL)
    464     {
    465         if (check(builtins[i].name))
    466         {
    467             builtins[i].handler();
    468             found = 1;
    469             break;
    470         }
    471         i++;
    472     }
    473 
    474     return found;
    475 }
    476 
    477 void do_exit(void)
    478 {
    479     printf("exit
    ");
    480     exit(EXIT_SUCCESS);
    481 }
    482 
    483 void do_cd(void)
    484 {
    485     printf("do_cd ... 
    ");
    486 }
    487 
    488 void do_type(void)
    489 {
    490     printf("do_type ... 
    ");
    491 }
    View Code

    参考资料:

    1. BruceMolay, 莫莱, Molay,等. Unix/Linux编程实践教程[M]. 清华大学出版社, 2004.

    2. c++教程网, linux_miniShell实践

    3. RandalE.Bryant, DavidR.O'Hallaron, 布莱恩特,等. 深入理解计算机系统[M]. 机械工业出版社, 2011.

  • 相关阅读:
    双重标准,我该怎么解决
    今天突然点开一个网页
    反省
    (无)
    (无)
    GetModuleHandleW 分析
    关于逆向360相关的一点感想
    OpenGL的编译和使用
    360 补天平台,也没个什么人啊。。。
    openssl编译方法
  • 原文地址:https://www.cnblogs.com/wangxiaobao/p/6438725.html
Copyright © 2011-2022 走看看