管道(pipe)是进程间通讯的一种实现方式。在 Linux 系统中,管道本质上是一种特殊的文件,它的主要用途是实现进程间的通讯。文中演示所用环境为 Ubuntu 18.04 desktop。shell
在 shell 中执行下面的命令:编程
$ echo abc | cat
echo 命令的输出经过管道做为了 cat 命令的输入。这里面的具体操做是由 shell 程序完成的。数组
管道的一个显著特色是:建立一个管道后,会得到两个文件描述符,分别用于对管道进行读取和写入操做。一般将这两个文件描述符称为管道的读取端和写入端,从写入端写入管道的任何数据均可以从读取端读取。
对一个进程来讲,管道的写入和读取操做与写入和读取一个普通文件没有什么区别,只是在内核中经过这种机制来实现进程间的通讯而已。数据结构
建立管道使用的系统调用的函数声明以下:函数
#include <unistd.h> int pipe(int filedescriptors[2]);
pipe 函数相对来讲是一个比较底层的函数,它建立一个管道(相对于命名管道而言,这个管道又被称为匿名管道)。参数 filedescriptors 是一个长度为 2 的整型数组,用于存放调用该函数所建立管道的文件描述符。其中 filedescriptors[0] 存放管道读取端的文件描述符,filedescriptors[1] 存放管道写入端的文件描述符。调用成功时,返回值为 0;调用失败时,返回值为 -1。
由此可知,管道自己是一个抽象的概念,其本质是经过对特殊文件的读写实现进程间的通讯。一个管道实际上就是个只存在于内存中的文件,对这个文件的操做要经过两个文件描述符进行,它们分别表明管道的两端。所以管道是一种特殊的文件,它不属于某一种文件系统,而是一种独立的文件系统,有其本身的数据结构。spa
调用 pipe 函数建立了一个管道后,还不能实现经过管道在两个进程间通讯的目的,由于此时管道的读取端和写入端的文件描述符都属于同一个进程。咱们知道,在 Linux 系统中,经过 fork 系统调用建立子进程时,父进程中打开的文件描述符仍将保持打开状态。因此,常见的作法是:先在父进程中建立管道,而后经过 fork 调用建立子进程,这时就能够经过管道在父子进程间传递数据了。
实际的使用中,经常在子进程中调用 exec 族函数执行特定的程序,而后根据数据传输的方向分别关闭父进程和子进程中的一个文件描述符(注意,此时只能单向传输数据。若是要双向传输数据,最好是建立两个单向传输的管道)。例如:要实现父进程向子进程传输数据,则关闭父进程中的读取端(filedescriptors[0])文件描述符和子进程中的写入端文件描述符(filedescriptors[1])。设计
下面的 demo 演示了如何在父子进程和子进程之间创建管道:code
#include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main(void) { int file_pipes[2]; pid_t fork_result; if(pipe(file_pipes) == 0) { fork_result = fork(); if(fork_result == -1) { fprintf(stderr, "Fork failure"); exit(EXIT_FAILURE); } if(fork_result == 0) { close(file_pipes[1]); exit(EXIT_SUCCESS); } else { close(file_pipes[0]); } } exit(EXIT_SUCCESS); }
上面的代码建立了一个由父进程向子进程传输数据的管道。blog
因为管道是一种特殊的文件,用户在使用中彻底能够像读写普通文件同样对管道进行读写,所使用的函数为 read 和 write。
系统定义的常数 PIPE_BUF 规定了管道缓冲区的大小,当写入数据超过规定的大小时,就会发生数据错乱。
虽然管道是一种特殊的文件,它的读写操做和普通文件的读写操做也彻底相同,但管道不是一个真实存在的文件,它只在内核中存储,而不存在于文件系统中。进程
下面是扩展后的 demo,演示了如何在父子进程和子进程之间创建管道并传输数据:
#include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main(void) { int data_processed; int file_pipes[2]; const char some_data[] = "123"; char buffer[BUFSIZ + 1]; pid_t fork_result; memset(buffer, '\0', sizeof(buffer)); if(pipe(file_pipes) == 0) { fork_result = fork(); if(fork_result == -1) { fprintf(stderr, "Fork failure"); exit(EXIT_FAILURE); } if(fork_result == 0) { close(file_pipes[1]); data_processed = read(file_pipes[0], buffer, BUFSIZ); printf("Read %d bytes: %s\n", data_processed, buffer); exit(EXIT_SUCCESS); } else { close(file_pipes[0]); data_processed = write(file_pipes[1], some_data, strlen(some_data)); printf("Write %d bytes\n", data_processed); } } exit(EXIT_SUCCESS); }
把上面的代码保存到文件 pipedemo.c 中,并编译:
$ gcc -Wall pipedemo.c -o pipe_demo
而后运行程序:
$ ./pipe_demo
这个程序先调用 pipe 函数建立一个管道,接着调用 fork 函数建立出一个子进程。若是 fork 函数调用成功,父进程就向管道中写入数据,而子进程则从管道中读取数据。父子进程都在只调用了一次 write 或 read 以后退出。
管道虽然被普遍使用,可是也有其局限性。管道的最大特色就是要求进程之间具备同源性,即它们必须是最终由同一个进程所派生的进程。固然这个缺点能够经过命名管道解决。
管道的另外一个缺点是半双工的工做方式,即只容许单向传输数据。这一点管道和命名管道是相同的,能够建立两个单向管道来实现数据的双向传输。
参考:
《Linux 程序设计》
《Linux 环境下 C 编程指南》