第44章 管道和FIFO

概述

管道是一个字节流

没有消息边界概念,可以读取任意大小数据,通过管道传递的数据时顺序的,读出来的字节顺序与写入的顺序一致,无法使用lseek随机访问

管道是单向的

在管道中数据的传递方式是单向的,一端用于写入,一端用于读取

管道的容量是有限的

管道其实是内核内存中维护的缓冲器,其存储能力有限

从管道读取数据

管道为空,则读取一直阻塞直到有数据写入,如管道的写入端关闭了,读取剩余数据后文件会结束(read返回0)

向管道写入数据

如果多个进程同时写入同一个管道,如果同一时刻写入的数据量不超过PIPE_BUF字节,可以确保写入的数据不会发生混合的情况,SUSv3要求PIPE_BUF至少为_POSIX_PIPE_BUF,可通过fpathconf(fd, _PC_PIPE_BUF)确认,如果写入数据量大于PIPE_BUF,可能与其他进程写入的数据交叉;当只有一个进程写入数据时,该取值就无更重要了

创建和使用管道

  1. #include <unistd.h>
  2. int pipe(int fd[2]);
  3. // 若成功,返回0,若出错,返回-1
  4. // 若成功,fd[0]:读取端,fd[1]:写入端
  5. #define _GNU_SOURCE /* See feature_test_macros(7) */
  6. #include <fcntl.h> /* Obtain O_* constant definitions */
  7. #include <unistd.h>
  8. int pipe2(int pipefd[2], int flags);
  9. // 若成功,返回0,若出错,返回-1
  10. // 支持三个标记:O_CLOEXEC,O_DIRECT,O_NONBLOCK

管道在单个进程中用途不多,父子进程同时从管道读取和写入的做法也不常见(简单的做法是创建两个管道,不过要考虑死锁情形);通常是fork之后,一个进程关闭读端,一个进程关闭写端

管道允许相关进程间的通信

相关:指的是这两个进程具有相同的祖先
例外:通过UNIX domain socket传递管道的文件描述符

关闭管道未使用的文件描述符

读取数据的进程应该关闭管道的写端,这样当其他进程完成输出关闭写端时,读取数据的进程就能知道文件结束(read返回0),否则read会一直阻塞,因为内核知道管道的写端文件描述符依然打开着
写入数据的进程应该关闭管道的读端,是处于不同的原因,如果未关闭,即使在其他进程已经关闭了管道的读端,写入数据依然能够成功,最后写入进程会将数据充满整个管道,后续写入请求被永远阻塞
只有当所有引用管道的文件描述符被关闭之后,才会销毁该管道占用的资源

