进程间通信简介(二)——管道

来源:互联网 发布:中国的阿拉伯之春 知乎 编辑:程序博客网 时间:2024/06/08 07:49

3 管道

3.1 管道的概念

        管道是Linux/Unix系统中比较原始的进程间通信形式,它实现数据以一种数据流的方式在进程间流动。在系统中其相当于文件系统上的一个文件来缓存所要传输的数据。在某些特性上又不同于文件,例如,当数据读出后,则管道中就没有数据了,但文件没有这个特性。匿名管道在系统中是没有实名的,并不可以在文件系统中以任何方式看到该管道。它只是进程的一种资源,会随着进程的结束而被系统清除。创建一个管道时生成了两个文件描述符,但对于管道中所使用的文件描述符并没有路径名,也就是不存在任何意义上的文件,它们只是在内存中跟某一个索引节点相关联的两个文件描述符。

        管道通信具有以下特点:

  • 管道没有名字,所以也称为匿名管道。
  • 管道是半双工的(至少在大多数系统中是这样的),数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。
  • 只能用于父进程或兄弟进程之间(具有亲缘关系的进程)。
  • 单独构成一种独立的文件系统。管道对于管道两端的进程而言就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。
  • 数据的读出和写入:一个进程向管道中写的内存被管道另一端的进程读出。写入的内容每次都被添加在管道缓冲区的末端,并且每次都是从缓冲区的头部读出数据。
  • 管道的缓冲区是有限的(管道只存在内存中,在管道创建时,为缓冲区分配一个页面大小)。
  • 管道所传送的都是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算做一个消息(或命令、记录)等。

3.2 进程管道

        可能最简单的在两个程序之间传递数据的方法就是使用popen和pclose函数了。原型如下:

#include <stdio.h>

FILE *popen (const char *command, const char *open_mode);

int pclose (FILE *stream_to_close);

        1. popen函数

        popen函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。command字符串是要运行的程序名和相应的参数,open_mode必须是“r”或者“w”。

        如果open_mode是“r”,被调用程序的输出就可以被调用程序使用,调用程序利用popen函数返回的FILE*文件流指针,就可以通过常用的stdio库函数(如fread)来读取被调用程序的输出。如果open_mode是“w”,调用程序就可以用fwrite调用向被调用程序发送数据,而被调用程序可以在自己的标准输入上读取这些数据,被调用的程序通常不会意识到自己正从另外一个进程读取数据,它只是在标准输入流上读取数据,然后做出相应的操作。

        每个popen调用都必须指定“r”或者“w”,在popen函数的标准实现中不支持任何其他选项,这意味着我们不能调用另一个程序并同时对它进行读写操作。popen函数在失败时返回一个空指针。如果想通过管道实现双向通信,最普遍的解决方法就是使用两个管道,每个管道负责一个方向的数据流。

        2. pclose函数

        用popen启动的进程结束时可以用pclose函数关闭与之关联的文件流。pclose调用只在popen启动的进程结束后才返回,如果调用pclose时它仍在运行,pclose调用将等待该进程的结束。pclose调用的返回值通常是它所关闭的文件流所在进程的退出码。如果调用进程在调用pclose之前执行了一个wait语句,被调用进程的退出状态就会丢失,因为被调用进程已结束。此时,pclose将返回-1并设置errno为ECHILD。

        接下来演示使用popen函数访问uname命令给出的信息(命令uname -a的作用是打印系统信息,包括计算机型号、操作系统名称、版本和发行号以及计算机的网络名)。

#include <stdio.h>#include <unistd.h>#include <string.h>#include <stdlib.h>int main(){FILE *read_fp;char buffer[BUFSIZ + 1];int chars_read;memset(buffer, '\0', sizeof(buffer));read_fp = popen("uname -a", "r");if (read_fp != NULL){chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);if (chars_read > 0)printf("Output was:-\n%s\n", buffer);pclose(read_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);}

        编译并运行:

$ ./popen_uname
Output was:-
Linux regan 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$

        接下来演示将输出发送到外部程序的示例。这里将数据通过管道送往另一个程序,使用od(八进制输出)命令。

