Tinyshell: 一個簡易的shell命令解釋器

這是本身最近學習Linux系統編程以後寫的一個練手的小程序,能很好地複習系統編程中的進程管理、信號、管道、文件等內容。linux

經過回顧寫的過程當中遇到的問題的形式記錄程序的關鍵點,最後給出完整程序代碼。c++

0. Tinyshell的功能shell

這個簡易的shell解釋器能夠解析磁盤命令,支持管道和輸入輸出重定向,內置命令只實現了exit,能夠斷定後臺執行命令(&),但未實現bg功能(後臺命令直接返回)。編程

 

1. shell是如何運行程序的小程序

基本的模式就是主進程從鍵盤獲取命令、解析命令,並fork出子進程執行相應的命令,最後主進程在子進程結束後回收(避免殭屍進程)。數組

這裏執行命令能夠採用exec家族中的execvp數據結構

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

兩個參數分別傳遞程序名(如ls)和命令行參數(如 -l)便可。app

 

2. 怎麼解析命令?ide

因爲命令行解析要實現管道功能和I/O重定向功能,因此解析命令也稍微有些複雜。函數

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

avline解析命令,去除空格,不一樣字符串之間以\0間隔。

定義一個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];            /*解析過添加好\0的命令*/
 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("\n[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("\nexit\n");
147 }
148 
149 
150 /*
151  * 讀取命令
152  * 成功返回0,失敗或者讀取到文件結束符(EOF)返回-1
153  */
154 int read_command(void)
155 {
156     /* 按行讀取命令,cmdline中包含\n字符 */
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("\n"))
170         return 0;
171 
172     /* 判斷是否內部命令並執行它 */
173     if (builtin())
174         return 0;
175 
176 
177     /* 一、解析第一條簡單命令 */
178     get_command(0);
179     /* 二、斷定是否有輸入重定向符 */
180     if (check("<"))
181         getname(infile);
182     /* 三、斷定是否有管道 */
183     int i;
184     for (i=1; i<PIPELINE; ++i)
185     {
186         if (check("|"))
187             get_command(i);
188         else
189             break;
190     }
191     /* 四、斷定是否有輸出重定向符 */
192     if (check(">"))
193     {
194         if (check(">"))
195             append = 1;
196         getname(outfile);
197     }
198     /* 五、斷定是否後臺做業 */
199     if (check("&"))
200         backgnd = 1;
201     /* 六、斷定命令結束‘\n’*/
202     if (check("\n"))
203     {
204         cmd_count = i;
205         return cmd_count;
206     }
207     else
208     {
209         fprintf(stderr, "Command line syntax error\n");
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 != '\0')
227     {
228         /* 去除空格 */
229         while (*lineptr == ' ' || *lineptr == '\t')
230             *lineptr++;
231 
232         /* 將第i條命令第j個參數指向avptr */
233         cmd[i].args[j] = avptr;
234         /* 提取參數 */
235         while (*lineptr != '\0'
236             && *lineptr != ' '
237             && *lineptr != '\t'
238             && *lineptr != '>'
239             && *lineptr != '<'
240             && *lineptr != '|'
241             && *lineptr != '&'
242             && *lineptr != '\n')
243         {
244                 /* 參數提取至avptr指針所向的數組avline */
245                 *avptr++ = *lineptr++;
246                 inword = 1;
247         }
248         *avptr++ = '\0';
249         switch (*lineptr)
250         {
251         case ' ':
252         case '\t':
253             inword = 0;
254             j++;
255             break;
256         case '<':
257         case '>':
258         case '|':
259         case '&':
260         case '\n':
261             if (inword == 0)
262                 cmd[i].args[j] = NULL;
263             return;
264         default: /* for '\0' */
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 == '\t')
279         lineptr++;
280 
281     p = lineptr;
282     while (*str != '\0' && *str == *p)
283     {
284         str++;
285         p++;
286     }
287 
288     if (*str == '\0')
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 == '\t')
301         lineptr++;
302 
303     while (*lineptr != '\0'
304             && *lineptr != ' '
305             && *lineptr != '\t'
306             && *lineptr != '>'
307             && *lineptr != '<'
308             && *lineptr != '|'
309             && *lineptr != '&'
310             && *lineptr != '\n')
311     {
312             *name++ = *lineptr++;
313     }
314     *name = '\0';
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\n", 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] != '\0')
388         cmd[0].infd = open(infile, O_RDONLY);
389 
390     if (outfile[0] != '\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\n");
480     exit(EXIT_SUCCESS);
481 }
482 
483 void do_cd(void)
484 {
485     printf("do_cd ... \n");
486 }
487 
488 void do_type(void)
489 {
490     printf("do_type ... \n");
491 }
View Code

 

參考資料:

1. BruceMolay, 莫萊, Molay,等. Unix/Linux編程實踐教程[M]. 清華大學出版社, 2004.

2. c++教程網, linux_miniShell實踐

3. RandalE.Bryant, DavidR.O'Hallaron, 布萊恩特,等. 深刻理解計算機系統[M]. 機械工業出版社, 2011.

相關文章
相關標籤/搜索