PIPE(无名管道)

管道是最初的 Unix IPC 形式,其局限在于其没有名字,只能由有亲缘关系的进程使用。PIPE 使用常用的 read 和 write 函数访问。所有的 Unix 都提供无名管道,提供一个单向数据流(半双工),但是有些版本的 Unix 提供全双工管道。

  1. #include <unistd.h>
  2. int pipe(int fd[2]); //返回值:成功为0,出错为-1

如下展示了单个进程中管道的模样,从图中可以看到,fd[0] 用于读,称为读端,fd[1] 用于写,称为写端,如下图所示:
image.png
管道由单个进程创建,却很少在单个进程中使用。最经典的用途就是在父子进程中提供通信手段。父进程调用 fork 派生出一个自身副本,如下图:
image.png
如果关闭父进程管道的读段,关闭子进程管道的写入端,就会在父子进程间提供了一个单向的数据流。
image.png
如果我们需要一个双向的数据流时,必须创建两个管道,每个方向上一个。如下步骤:

  1. 创建管道1(fd[0] 和 fd[1])和管道2(fd[0] 和 fd[1])。
  2. 调用 fork 函数,创建子进程。
  3. 父进程关闭管道 1 的读端 fd1[0]
  4. 父进程关闭管道 2 的写端 fd2[1]
  5. 子进程关闭管道 1 的写端 fd1[1]
  6. 子进程关闭管道 2 的读端 fd2[0]

那么最总的管道布局图如下:
image.png
那么,我们可以通过这种模型,实现父进程和子进程之间的通信的典型实例,主要功能是:客户端输入一个路径名,把它写入 IPC 通道,服务器从该通道读出路径名,并尝试打开文件读取,读取成功并写入IPC通道,否则响应一个出错消息。客户端再将消息打印出来,实例代码如下:

  1. #include <unistd.h>
  2. #include <stdio.h>
  3. #include <sys/wait.h>
  4. #include <sys/types.h>
  5. #include <stdlib.h>
  6. #include <string.h>
  7. #include <sys/stat.h>
  8. #include <fcntl.h>
  9. #define MAXLINE 128
  10. void client(int readfd, int writefd)
  11. {
  12. size_t len;
  13. ssize_t n;
  14. char buff[MAXLINE];
  15. fgets(buff, MAXLINE, stdin);
  16. len = strlen(buff);
  17. if (buff[len - 1] == '\n')
  18. len--;
  19. write(writefd, buff, len);
  20. while ((n = read(readfd, buff, MAXLINE)) > 0) //阻塞读
  21. {
  22. write(STDOUT_FILENO, buff, n);
  23. }
  24. }
  25. void server(int readfd, int writefd)
  26. {
  27. int fd;
  28. ssize_t n;
  29. char buff[MAXLINE + 1];
  30. if ((n = read(readfd, buff, MAXLINE)) == 0)
  31. {
  32. printf("EOF read pathname");
  33. exit(1);
  34. }
  35. buff[n] = '\0';
  36. if ((fd = open(buff, O_RDONLY)) < 0) //出错
  37. {
  38. snprintf(buff + n, sizeof(buff) - n, ":cant't open\n");
  39. n = strlen(buff);
  40. write(writefd, buff, n);
  41. }
  42. else
  43. {
  44. while ((n = read(fd, buff, MAXLINE)) > 0)
  45. {
  46. write(writefd, buff, n);
  47. }
  48. close(fd);
  49. }
  50. }
  51. int main()
  52. {
  53. int pipe1[2];
  54. int pipe2[2];
  55. pid_t childpid;
  56. //TODO:返回值判断
  57. pipe(pipe1);
  58. pipe(pipe2);
  59. if ((childpid = fork()) == 0) //子进程
  60. {
  61. close(pipe1[1]);
  62. close(pipe2[0]);
  63. server(pipe1[0], pipe2[1]); //充当服务器
  64. exit(0);
  65. }
  66. //父进程
  67. close(pipe1[0]);
  68. close(pipe2[1]);
  69. client(pipe2[0], pipe1[1]); //充当客户端
  70. waitpid(childpid, NULL, 0);
  71. exit(0);
  72. }

