介绍
Shell实现:基本功能一文介绍了如何实现shell的基本功能,本文介绍如何实现I/O重定向和管道。
I/O重定向使得程序可以自由地指定数据的流向,不一定从键盘读取数据或输出结果到屏幕上。管道使得一条命令的输出可以作为另一条命令的输入,多条命令可以配合完成一项任务。例如下面的命令:
1 2 3 4 5 6 7 8 9
| $ ls -l > 1.txt $ cat 1.txt total 24 -rw-r--r-- 1 krist users 0 Apr 15 16:37 1.txt -rw-r--r-- 1 krist users 86 Apr 15 09:56 Makefile -rw-r--r-- 1 krist users 13177 Apr 15 09:56 psh.c -rw-r--r-- 1 krist users 21 Apr 15 10:05 README.md $ cat < 1.txt | wc -l 5
|
第一条命令中的>
符号表示将ls -l
命令的输出结果重定向到文件1.txt
。最后一条命令中的<
符号表示将文件1.txt
的内容作为cat
命令的输入(cat < 1.txt
等价于cat 1.txt
),|
符号表示将前面命令的运行结果作为命令wc -l
的输入,所以命令最终的运行结果为1.txt
中文本的行数。
由于在下暂时才疏学浅,只好先实现简单的功能,传达基本精神即可。因此,本文的shell实现对输入的命令进行限制:
- 一次只能有一种操作,即最多只能包含一个
<
,>
或|
符号,所以cat < 1.txt | wc -l
是不合法的
- 三种符号的前后必须有空格,类似于
ls>1.txt
或cat 1.txt| wc -l
是不合法的
主流程
Shell的主进程会fork一个子程序运行用户输入的命令。子进程首先检查命令中是否包含<
,>
或|
符号,以确定命令的类型,然后用做相应的处理。主流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| pid_t pid = fork(); switch (pid) { case -1: perror("fork"); exit(EXIT_FAILURE); case 0: int symbol_pos; int cmd_type = check_cmd(arg_vec, &symbol_pos); switch (cmd_type) { case CMD_NORMAL: exec_normal(arg_vec); case CMD_INPUT_REDIRECT: exec_input_redirect(arg_vec, symbol_pos); case CMD_OUTPUT_REDIRECT: exec_output_redirect(arg_vec, symbol_pos); case CMD_PIPELINE: exec_pipeline(arg_vec, symbol_pos); case CMD_INVALID: fprintf(stderr, "Invalid command!\n"); exit(EXIT_FAILURE); default: break; } default: while (wait(&status) != pid); }
|
arg_vec
变量是字符串数组,保存处理之后的用户命令。如果输入命令为ls -al | wc -l
,那么该数组的内容为:
1
| char *arg_vec[] = {"ls", "-al", "|", "wc", "-l", NULL};
|
实现原理
所有的系统调用(system call)都通过文件描述符(file
descriptor)对各种类型的文件进行I/O操作。每个进程都维护自己的一组文件描述符。
一般来说,所有的程序都会使用三个标准的文件描述符0、1和2,分别对应着标准输入、标准输出和标准错误。当通过shell运行命令时,这三个描述符在程序运行之前就会打开。准确来说,是程序继承了shell的描述符,而shell会一直保持这三个描述符是打开的。
程序会从标准输入读入数据,输出结果到标准输出,输出错误到标准错误。当我们使用交互式的shell时,这三个描述符都连接到shell所运行的终端上,所以程序会从键盘读取数据,然后运行,最后把结果和错误打印到屏幕上。所以,要进行I/O重定向和管道操作,就要重定向相应的文件描述符。
I/O重定向
输入重定向
程序从标准输入读取数据,如果将文件描述符0定位到一个文件上,那么此文件就成了标准输入的源。实现上述功能要用到dup2
函数:
1
| int dup2(int oldfd, int newfd);
|
dup2
函数将oldfd
文件描述符复制给newfd
,如果newfd
之前打开了,dup2
会先将它关闭。将文件描述符0重定向到文件的步骤如下:
1 2 3
| int fd = open(filename, O_RDONLY); dup2(fd, 0); close(fd);
|
将标准输入重定向到文件后,再执行用户输入的命令,命令会从指定的文件中读取数据作为输入,完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void exec_input_redirect(char **arg_vec, int pos) { char *filename = arg_vec[pos+1]; arg_vec[pos] = NULL;
int fdin = open(filename, O_RDONLY); if (fdin == -1) { perror("open"); exit(EXIT_FAILURE); } if (dup2(fdin, STDIN_FILENO) == -1) { perror("dup2"); exit(EXIT_FAILURE); } if (close(fdin) == -1) { perror("close"); exit(EXIT_FAILURE); } execvp(arg_vec[0], arg_vec); perror("execvp"); exit(EXIT_FAILURE); }
|
输出重定向
类似地,实现输出重定向需将文件描述符1定位到文件上。完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| void exec_output_redirect(char **arg_vec, int pos) { char *filename = arg_vec[pos + 1]; arg_vec[pos] = NULL;
int fdout = open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (fdout == -1) { perror("open"); exit(EXIT_FAILURE); } if (dup2(fdout, STDOUT_FILENO) == -1) { perror("dup2"); exit(EXIT_FAILURE); } if (close(fdout) == -1) { perror("close"); exit(EXIT_FAILURE); } execvp(arg_vec[0], arg_vec); perror("execvp"); exit(EXIT_FAILURE); }
|
管道
管道(pipe)是进程间通信的重要手段之一。调用pipe
函数创建一个管道,并将其两端连接到两个文件描述符,其中pipefd[0]
为读数据端的文件描述符,pipefd[1]
为写数据端的文件描述符:
当进程创建一个管道之后,该进程就有了连向管道两端的连接(即为两个文件描述符)。当该进程fork一个子进程时,子进程也继承了这两个连向管道的连接,如下面左图所示。父进程和子进程都可以将数据写到管道的写数据端口,并从读数据端口将数据读出。两个进程都可以读写管道,但当一个进程读,另一个进程写时,管道的使用效率是最高的,因此,每个进程最好关闭管道的一端,如下面右图所示。
Shell要实现管道功能,需将前一条命令的输出作为后一条命令的输入。那么以上面右图为基础,还需将前一进程的标准输出重定向到管道的写数据端,将后一进程的标准输入重定向到管道的读数据端,如下图所示:
我们将运行整条命令的子进程称为进程A,本文shell的实现中,进程A并不执行命令,而是再fork两个进程,称之为进程B1和B2,分别执行两条命令。两个进程都从进程A继承了管道两端的连接,可通过该管道通信。进程A不再需要管道连接,于是关闭两个文件描述符,然后等待进程B1和B2执行完毕。完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| void exec_pipeline(char **arg_vec, int pos) { char **arg_vec1 = &arg_vec[0]; arg_vec[pos] = NULL; char **arg_vec2 = &arg_vec[pos+1];
int pfd[2]; if (pipe(pfd) == -1) { perror("pipe"); exit(EXIT_FAILURE); }
switch (fork()) { case -1: perror("fork"); exit(EXIT_FAILURE); case 0: if (close(pfd[0]) == -1) { perror("close"); exit(EXIT_FAILURE); } if (pfd[1] != STDOUT_FILENO) { if (dup2(pfd[1], STDOUT_FILENO) == -1) { perror("dup2"); exit(EXIT_FAILURE); } if (close(pfd[1]) == -1) { perror("close"); exit(EXIT_FAILURE); } } execvp(arg_vec1[0], arg_vec1); perror("execvp"); exit(EXIT_FAILURE); default: break; }
switch (fork()) { case -1: perror("fork"); exit(EXIT_FAILURE); case 0: if (close(pfd[1]) == -1) { perror("close"); exit(EXIT_FAILURE); } if (pfd[0] != STDIN_FILENO) { if (dup2(pfd[0], STDIN_FILENO) == -1) { perror("dup2"); exit(EXIT_FAILURE); } if (close(pfd[0]) == -1) { perror("close"); exit(EXIT_FAILURE); } } execvp(arg_vec2[0], arg_vec2); perror("execvp"); exit(EXIT_FAILURE); default: break; }
if (close(pfd[0]) == -1) { perror("close"); exit(EXIT_FAILURE); } if (close(pfd[1]) == -1) { perror("close"); exit(EXIT_FAILURE); } if (wait(NULL) == -1) { perror("wait"); exit(EXIT_FAILURE); } if (wait(NULL) == -1) { perror("wait"); exit(EXIT_FAILURE); }
exit(EXIT_SUCCESS); }
|
完整的代码请参考这里。
参考
- The Linux
Programming Interface
- Unix/Linux编程实践教程