这是自己最近学习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解析命令,去除空格,不同字符串之间以\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>View Code
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 /* 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、判定命令结束‘\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.