shell 中的管道原理

在上面已经对管道做了详细解释,那么 shell 中的管道是如何运作的呢?
假如在 shell 中输入一个像下面这种命令:

  1. $ who | sort | lp

该 shell 将执行上述步骤创建三个进程和其间的两个管道。还把每个管道的读出端复制到相应的进程的标准输入,把每个管道的写入端复制到相应进程的标准输出,就有了如下这样的管道线:
image.png

全双工管道

上面提到,某些 Unix 的 pipe 是提供全双工的,除此之外,许多内核都提供的 socketpair 函数也是全双工的。那么全双工管道到底提供什么?
首先看一下半双工管道:
image.png
再看全双工管道:
image.png
这里的全双工管道是由两个半双工管道构成的。写入 fd[1] 的数据只能从 fd[0] 读出,写 入 fd[0] 的数据只能从 fd[1] 读出。
可使用单个全双工管道完成双向通信,如下程序:

  1. #include <unistd.h>
  2. #include <string.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/wait.h>
  6. int main()
  7. {
  8. int fd[2];
  9. int n;
  10. char c;
  11. pid_t childpid;
  12. pipe(fd);
  13. if ((childpid = fork()) == 0) //子进程
  14. {
  15. sleep(1);
  16. if ((n = read(fd[0], &c, 1)) != 1)
  17. {
  18. printf("child:read returned %d", n);
  19. exit(1);
  20. }
  21. printf("child read:%c\n", c);
  22. write(fd[0], "c", 1);
  23. exit(0);
  24. }
  25. //父进程
  26. write(fd[1], "p", 1);
  27. sleep(2);
  28. if ((n = read(fd[1], &c, 1)) != 1)
  29. {
  30. printf("parent:read returned %d", n);
  31. exit(1);
  32. }
  33. printf("parent read:%c\n", c);
  34. waitpid(childpid, NULL, 0);
  35. exit(0);
  36. }

在某些 Unix 如 Solaris 2.6 上是全双工的,这份代码可以运行。
然而,遗憾的是,这份代码并不能在 Linux 上正常运行,因为 Linux 上的 pipe 只是半双工的。父进程试图在 fd[1] 端 read 会中止,子进程在 fd[0] 端 write 时中止,并出现错误。

popen/pclode 函数

popen 主要用于创建一个管道并启动另外一个进程,这个进程要么从该管道读出标准输入,要么从该管道写入标准输出。
pclose 主要用于关闭由 popen 创建的标准 I/O 流,等待其中的命令终止,然后返回 shell 的终止状态。

  1. #include <stdio.h>
  2. FILE *popen(const char *command, const char *type); //返回值:成功文件指针,出错NULL
  3. int pclose(FILE *stream); //返回成功则为 shell 终止状态,出错为-1

参数:
command:是一个 shell 命令。popen 在调用进程和所指定的命令之间建立一个管道,由 popen 返回的值是一个标准 I/O FILE 指针,该指针或者用于输入,或者用于输出,取决于 type。
type:如果为 r,调用进程读取 command 的标准输出;如果是 w,调用进程写到 command 的标准输入。
如下使用 popen 函数和 shell 的 cat 命令实例,cat 命令的输出被复制到标准输出。

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <string.h>
  4. #include <stdlib.h>
  5. #define MAXLINE 128
  6. int main()
  7. {
  8. size_t n;
  9. char buff[MAXLINE];
  10. char command[MAXLINE];
  11. FILE *fp;
  12. fgets(buff, MAXLINE, stdin); //read path name
  13. n = strlen(buff);
  14. if (buff[n - 1] == '\n')
  15. n--;
  16. snprintf(command, sizeof(command), "cat %s", buff);
  17. fp = popen(command, "r");
  18. while (fgets(buff, MAXLINE, fp) != NULL)
  19. {
  20. fputs(buff, stdout);
  21. }
  22. pclose(fp);
  23. exit(0);
  24. }

在 Linux 下运行结果:

  1. ./test
  2. /etc/shadow
  3. cat: /etc/shadow: Permission denied