#include <stdio.h>#include <unistd.h>#include <string.h>#include <stdlib.h>int main(){FILE *write_fp;char buffer[BUFSIZ + 1];sprintf(buffer, "The file name is 'popen_od.c'\n");write_fp = popen("od -c", "w");if (write_fp != NULL){fwrite(buffer, sizeof(char), strlen(buffer), write_fp);pclose(write_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);}

        编译并运行:

$ ./popen_od
0000000   T   h   e       f   i   l   e       n   a   m   e       i   s
0000020       '   p   o   p   e   n   _   o   d   .   c   '  \n
0000036
$

        在shell命令行上可以使用下面的命令得到同样的输出结果:

$ echo "The file name is 'popen_od.c'" | od -c

3.2.1 传递更多的数据

        目前所使用的机制都只是将所有数据通过一次fread或者fwrite调用来发送或接收。有时可能希望能以块方式发送数据,或者根本不知道输出数据的长度。为了避免定义一个非常大的缓冲区,可以使用多个fread或fwrite调用来将数据分为几部分处理。

        接下来演示通过管道读取所有数据,因为从被调用的进程ps ax读取数据,而该进程输出的数据有多少事先无法知道,所以必须对管道进行多次读取。

#include <stdio.h>#include <unistd.h>#include <string.h>#include <stdlib.h>#define BUFSIZE 64int main(){FILE *read_fp;char buffer[BUFSIZE + 1];int chars_read;memset(buffer, '\0', sizeof(buffer));read_fp = popen("ps ax", "r");if (read_fp != NULL){chars_read = fread(buffer, sizeof(char), BUFSIZE, read_fp);while (chars_read > 0){buffer[chars_read - 1] = '\0';printf("Reading %d:-\n %s\n", BUFSIZE, buffer);chars_read = fread(buffer, sizeof(char), BUFSIZE, read_fp);}pclose(read_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);}
        这里将BUFSIZE设定为小一点的数值,编译并运行,你就可以看到打印输出有明显的分割字符串。

$ ./popen_psax

...(内容太多,省略)

Reading 64:-
 7    S+     0:00 ./popen_psax
 3887 pts/7    S+     0:00 sh -c
Reading 64:-
 s ax
 3888 pts/7    R+     0:00 ps ax

...

$

3.2.2 如何实现popen

        请求popen调用运行一个程序时,它首先启动shell,即系统中的sh命令,然后将command字符串作为一个参数传递给它,这有两个效果,一个好,一个不太好。

        在Linux(以及所有的类Unix系统)中,所有的参数扩展都是由shell来完成的,所以,在启动程序之前先启动shell来分析命令字符串,就可以使各种shell扩展(如*.c所指的是哪些文件)在程序启动之前就全部完成。这个功能非常有用,它允许我们通过popen启动非常复杂的shell命令。而其他一些创建进程的函数(如execl)调用起来就复杂得多,因为调用进程必须自己去完成shell扩展。

        使用shell的一个不太好的影响是,针对每个popen调用,不仅要启动一个被请求的程序,还要启动一个shell,即每个popen调用将多启动两个进程,从节省系统资源的角度来看,popen函数的调用成本略高,而且对目标命令的调用比正常方式要慢一些。

        接下来演示popen函数的行为。这个程序对所有popen示例程序的源文件的总行数进行统计,方法是用cat命令显示文件的内容并将输出通过管道传递给命令wc -l,由后者统计总行数。在命令行上,可以使用以下命令:

$ cat popen*.c | wc -l

        事实上,输入命令的方式更简单而且更有效率。

#include <stdio.h>#include <unistd.h>#include <string.h>#include <stdlib.h>#define BUFSIZE 128int main(){FILE *read_fp;char buffer[BUFSIZE + 1];int chars_read;memset(buffer, '\0', sizeof(buffer));read_fp = popen("cat popen*.c | wc -l", "r");if (read_fp != NULL){chars_read = fread(buffer, sizeof(char), BUFSIZE, read_fp);while (chars_read > 0){buffer[chars_read - 1] = '\0';printf("Reading:-\n %s\n", buffer);chars_read = fread(buffer, sizeof(char), BUFSIZE, read_fp);}fclose(read_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);}

        编译并运行:

$ ./popen_wc
Reading:-
 129
$

3.3 管道的创建与关闭

        Linux环境下使用pipe函数创建一个匿名管道,原型如下:

#include <unistd.h>

