介绍
Shell实现:基本功能一文介绍了如何实现Shell的基本功能,本文介绍如何实现I/O重定向和管道。
I/O重定向使得程序可以自由地指定数据的流向,不一定从键盘读取数据或输出结果到屏幕上;管道使得一条命令的输出可以作为另一条命令的输入,多条命令可以配合完成一项任务。例如下面的命令:
1 | $ ls -l > 1.txt |
第一条命令中的>
符号表示将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 | pid_t pid = fork(); |
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 | int fd = open(filename, O_RDONLY); // 打开文件,描述符fd对应文件 |
将标准输入重定向到文件后,再执行用户输入的命令,命令会从指定的文件中读取数据作为输入,完整代码如下:
1 | void exec_input_redirect(char **arg_vec, int pos) |
输出重定向
类似地,实现输出重定向需将文件描述符1定位到文件上。完整代码如下:
1 | void exec_output_redirect(char **arg_vec, int pos) |
管道
管道(pipe)是进程间通信的重要手段之一。调用pipe
函数创建一个管道,并将其两端连接到两个文件描述符,其中pipefd[0]
为读数据端的文件描述符,pipefd[1]
为写数据端的文件描述符:
1 | int pipe(int pipefd[2]) |
当进程创建一个管道之后,该进程就有了连向管道两端的连接(即为两个文件描述符)。当该进程fork一个子进程时,子进程也继承了这两个连向管道的连接,如下面左图所示。父进程和子进程都可以将数据写到管道的写数据端口,并从读数据端口将数据读出。两个进程都可以读写管道,但当一个进程读,另一个进程写时,管道的使用效率是最高的,因此,每个进程最好关闭管道的一端,如下面右图所示。
Shell要实现管道功能,需将前一条命令的输出作为后一条命令的输入。那么以上面右图为基础,还需将前一进程的标准输出重定向到管道的写数据端,将后一进程的标准输入重定向到管道的读数据端,如下图所示:
我们将运行整条命令的子进程称为进程A,本文Shell的实现中,进程A并不执行命令,而是再fork两个进程,称之为进程B1和B2,分别执行两条命令。两个进程都从进程A继承了管道两端的连接,可通过该管道通信。进程A不再需要管道连接,于是关闭两个文件描述符,然后等待进程B1和B2执行完毕。完整代码如下:
1 | void exec_pipeline(char **arg_vec, int pos) |
完整的代码请参考这里。