虽然得到一个出错的信息,但是调用是成功的,因为 cat 将出错消息写到标准错误输出,调用进程读取到该输出,并打印出来。

FIFO(有名管道)

上面说到 pipe 没有名字,无法用于无亲缘关系的两个进程之间。而 FIFO 类似于管道,其全称 First in first out,是一个单向(半双工)数据流,但是每个 FIFO 都有一个路径名与之关联,也就支持了无亲缘关系的进程访问同一个 FIFO,而 FIFO 也随之被称为又名管道。
FIFO 由 makfifo 函数创建:

  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. int mkfifo(const char *pathname, mode_t mode); //返回值:成功0,出错-1

参数:
pathname:一个普通的 Unix 路径名,作为该 FIFO 的名字。
mode:指定文件权限位,类似于 open 的第二个参数,如下图:
image.png
FIFO 函数已经默认指定 O_CREAT | O_EXCL。所以如果创建一个所指定名字的 FIFO 已存在,返回一个 EEXIST 的错误。如果不希望创建一个新的 FIFO,就改为调用 open 而不是 mkfifo。
所以正确的做法就是:要打开一个已存在的 FIFO 或 创建一个新的 FIFO,应先调用 mkfifo,再检查它是否返回 EEXIST 错误,若返回该错误则改为调用 open。
我们也可以从 shell 命令创建 FIFO,使用 mkfifo 命令即可。
创建出一个 FIFO 后,必须打开来读或者写,所用的可以是 open 函数或者标准 I/O 函数,如 fopen。FIFO 不能打开来即读又写,因为它是半双工的。
对 pipe 或 FIFO 的 write 总是向末尾添加数据,read 总是从头返回数据。如果对 pipe 或 FIFO 调用 lseek,将返回 ESPIPE 错误。
现在重新使用 FIFO 编写上面的客户端-服务器的程序。使用两个 FIFO代替两个 PIPE。

  1. #include <sys/types.h>
  2. #include <errno.h>
  3. #include <fcntl.h>
  4. #include <limits.h>
  5. #include <signal.h>
  6. #include <stdio.h>
  7. #include <stdlib.h>
  8. #include <string.h>
  9. #include <sys/stat.h>
  10. #include <unistd.h>
  11. #include <sys/wait.h>
  12. #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
  13. #define FIFO1 "/tmp/fifo.1"
  14. #define FIFO2 "/tmp/fifo.2"
  15. #define MAXLINE 128
  16. void client(int readfd, int writefd)
  17. {
  18. size_t len;
  19. ssize_t n;
  20. char buff[MAXLINE];
  21. fgets(buff, MAXLINE, stdin); //读取输入的文件名
  22. len = strlen(buff);
  23. if (buff[len - 1] == '\n')
  24. len--;
  25. write(writefd, buff, len); //写入到管道中
  26. while ((n = read(readfd, buff, MAXLINE)) > 0) //从管道中读
  27. {
  28. write(STDOUT_FILENO, buff, n);
  29. }
  30. }
  31. void server(int readfd, int writefd)
  32. {
  33. int fd;
  34. ssize_t n;
  35. char buff[MAXLINE + 1];
  36. if ((n = read(readfd, buff, MAXLINE)) == 0) //服务器读取客户端管道中发来的文件名
  37. {
  38. printf("EOF read pathname");
  39. exit(1);
  40. }
  41. buff[n] = '\0';
  42. if ((fd = open(buff, O_RDONLY)) < 0) //打开文件
  43. {
  44. snprintf(buff + n, sizeof(buff) - n, ":cant't open\n");
  45. n = strlen(buff);
  46. write(writefd, buff, n);
  47. }
  48. else
  49. {
  50. while ((n = read(fd, buff, MAXLINE)) > 0)
  51. {
  52. write(writefd, buff, n); //读取内容写入管道中
  53. }
  54. close(fd);
  55. }
  56. }
  57. int main()
  58. {
  59. int readfd, writefd;
  60. pid_t childpid;
  61. if ((mkfifo(FIFO1, FILE_MODE) < 0) && (errno != EEXIST))
  62. printf("can't create %s\n", FIFO1);
  63. if ((mkfifo(FIFO2, FILE_MODE) < 0) && (errno != EEXIST))
  64. {
  65. unlink(FIFO1);
  66. printf("can't create %s\n", FIFO2);
  67. }
  68. if ((childpid = fork()) == 0) //子进程
  69. {
  70. readfd = open(FIFO1, O_RDONLY, 0);
  71. writefd = open(FIFO2, O_WRONLY, 0);
  72. server(readfd, writefd);
  73. exit(0);
  74. }
  75. //父进程
  76. writefd = open(FIFO1, O_WRONLY, 0);
  77. readfd = open(FIFO2, O_RDONLY, 0);
  78. client(readfd, writefd);
  79. waitpid(childpid, NULL, 0);
  80. close(readfd);
  81. close(writefd);
  82. unlink(FIFO1);
  83. unlink(FIFO2);
  84. exit(0);
  85. }