int pipe (int pipefd[2]);

        返回值:若成功返回0,否则返回-1并设置errno来表明失败原因。在Linux手册页中定义了下面一些错误:

  • EMFILE:进程使用的文件描述符过多。
  • ENFILE:系统的文件表已满。
  • EFAULT:文件描述符无效。

        参数pipefd[2]是一个长度为2的文件描述符数组,pipefd[0]是读出端的文件描述符,pipefd[1]是写入端的文件描述符。当函数成功返回后,则自动维护了一个从pipefd[1]到pipefd[0]的数据通道。数据基于先进先出的原则(通常简写为FIFO)进行处理,这意味着如果把字节1,2,3写到pipefd[1],从pipefd[0]读取到的数据也会是1,2,3。这与栈的处理方式不同,栈采用后进先出的原则,通常简写为LIFO。特别注意的是,这里使用的是文件描述符而不是文件流,所以我们必须用底层的read和write调用来访问数据,而不是使用文件流库函数fread和fwrite。管道的关闭使用的是基于文件描述符的close函数,原型如下:

#include <unistd.h>

int close (int fd);

        返回值:若成功返回0,失败则返回-1,并将errno设置为相关的错误信息。

        接下来演示使用pipe函数创建管道,然后通过管道读写数据。

#include <stdio.h>#include <unistd.h>#include <string.h>#include <stdlib.h>#define BUFSIZE 64int main(){int data_processed;int file_pipes[2];const char data[] = "Hello World!";char buffer[BUFSIZE + 1];memset(buffer, '\0', sizeof(buffer));if (pipe(file_pipes) == 0){data_processed = write(file_pipes[1], data, strlen(data));printf("Wrote %d bytes\n", data_processed);data_processed = read(file_pipes[0], buffer, BUFSIZE);printf("Read %d bytes: %s\n", data_processed, buffer);close(file_pipes[0]);close(file_pipes[1]);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);}

        编译并运行:

$ ./pipe_local
Wrote 12 bytes
Read 12 bytes: Hello World!
$

        如果尝试用pipefd[0]写数据或者用pipefd[1]读数据,其后果并未在文档中明确定义,所以其行为可能会非常奇怪,并且随着系统的不同,其行为可能会发生变化。

        乍看之下,这个使用管道的例子并无特别之处,它做的工作也可以用一个简单的文件完成。管道的真正优势体现在,当你想在两个进程之间传递数据的时侯。因为当程序用fork调用创建新进程时,原先打开的文件描述符仍将保持打开状态,如果在原先的进程中创建一个管道,然后再调用fork创建新进程,我们即可通过管道在两个进程之间传递数据。

        接下来演示跨越fork调用的管道。

#include <stdio.h>#include <unistd.h>#include <string.h>#include <stdlib.h>#define BUFSIZE 64int main(){int data_processed;int file_pipes[2];const char data[] = "Hello World";char buffer[BUFSIZE + 1];pid_t pid;memset(buffer, '\0', sizeof(buffer));if (pipe(file_pipes) == 0){pid = fork();if (pid == -1){fprintf(stderr, "fork failed!\n");exit(EXIT_FAILURE);}if (pid == 0){data_processed = read(file_pipes[0], buffer, BUFSIZE);printf("Child process[%d]: Read %d bytes: %s\n", getpid(), data_processed, buffer);exit(EXIT_SUCCESS);}else{data_processed = write(file_pipes[1], data, strlen(data));printf("Parent process[%d]: Wrote %d bytes\n", getpid(), data_processed);}}exit(EXIT_SUCCESS);}

        编译并运行:

$ ./pipe_fork
Parent process[4194]: Wrote 11 bytes
Child process[4195]: Read 11 bytes: Hello World
$

        这个程序首先调用pipe函数创建一个管道,接着用fork调用创建一个新进程。如果fork调用成功,父进程就写数据到管道中,而子进程从管道中读取数据。父子进程都在只调用一次write或read之后就退出。如果父进程在子进程之前退出,就会在两部分输出内容之间看到shell提示符(我的系统没有这种情况)。