用管道进行进程同步

  1. 父进程创建子进程之前创建管道
  2. 每个子进程继承管道的写端文件描述符,在完成动作之后将其关闭
  3. 所有子进程关闭写端的文件描述符之后,父进程read返回0,表示子进程都已经结束

    用管道连接过滤器

    使用管道连接两个过滤器(从stdin读取和写入到stdout的程序)使得一个程序的标准输出被定向到管道中,而另一个程序的标准输入则从管道读取的方法:复制文件描述符 ``` if (fd[1] != STDOUT_FILENO) { dup2(fd[1], STDOUT_FILENO); close(fd[1]); }
  1. ### 用管道与shell命令进行通信

include

FILE popen(const char cmd, const char *mode); // 若成功,返回文件流指针,若出错,返回NULL // 创建一个管道,再创建一个子进程来执行shell,而shell又创建一个子进程来执行cmd字符串 // mode的取值: r:以读方式打开,cmd标准输出写入管道,调用进程从管道读取 w:以写方式打开,cmd标准输入读取管道,调用进程向管道写入

int pclose(FILE *fp); // 若成功,返回子进程的终止状态,若出错,返回-1

  1. popenpipe一样,从管道读取时,若调用进程在cmd关闭写端,则read返回0,向管道写入时,若cmd关闭读端,则调用进程收到SIGPIPE信号及EPIPE错误;一旦IO结束,应该使用pclose关闭管道并等待子进程中shell的终止(不应该使用fclose,它不会等待子进程)<br />有关system的规范适用于popen,使用popen更加方便,它会构建管道、执行文件描述符、关闭未使用的描述符并处理forkexec的所有细节,这种便捷性牺牲的是效率,与system一样,特权进程中永远不该使用popen;调用systemshell命令被封装在单个函数,而调用popen时,调用进程与shell命令并行运行,然后调用pclose,具体的细节差异:
  2. - popen不忽略SIGINTSIGQUIT信号,如果这些信号从键盘产生,它们会被发送到调用进程和被执行的命令中
  3. - 调用进程在popenpclose之间可能创建子进程,SUSv3要求popen不能阻塞SIGCHLD信号
  4. ### 管道和stdio缓冲
  5. popen返回的文件流没有引用一个终端,stdio库对这种文件流应用块缓冲,即以w打开popen时,默认只有sdio缓冲器满或pclose关闭了管道之后输出才被发送到另一个进程,如果要立刻接收数据,需要定期调用fflush或使用setbuf(fp, NULL)禁用stdio缓冲
  6. ### FIFO
  7. FIFO类似,差别在于FIFO在文件系统中拥有一个名称,使得其打开方式与普通文件类似,这样能够将FIFO用于非相关进程之间的通信,FIFO也有一个读端和写端,且从管道中读取数据的顺序与写入的顺序一致,FIFO的名称来源于此,也称为命名管道,shell 中使用mkfifo创建一个FIFO

mkfifo test ls -al test prw-r—r— 1 sky staff 0 1 30 15:52 test // p表示文件类型是FIFO ls -F test test| // | 表示管道符

  1. ```
  2. #include <sys/stat.h>
  3. int mkfifo(const char *pathname, mode_t mode);
  4. // 若成功,返回0,若出错,返回-1
  5. // 一旦FIFO被创建,任何进程都可以打开,只要能够通过常规的文件权限检测
  6. // SUSv3明确指出以O_RDWR方式打开一个FIFO的结果是未知的

shell管道线的一个特征是线性的,每个进程读取前一个进程的输出并发送到下一个进程的中,使用FIFO能在管道中创建子进程,这样除了将一个进程的输出发送到下一个进程外,还可以复制进程的输出并发送到另一个进程,完成这个任务需要tee命令:它将从标准输入读取的数据复制两份,一份写到标准输出,一份写入到指定的文件

  1. wc -l < test & // 从管道文件中读取数据,如果没有数据则一直阻塞
  2. [1] 31130
  3. ls -l | tee test | sort -k5n // 将ls -l的输出写到管道文件,并传递给sort
  4. prw-r--r-- 1 sky staff 0 1 30 15:52 744
  5. prw-r--r-- 1 sky staff 0 1 30 15:52 test
  6. total 16
  7. -rw-r--r--@ 1 sky staff 6456 1 30 15:54 README.md
  8. [1]+ Stopped wc -l < test // 从管道文件读取数据完毕,进程结束

使用管道实现一个客户端/服务器程序

客户端命名的方式:

  • 客户端生成自己的FIFO路径名,将其作为请求消息的一部分发送给服务器
  • 客户端和服务器约定一个构件客户端FIFO路径名的规则,客户端可以将构建自己路径名的信息作为消息的一部分发送给服务器

字节流的分割方式:

  • 每条消息使用诸如换行符之类的分隔符结束
  • 每条消息包含一个固定大小的头,头中包含一个字段,表示剩余消息的长度
  • 使用固定长度的消息

    非阻塞IO

    打开FIFO时使用O_NONBLOCK标记的目的:

  • 允许单个进程打开一个FIFO的两端

  • 防止打开两个FIFO的进程之间产生死锁

可以使用fcntl启用或禁用O_NONBLOCK标记