需要注意的是:创建打开一个 FIFO 则需要在调用 mkfifo 后再调用 open。管道在所有进程都关闭它之后自动消息。FIFO 的名字只有通过调用 unlink 才能从文件系统中删除。这样的好处就是:FIFO 在文件系统中有一个名字,该名字允许某个进程创建一个 FIFO,与它无关的另一个进程来打开这个 FIFO。
image.png
如果没有正确的使用 FIFO 会发生问题。比如我们将上面的父进程中两个 open 调用顺序互换,程序就不能工作,因为当没有任何进程打开某个 FIFO 来写,那么打开该 FIFO 来读的进程将阻塞。父子进程都将打开一个 FIFO 来读,但是当时并没有任何进程已打开该文件来写,父子进程将阻塞,形成死锁。
因为 FIFO 的 open 使用 O_RDONLY 标志将阻塞到另一个只写 O_WRONLY 标志打开该 FIFO 为止。
再来看一个无亲缘关系的进程的例子:
客户端:

  1. #include <sys/types.h>
  2. #include <errno.h>
  3. #include <fcntl.h>
  4. #include <limits.h>
  5. #include <signal.h>
  6. #include <stdio.h>
  7. #include <stdlib.h>
  8. #include <string.h>
  9. #include <sys/stat.h>
  10. #include <unistd.h>
  11. #include <sys/wait.h>
  12. #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
  13. #define FIFO1 "/tmp/fifo.1"
  14. #define FIFO2 "/tmp/fifo.2"
  15. #define MAXLINE 128
  16. void client(int readfd, int writefd)
  17. {
  18. size_t len;
  19. ssize_t n;
  20. char buff[MAXLINE];
  21. fgets(buff, MAXLINE, stdin);
  22. len = strlen(buff);
  23. if (buff[len - 1] == '\n')
  24. len--;
  25. write(writefd, buff, len);
  26. while ((n = read(readfd, buff, MAXLINE)) > 0) //阻塞读
  27. {
  28. write(STDOUT_FILENO, buff, n);
  29. }
  30. }
  31. int main()
  32. {
  33. int readfd, writefd;
  34. writefd = open(FIFO1, O_WRONLY, 0);
  35. readfd = open(FIFO2, O_RDONLY, 0);
  36. client(readfd, writefd);
  37. close(readfd);
  38. close(writefd);
  39. unlink(FIFO1);
  40. unlink(FIFO2);
  41. return 0;
  42. }