3.4 管道的读写

        可以使用read和write函数对管道进行读写操作,需要注意的是,管道的两端是固定了任务的,即管道的读出端只能用于读取数据,管道的写入端则只能用于写入数据。如果试图从管道写端读取数据,或者向管道读端写入数据都会导致错误发生(前面已说过这个问题)。一般文件的I/O函数都可以用于管道,如close、read、write等。

        当对一个读端已经关闭的管道进行写操作时,会产生信号SIGPIPE,说明管道读端已经关闭,并且write操作返回-1,errno的值设为EPIPE,对于SIGPIPE信号可以进行捕捉处理。如果写入进程不能捕捉或者干脆忽略SIGPIPE信号,则写入进程会中断。

        管道的读取规则是:如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。另外,在进行读写管道时,对一个管道进行读操作后,read函数返回0,有两种意义,一种是管道中无数据并且写入端已经关闭(即写入端不存在)。另一种是管道中无数据,写入端依然存活。这两种情况要根据需求分别处理。

        管道的写入规则是:向管道中写入数据时,管道缓冲区一旦有空闲区域,写进程就会立即试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。注意,只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIGPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作是应用程序终止)。

        如果要建立一个父进程到子进程的数据通道,可以先调用pipe函数紧接着调用fork函数,由于子进程自动继承父进程的数据段,则父子进程同时拥有管道的操作权,此时管道的方向取决于用于怎么维护该管道。当用户想要建立一个父进程到子进程的数据通道(即数据传送方向是从父进程到子进程)时,也就是要在子进程中读出之前在父进程中写入的数据,那么就要在父进程中关闭管道的读出端(以顺利地写入数据),相应地在子进程中关闭管道的写入端(以顺利地读出数据)。相反的,当维护子进程到父进程的数据通道时,在父进程中关闭写入端,子进程中关闭读出端即可。总之,使用pipe及fork组合,可以构造出所有的父进程与子进程,或子进程到兄弟进程的管道。

        接下来演示使用pipe和fork组合实现父子进程通信。这次代码,注意要维护管道的数据方向。

#include <stdio.h>#include <unistd.h>#include <sys/types.h>#include <limits.h>#include <string.h>#include <stdlib.h>#define BUFSIZE PIPE_BUF/* PIPE_BUF: 管道默认一次性读写的数据长度 */void err_quit(const char *msg){printf("%s", msg);exit(1);}int main(void){int fd[2];char buf[BUFSIZE] = "To my child process\n";pid_t pid;int len;if ((pipe(fd)) < 0)err_quit("pipe failed\n");if ((pid = fork()) < 0)err_quit("fork failed\n");else if (pid > 0){close(fd[0]);write(fd[1], buf, strlen(buf));exit(0);}else{close(fd[1]);len = read(fd[0], buf, BUFSIZE);if (len < 0)err_quit("process failed when read a pipe\n");elsewrite(STDOUT_FILENO, buf, len);exit(0);}exit(1);}

        编译并运行:

$ ./pipe_fork2
To my child process
$

        程序在父进程段中关闭了管道的读出端,并相应地在子进程关闭了管道的输入端,从而实现数据从父进程流向子进程。

        而管道在兄弟进程间应用时,应该现在父进程中建立管道,然后调用fork函数创建两个子进程,在兄弟子进程中维护管道的数据方向。这里的问题是维护管道的顺序,当父进程创建了管道,只有子进程已经继承了管道后,父进程才可以执行关闭管道的操作。如果在fork之前已经关闭管道,子进程将不能继承任何可以使用的管道。

        接下来演示管道在兄弟进程间通信。

#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <sys/types.h>#include <limits.h>#include <string.h>#define BUFSIZE PIPE_BUFvoid err_quit(const char *msg){printf("%s", msg);exit(1);}int main(void){int fd[2];char buf[BUFSIZE] = "To my brother\n";pid_t pid;int len;if ((pipe(fd)) < 0)err_quit("pipe failed\n");if ((pid = fork()) < 0)err_quit("fork failed\n");else if (pid == 0){close(fd[0]);write(fd[1], buf, strlen(buf));exit(0);}if ((pid = fork()) < 0)err_quit("fork failed\n");else if (pid > 0){close(fd[0]);close(fd[1]);exit(0);}else{close(fd[1]);len = read(fd[0], buf, BUFSIZE);write(STDOUT_FILENO, buf, len);exit(0);}exit(0);}

        编译并运行:

$ ./pipe_brother
To my brother
$

        程序中父进程在创建第一个子进程时并没有关闭管道两端,而是在创建第二个子进程时才关闭管道。这时为了在创建第二个进程时,子进程可以继承存活的管道,而不是一个两端都已经关闭的管道。

