13.5.1 管道关闭后的读操作
接下来先研究一下打开的文件描述符,此前一直采取的是让读进程读取一些数据然后直接退出的方式,并假设linux会把清理文件当作是在进程结束时应该做的工作的一部分.但大多数从标准输入读取数据的程序采用的确实与此前的例子非常不同的另外一种做法.通常,它们并不直到有多少数据需要读取,所以往往采用循环的方法,读取数据--处理数据--读取更多的数据,直到没有数据可读为止.
当没有数据可读时,read调用通常会阻塞,即它将暂停进程来等待直到有数据到达为止.如果管道的另一端已被关闭,也就是说,没有进程打开这个管道并向它写数据了,这是read调用就会阻塞.但这样的阻塞不是很有用,因此对一个已关闭数据的管道做read调用将返回0而不是阻塞.这就使读进程能够像检测文件结束一样,对管道进程检测并作出相应的动作.注意,这与读取一个无效的文件描述符不同,read把无效的文件描述符看作一个错误并返回-1.
如果跨越fork调用使用管道,就会有两个不同的文件描述符可以用于向管道写数据,一个在父进程中,一个在子进程中,只有把父子进程中的针对管道的写文件描述符都关闭,管道才会被认为是关闭了,对管道的read调用才会失败.
13.5.2 把管道用作标准输入和输出
现在,已经直到如何使得对一个空管道的读操作失败,下面看一种用管道连接两个进程的更简洁的方法.把其中一个管道文件描述符设置为一个已知值,一般是标准输入0或标准输出1.在父进程中做这个设置稍微有点复杂,但它使得子程序的编写变得非常简单.这样做的最大好处是可以调用标准程序,即那些不需要以文件描述符为参数的程序.为了完成这个工作,先家邵第3章中的dup函数,dup函数有两个紧密关联的版本,它们的原型如下所示:
#include <unistd.h> int dup(int file_descriptor); int dup2(int file_descriptor_one, int file_descriptor_two);dup调用的目的是打开一个新的文件描述符,这与open调用有点类似.不同之处是, dup调用创建的新文件描述符与作为它的参数的那个已有文件描述符指向同一个文件(或管道). 对于dup函数来说,新的文件描述符总是取最小的可用值.而对dup2函数来说,它所创建的新文件描述符或者与参数file_descriptor_two相同,或者是第一个大于该参数的可用值.
dup调用是专门用于复制文件描述符的.
那么, dup是如何帮助在进程之间传递数据的呢?诀窍在于,标准输入的文件描述符总是0,而dup返回的新的文件描述符又总是使用最小可用的数字.因此,如果首先关闭文件描述符0然后调用dup,那么新的文件描述符就将是数字0.因为新的文件描述符是复制一个已有的文件描述符,所以标准输入就会改为指向一个传递给dup函数的文件描述符锁对应的文件或管道.这样就创建了两个文件描述符,它们指向同一个文件或管道,而且其中之一是标准输入 .
用close和dup函数对文件描述符进行处理
理解当关闭文件描述符0,然后调用dup究竟发生了什么事情的最简单的方法就是,查看开头的4个文件描述符的状态在这一过程的改变情况,如下所示:
文件描述符 初始值 关闭文件描述符0后 dup调用后
0 标准输入 {已关闭} 管道文件描述符
1 标准输出 标准输出 标准输出
2 标准错误输出 标准错误输出 标准错误输出
3 管道文件描述符 管道文件描述符 管道文件描述符
修改程序pipe3.c,把子程序的stdin文件描述符替换为创建的管道的读取端,还对文件描述符做一些清理,使得子程序可以正确检测到管道中数据的结束.
编写程序pipe5.c.
/************************************************************************* > File Name: pipe5.c > Description: pipe5.c程序把管道用作标准输入和标准输出 > Author: Liubingbing > Created Time: 2015年07月12日 星期日 22时26分09秒 > Other: pipe5.c程序 ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main() { int data_processed; int file_pipes[2]; const char some_data[] = "123"; pid_t fork_result; /* pipe函数创建一个管道,file_pipes[1]用于向管道写数据,file_pipes[0]用于从管道读回数据 */ if (pipe(file_pipes) == 0) { /* fork函数创建一个子进程 */ fork_result = fork(); if (fork_result == (pid_t) - 1) { fprintf(stderr, "Fork failure"); exit(EXIT_FAILURE); } /* fork_result = 0表明是子进程 */ if (fork_result == (pid_t)0) { /* 贯标文件描述符0, 即关闭子进程的标准输入 */ close(0); /* dup函数调用后,标准输入改为file_pipes[0] */ dup(file_pipes[0]); /* 关闭原先用来从管道读取数据的文件描述符file_pipes[0]*/ close(file_pipes[0]); /* 关闭原先用于向管道写数据的文件描述符file_pipes[1] */ close(file_pipes[1]); /* 使用"od"程序替换当前进程 */ execlp("od", "od", "-c", (char *)0); exit(EXIT_FAILURE); } else { /* 父线程 */ /* 关闭原来用于从管道读取数据的文件描述符file_pipes[0] */ close(file_pipes[0]); /* 向管道写入数据 */ data_processed = write(file_pipes[1], some_data, strlen(some_data)); /* 写完之后,关闭管道的写入端 */ close(file_pipes[1]); printf("%d - wrote %d bytes\n", (int)getpid(), data_processed); } } exit(EXIT_SUCCESS); }程序pipe5.c的运行结果如下所示:
与之前一样,这个 程序创建一个管道,然后通过fork创建一个子进程,此时,父子进程都有可以访问管道的文件描述符,一个用于读数据,一个用于写数据,所以 总共有4个打开的文件描述符.
首先看子进程, 子进程先用close(0)关闭它的标准输入, 然后调用dup(file_pipes[0])把与管道的读取端关联的文件描述符复制为文件描述符0,即标准输入. 接下来,子进程关闭原先的用于从管道读取数据的文件描述符file_pieps[0].因为子进程不会向管道写数据,所以它把与管道关联的写操作文件描述符file_pipes[1]也关闭了,现在它只有一个与管道关联的文件描述符,即文件描述符0,它的标准输入.
接下来, 子进程就可以用exec来启动任何从标准输入读取数据的程序了.在本例中,使用od命令,它将等待数据的到来,就好像它在等待来自用户终端的输入一样.事实上,如果没有明确使用检测这两者之间不同的特殊代码,它并不知道是来自一个管道,而不是来自一个终端.
父进程首先关闭管道的读取端file_pipes[0],因为它不会从管道读取数据,接着它向管道写入数据.当所有数据写完后,父进程关闭管道的写入端并退出.因为现在已没有打开的文件描述符可以向管道写数据了,od程序读取写到管道中的3个字节数据后,后续的读操作符将返回0字节,表示已到达文件尾.当读操作返回0时,od程序就退出运行. 这类似与在终端上运行od命令,然后按下Ctrl+D组合键发送文件尾标志.
如下图:(直接盗用原图了)