服务器:

  1. #include <sys/types.h>
  2. #include <errno.h>
  3. #include <fcntl.h>
  4. #include <limits.h>
  5. #include <signal.h>
  6. #include <stdio.h>
  7. #include <stdlib.h>
  8. #include <string.h>
  9. #include <sys/stat.h>
  10. #include <unistd.h>
  11. #include <sys/wait.h>
  12. #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
  13. #define FIFO1 "/tmp/fifo.1"
  14. #define FIFO2 "/tmp/fifo.2"
  15. #define MAXLINE 128
  16. void server(int readfd, int writefd)
  17. {
  18. int fd;
  19. ssize_t n;
  20. char buff[MAXLINE + 1];
  21. if ((n = read(readfd, buff, MAXLINE)) == 0)
  22. {
  23. printf("EOF read pathname");
  24. exit(1);
  25. }
  26. buff[n] = '\0';
  27. if ((fd = open(buff, O_RDONLY)) < 0) //出错
  28. {
  29. snprintf(buff + n, sizeof(buff) - n, ":cant't open\n");
  30. n = strlen(buff);
  31. write(writefd, buff, n);
  32. }
  33. else
  34. {
  35. while ((n = read(fd, buff, MAXLINE)) > 0)
  36. {
  37. write(writefd, buff, n);
  38. }
  39. close(fd);
  40. }
  41. }
  42. int main()
  43. {
  44. int readfd, writefd;
  45. if ((mkfifo(FIFO1, FILE_MODE) < 0) && (errno != EEXIST))
  46. printf("can't create %s\n", FIFO1);
  47. if ((mkfifo(FIFO2, FILE_MODE) < 0) && (errno != EEXIST))
  48. {
  49. unlink(FIFO1);
  50. printf("can't create %s\n", FIFO2);
  51. }
  52. readfd = open(FIFO1, O_RDONLY, 0);
  53. writefd = open(FIFO2, O_WRONLY, 0);
  54. server(readfd, writefd);
  55. return 0;
  56. }

PIPE 和 FIFO 的非阻塞属性

PIPE 和 FIFO 的打开、读入和写入还有一些属性,比如将管道设置为非阻塞。
(1)调用 open 时可指定 O_NONBLOCK 标志。如下:

  1. writefd = open(FIFO1, O_WRONLY | O_NONBLOCK, 0);

(2)若描述符已经打开,可以调用 fcntl 以启用 O_NONBLOCK 标志,在 PIPE 中来说,必须使用这种技术,因为其没有 open 调用。在使用 fcntl 时,先使用 F_GETFL 命令取得当前文件状态标志,将它和 O_NONBLOCK 标志按位或后,再使用 F_SETFL 命令存储这些文件标志。

  1. int flags;
  2. if ((flags = fcntl(fd, F_GETFL, 0)) < 0)
  3. err_sys("F_GETFL error.");
  4. flags |= O_NNONBLOCK;
  5. if (fcntl(fd, F_SETFL, flags) < 0)
  6. err_sys("F_SETFL error");

非阻塞标志对管道 PIPE 和 FIFO 的影响:
image.png
write 操作是原子性的,若有两个进程差不多同时向一个 PIPE 或者 FIFO 写,那么要么先写入来自第一个进程的所有数据,再写第二个进程的所有数据,要么颠倒,系统不会混杂写。O_NONBLOCK 标志对对于其原子性没有影响。
如果向一个没有为读打开着的 PIPE 或者 FIFO 写入,那么内核将产生一个 SIGPIPE 信号,若进程没有捕捉也没有忽略,默认行为将是终止进程。

PIPE 和 FIFO 的限制

系统加于 PIPE 和 FIFO 的唯一限制为:
OPEN_MAX 一个进程在任意时刻打开的最大描述符数(Posix 要求至少为 16)
PIPE_BUF 可原子地写往一个 PIEP 或 FIFO 的最大数据量(Posix 要求至少为512)。
OPEN_MAX 的值可通过调用 sysconf 函数查询。它通常可通过执行 ulimit 命令或 limit 命令从 shell 中修改。也可通过调用 setrlimit 函数从一个进程中修改。
PIPE_BUF 的值通常定义在头文件中,但是 Posix 认为它是一个路径名变量。意味着它的值可以随所指定的路径名而变化(只对 FIFO 而言, 因为 PIPE 没有名字),因为不同的路径名可以落在不同的文件系统上,而这些文件系统可能有不同的特征。于是 PIPE_BUF 的值可在运行时通过调用 pathconf 或 fpathconf 取得。如下实例:

  1. int main(int argc, char **argv)
  2. {
  3. if (argc != 2)
  4. {
  5. printf("usage: pipeconf <pathname>");
  6. exit(1);
  7. }
  8. printf("PIPE_BUF = %ld, OPEN_MAX = %ld\n",
  9. pathconf(argv[1], _PC_PIPE_BUF), sysconf(_SC_OPEN_MAX));
  10. exit(0);
  11. }