3.5 父进程和子进程

        接下来事先在子进程中运行一个与其父进程完全不同的另外一个程序,而不是仅仅运行一个相同的程序,使用exec调用来完成这一工作。这里有一个难点在于,通过exec调用的进程需要知道应该访问哪个文件描述符。之前的例子中,因为子进程本身有file_pipes数据的一份副本,所以这并不是问题。但经过exec调用后,原先的进程已经被新的子进程替换了。为了解决这个问题,可以将文件描述符(它实际上只是一个数字)作为一个参数传递给用exec启动的程序。

        接下来创建两个程序,第一个程序是数据生产者,它只负责创建管道和启动子进程,后者是数据消费者。

/* 数据生产者 */#include <stdio.h>#include <unistd.h>#include <string.h>#include <stdlib.h>#include <sys/types.h>#define BUFSIZE 64int main(){int data_processed;int fd[2];const char data[] = "send to customer";char buffer[BUFSIZE + 1];pid_t pid;memset(buffer, '\0', sizeof(buffer));if (pipe(fd) == 0){pid = fork();if (pid == (pid_t)-1){fprintf(stderr, "fork failed\n");exit(EXIT_FAILURE);}if (pid == 0){sprintf(buffer, "%d", fd[0]);execl("pipe_customer", "pipe_customer", buffer, NULL);exit(EXIT_SUCCESS);}else{data_processed = write(fd[1], data, strlen(data));printf("%d - wrote %d bytes\n", getpid(), data_processed);}}exit(EXIT_SUCCESS);}

/* 数据消费者 */#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#define BUFSIZE 64int main(int argc, char **argv){int data_processed;char buffer[BUFSIZE + 1];int fd;memset(buffer, '\0', sizeof(buffer));sscanf(argv[1], "%d", &fd);data_processed = read(fd, buffer, BUFSIZE);printf("%d - read %d bytes: %s\n", getpid(), data_processed, buffer);exit(EXIT_SUCCESS);}
        编译并运行:

$ ./pipe_producer
4696 - wrote 16 bytes
4697 - read 16 bytes: send to customer

3.5.1 管道关闭后的读操作

        现在先仔细研究下打开的文件描述符。至此一直采取的方法是让读进程读取一些数据然后直接退出,并假设Linux会把清理文件当作是在进程结束时应该做的工作的一部分。但大多数从标准输入读取数据的程序采用的却是与我们到目前为止见到的例子非常不同的另一种方法。通常它们并不知道有多少数据需要读取,所以往往采用循环的方法,读取数据——处理数据——读取更多的数据,直到没有数据可读为止。当没有数据可读时,read调用通常会阻塞,即它将暂停进程来等待直到有数据到达为止。如果管道的另一端已被关闭,也就是说,没有进程打开这个管道并向它写数据了,这时read调用就会阻塞。但这样的阻塞并不是很有用,因此对一个已关闭的写数据的管道做read调用将返回0而不是阻塞。这就使读进程能够像检测文件结束一样,对管道进行检测并作出相应的动作。注意,这与读取一个无效的文件描述符不同,read把无效的文件描述符看作一个错误并返回-1。如果跨越fork调用使用管道,就会有两个不同的文件描述符可以用于向管道写数据,一个在父进程中,一个在子进程中。只有把父子进程中的针对管道的写文件描述符都关闭,管道才会被认为是关闭了,对管道的read调用才会失败。在命名管道中会继续深入讨论这个问题。

3.5.2 把管道用作标准输入和标准输出

        现在我们已经直到如何使得对一个空管道的读操作失败,接下来来看一种用管道连接两个进程的更简洁的方法。我们把其中一个管道文件描述符设置为一个已知值,一般是标准输入0或者标准输出1。在父进程中做这个设置稍微有点复杂,但它使得子程序的编写变得非常简单。

        这样做的最大好处就是我们可以调用标准程序,即那些不需要以文件描述符为参数的程序。萎了完成这个工作,我们需要使用dup函数,有两个紧密关联的版本,原型如下:

#include <unistd.h>

int dup (int oldfd);

