全部的Unix
工具都使用文件描述符0
、1
和2
。以下图所示,标准输入文件的描述符是0
,标准输出的文件描述符是1
,标准错误输出的文件描述符则是2
。Unix
假设文件描述符0
、1
和2
都已经被打开,能够分别进行读、写和写的操做。git
经过使用输出重定向标志,命令cmd>filename
告诉shell
将文件描述符1定位到文件。因而shell
就将文件描述符与指定的文件链接起来。程序持续不断地将数据写到文件描述符1
中,根本没有意识到数据的目的地已经改变了。listargs.c
展现了程序甚至没有看到命令行中的重定向符号。github
#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
输出重定向的一些重要概念。编程
shell
并不将重定向标记和文件名传递给程序。api
重定向能够出如今命令行中的任何地方,而且在重定向标识符周围并不须要空格来区分。例如上图命令./listargs testing >xyz one two 2>oops
也能够写成./listargs >xyz testing one two 2>oops
,以下图所示。数组
文件描述符是一个数组的索引号。每一个进程都有其打开的一组文件,这些打开的文件被保持在一个数组中。文件描述符即为某文件在此数组中的索引。而且,当打开文件时,为此文件安排的文件描述符老是此数组中最低可用位置的索引。函数
考虑如何将标准输入重定向以致能够从文件中读取数据。更加精确的说,进程并非从文件读数据,而是从文件描述符读取数据。若是将文件描述符0
重定向到一个文件,那么此文件就成为标准输入的源。工具
第一种放方法是close-then-open
策略,具体步骤以下:oop
开始时,系统中采用的是典型的设置,即三种标准流是被链接到终端设备上的。输入的数据流通过文件描述符0
而输出的流通过文件描述符1
和2
。学习
接下来,调用close(0)
,将标准输入与终端设备的链接切断。
最后,使用open(filename, O_RDONLY)
打开一个想链接到stdin
上的文件。当前的最低可用文件描述符是0
,所以所打开的文件将被链接到标准输入上。任何从标准输入读取数据的函数都将今后文件中读取数据。
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
。
当输入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
的时候,它的子进程也获得了这两个连向管道的链接。父进程和子进程均可以将数据写到管道的写数据端口,并从读数据端口将数据读出。可是当一个进程读,而另外一个进程写的时候,管道的使用效率是最高的。程序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。