I/O重定向和管道
标准I/O与重定向的若干概念
3个标准文件描述符
所有的Unix
工具都使用文件描述符0
、1
和2
。如下图所示,标准输入文件的描述符是0
,标准输出的文件描述符是1
,标准错误输出的文件描述符则是2
。Unix
假设文件描述符0
、1
和2
都已经被打开,可以分别进行读、写和写的操作。
重定向I/O的是shell而不是程序
通过使用输出重定向标志,命令cmd>filename
告诉shell
将文件描述符1定位到文件。于是shell
就将文件描述符与指定的文件连接起来。程序持续不断地将数据写到文件描述符1
中,根本没有意识到数据的目的地已经改变了。listargs.c
展示了程序甚至没有看到命令行中的重定向符号。
#include <stdio.h>
int main(int ac, char* av[]) {
int i;
printf("Number of args: %d, Args are: \n", ac);
for(i = 0; i < ac; i++) {
printf("args[%d] %s\n", i, av[i]);
}
fprintf(stderr, "This message is sent to stderr.\n");
}
程序listargs
将命令行参数打印到标准输出。注意listargs
并没有打印出重定向符号和文件名。
如上图所示验证了关于shell
输出重定向的一些重要概念。
shell
并不将重定向标记和文件名传递给程序。- 重定向可以出现在命令行中的任何地方,并且在重定向标识符周围并不需要空格来区分。例如上图命令
./listargs testing >xyz one two 2>oops
也可以写成./listargs >xyz testing one two 2>oops
,如下图所示。
最低可用文件描述符原则
文件描述符是一个数组的索引号。每个进程都有其打开的一组文件,这些打开的文件被保持在一个数组中。文件描述符即为某文件在此数组中的索引。并且,当打开文件时,为此文件安排的文件描述符总是此数组中最低可用位置的索引。
将stdin重定向到文件
考虑如何将标准输入重定向以至可以从文件中读取数据。更加精确的说,进程并不是从文件读数据,而是从文件描述符读取数据。如果将文件描述符0
重定向到一个文件,那么此文件就成为标准输入的源。
方法1:close-then-open
第一种放方法是close-then-open
策略,具体步骤如下:
- 开始时,系统中采用的是典型的设置,即三种标准流是被连接到终端设备上的。输入的数据流经过文件描述符
0
而输出的流经过文件描述符1
和2
。 - 接下来,调用
close(0)
,将标准输入与终端设备的连接切断。 - 最后,使用
open(filename, O_RDONLY)
打开一个想连接到stdin
上的文件。当前的最低可用文件描述符是0
,因此所打开的文件将被连接到标准输入上。任何从标准输入读取数据的函数都将从此文件中读取数据。
方法2:open-close-dup-close
Unix
系统调用dup
建立指向已经存在的文件描述符的第二个连接,这种方法需要4
个步骤。
open(file)
,打开stdin
将要重定向的文件。这个调用返回一个文件描述符fd
,这个描述符并不是0
,因为0
在当前已经被打开了。close(0)
,将文件描述符0
关闭,现在文件描述符0
已经空闲了。dup(fd)
,系统调用dup(fd)
将文件描述符fd
做了一个复制。此处复制使用最低可用的文件描述符号。因此获得的文件描述符是0
。这样,就将磁盘文件与文件描述符0
连接在一起了。close(fd)
,使用close(fd)
来关闭原始连接,只留下文件描述符0
的连接。
dup
在学习管道的时候非常重要,一个简单一点的方案是将close(0)和dup(fd)
结合在一起作为一个单独的系统调用dup2
。
重定向I/O:who>userlist
当输入who>userlist
时,shell
运行who
程序,并将who
的标准输出重定向到名为userlist
的文件上。shell
实现该重定向的关键之处在于fork
和exec
之间的时间间隙。在fork
执行完后,子进程仍然在运行父进程也就是shell
程序,并准备执行exec
。exec
将替换进程中运行的程序,但是它不会改变进程的属性和进程中所有的连接。也就是说,在运行exec
之后,进程的用户ID
不会改变,其优先级也不会改变,并且其文件描述符也和运行exec
之前一样。因此,利用这个原则来实现重定向标准输出。
此时who
就是子进程要执行的命令,当执行fork
前,父进程的文件描述符1
指向终端。当执行fork
之后,子进程的文件描述符也喜欢指向终端,此时,子进程尝试执行close(1)
,close(1)
之后,文件描述符1
成为最低未用文件描述符,子进程现在再执行creat(userlist, mode)
打开文件userlist
,文件描述符1
被连接到文件userlist
。因此,子进程的标准输出被重定向到文件userlist
,子进程然后调用exec
执行who
。
子进程执行了who
程序,于是子进程中的代码和数据都被who
程序的代码和数据所替换了,然而文件描述符被保留下来。因为打开的文件并非是程序的代码也不是数据,它们属于进程的属性,因此exec
调用并不改变它们。
管道编程
管道是内核中一个单向的数据通道,管道有一个读取端和一个写入端,可以用来连接一个进程的输出和另一个进程的输入。
创建管道
使用系统调用result = pipe(int array[2])
来创建管道,并将其两端连接到两个文件描述符。如下图所示,array[0]
为读取数据端的文件描述符,而array[1]
则为写数据端的文件描述符。类似与open
调用,pipe
调用也使用最低可用文件描述符。
程序pipedemo.c
展示了如何创建管道并使用管道向自己发送数据。核心代码如下:
int len, i, apipe[2];
char buf[BUFSIZ];
if(pipe(apipe) == -1) {
perror("could not make pipe.");
exit(1);
}
printf("Got a pipe! It is file descriptors: [%d %d]\n", apipe[0], apipe[1]);
while(fgets(buf, BUFSIZ, stdin)) {
len = strlen(buf);
if(write(apipe[1], buf, len) != len) {
perror("writing to pipe.");
break;
}
for(i = 0; i < len; i++) {
buf[i] = 'X';
}
len = read(apipe[0], buf, BUFSIZ);
if(len == -1) {
perror("reading from pipe.");
break;
}
if(write(1, buf, len) != len) {
perror("writing to stdout");
break;
}
}
数据流从键盘到进程,从进程到管道,再从管道到进程以及从进程回到终端。
使用fork来共享管道
当进程创建一个管道之后,该进程就有了连向管道两端的连接。当这个进程调用fork
的时候,它的子进程也得到了这两个连向管道的连接。父进程和子进程都可以将数据写到管道的写数据端口,并从读数据端口将数据读出。但是当一个进程读,而另一个进程写的时候,管道的使用效率是最高的。程序pipedemo2.c
说明了如何将pipe
和fork
结合起来,创建一对通过管道来通信的进程,核心代码如下:
int pipefd[2];
int len;
char buf[BUFSIZ];
int read_len;
if(pipe(pipefd) == -1) {
oops("cannot get a pipe", 1);
}
switch(fork()) {
case -1:
oops("cannot fork", 2);
/*子进程*/
case 0:
len = strlen(CHILD_MESS);
while(1) {
if(write(pipefd[1], CHILD_MESS, len) != len) {
oops("write", 3);
}
sleep(5);
}
/*父进程*/
default:
len = strlen(PAR_MESS);
while(1) {
if(write(pipefd[1], PAR_MESS, len) != len) {
oops("write", 4);
}
sleep(1);
read_len = read(pipefd[0], buf, BUFSIZ);
if(read_len <= 0) {
break;
}
write(1, buf, read_len);
}
}
技术细节
- 从管道中读取数据
- 当进程试图从管道读取数据时,进程被挂起直到数据被写进管道。
-
当所有的写进程关闭了管道的写数据端时,试图从管道中读取数据的调用会返回
0
,这意味这文件的结束。 - 向管道中写数据
- 写入数据阻塞直到管道有空间去容纳新的数据。
- 如果所有的读进程都已关闭了管道的读数据端,那么对管道的写入调用将会执行失败。
总结
Unix
默认从文件描述符0
读取数据,写数据到文件描述符1
,将错误信息输出到文件描述符2
。- 创建文件描述符的系统调用总是使用最低可用文件描述符号。
- 重定向标准输入、标准输出和错误输出意味着改变文件描述符
0
、1
和2
的连接。 - 管道是内核中的一个数据队列,其每一端连接一个文件描述符。程序通过
pipe
系统调用来创建管道。 - 当父进程调用
fork
的时候,管道的两端都被复制到子进程中。 - 只有有共同父进程的进程之间才可以用管道连接。
代码
相关代码见Github。