int dup2(int oldfd, int newfd);

        dup调用的目的是打开一个新的文件描述符,这与open调用有点类似。不同之处在于,dup调用创建的新文件描述符与作为它的参数的哪个已有文件描述符指向同一个文件(或管道)。对于dup函数来说,新的文件描述符总是取最小的可用值,而对于dup2函数来说,它所创建的新文件描述符或者于参数newfd相同,或者是第一个大于该参数的可用值。我们可以使用更通用的fcntl调用(command参数设置为F_DUPFD)来达到与调用dup和dup2相同的效果。虽然如此,但dup调用更易于使用,因为它是专门用于复制文件描述符的,而且它的使用非常普遍,你可以发现,在已有程序中,它的使用逼fcntl和F_DUPFD更频繁。

        那么,dup是如何帮助我们在进程之间传递数据的呢?诀窍在于,标准输入的文件描述符总是0,而dup返回的新的文件描述符又总是使用最小可用的数字。因此,如果我们首先关闭文件描述符0然后调用dup,那么新的文件描述符就将是数字0。因为新的文件描述符是复制一个已有的文件描述符,所以标准输入就会改为指向一个我们传递给dup函数的文件描述符所对应的文件或管道。我们创建了两个文件描述符,它们指向同一个文件或管道,而且其中之一就是标准输入。

        要理解当我们关闭文件描述符0,然后调用dup究竟发生了什么的最简单方法局势,查看开头的4个文件描述符的状态在这一过程中的改变情况,见下表。

表3-5-2-1文件描述符初始值关闭文件描述符0后dup调用后0标准输入{已关闭}管道文件描述符1标准输出标准输出标准输出2标准错误输出标准错误输出标准错误输出3管道文件描述符管道文件描述符管道文件描述符        再回到之前的例子,我们将把子程序的stdin文件描述符替换为我们创建的管道的读取端,还将对文件描述符做一些清理,使得子程序可以正确检测到管道中数据的结束。

#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#include <sys/types.h>int main(){int data_processed;int fd[2];const char data[] = "I want to sleep";pid_t pid;if (pipe(fd) == 0){pid = fork();if (pid == (pid_t)-1){fprintf(stderr, "fork failed\n");exit(EXIT_FAILURE);}if (pid == (pid_t)0){close(0);dup(fd[0]);close(fd[0]);close(fd[1]);execlp("od", "od", "-c", NULL);exit(EXIT_SUCCESS);}else{close(fd[0]);data_processed = write(fd[1], data, strlen(data));close(fd[1]);printf("%d - wrote %d bytes\n", getpid(), data_processed);}}exit(EXIT_SUCCESS);}

        编译并运行:

$ ./pipe_dup
4779 - wrote 15 bytes
0000000   I       w   a   n   t       t   o       s   l   e   e   p
0000017

        与往常一样,这个程序创建一个管道,然后通过fork创建一个子进程。此时,父子进程都有可以访问管道的文件描述符,一个用于读数据,一个用于写数据,所以总共有4个打开的文件描述符。

        我们首先来看子进程。子进程先用close(0)关闭它的标准输入,然后调用dup(fd[0])把与管道的读取端关联的文件描述符复制为文件描述符0,即标准输入。接下来,子进程关闭原先的用来从管道读取数据的文件描述符fd[0]。因为子进程不会向管道写数据,所以它把与管道关联的写操作文件描述符fd[1]也关闭了。现在,它只有一个与管道关联的文件描述符,即文件描述符0,它的标准输入。

        接下来,子进程就可以用exec来启动任何从标准输入读取数据的程序了。在本例中,我们使用od命令。od命令将等待数据的到来,就好像它在等待来自用于终端的输入一样。事实上,如果没有明确使用检测这两者之间不同的特殊代码,它并不知道输入是来自一个管道,而不是来自一个终端。

        父进程首先关闭管道的读取端fd[0],因为它不会从管道读取数据。接着它向管道写入数据。当所有数据都写完以后,父进程关闭管道的写入端并退出。因为现在已没有打开的文件描述符可以向管道写数据了,od程序读取写到管道的15个字节数据后,后续的读操作将返回0字节,表示已到达文件尾。当读操作返回0时,od程序就退出运行。这类似于在终端上运行od命令,然后按下Ctrl+D组合键发送文件尾标志。


整理自 《Linux程序设计第4版》、《Linux C编程从初学到精通》。

0 0
原创粉丝点击