這是本身最近學習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 }
參考資料:
1. BruceMolay, 莫萊, Molay,等. Unix/Linux編程實踐教程[M]. 清華大學出版社, 2004.
2. c++教程網, linux_miniShell實踐
3. RandalE.Bryant, DavidR.O'Hallaron, 布萊恩特,等. 深刻理解計算機系統[M]. 機械工業出版社, 2011.