日常学习

fifo-pipe

February 05, 2019

The linux programming interface

管道, FIFO

管道是UNIX系统上最古老的一种IPC方法,可以用来在相关进程间传递数据。FIFO 则是 管道 概念的一个变体, 区别在于 FIFO可以用于 任意进程间的通信

管道

  1. shell命令中使用管道最为常见。比如 ls | wc -l, 示意图如下: 为执行该命令, shell创建了两个进程来分别执行ls、wc

  2. 管道的几个特征:
    1. 一个管道是一个字节流 (不存在消息边界的概念,读取进程可以读取任意大小的数据块,数据是顺序的、但是管道中没有离散消息的概念)
    2. 从管道中读取数据: 试图从一个当前为空的 管道中 读取数据将会被阻塞 直到至少 有一个字节被写入到管道中为止,写入端被关闭后,在读取完 所有数据之后 会读取到 文件结束
    3. 单向的: 数据传递的方向是单向的,一端为读写 一端为写入 (一些UNIX 的实现上 管道 是双向的,但是应该避免依赖这种语义,使用UNIX domain socket 来代替)
    4. 可以保证 写入不超过PIPE_BUF 字节的操作是原子的: 单个进程写入管道 无需关心PIPE_BUF的取值,多个进程写入同一个管道在一时刻写入的数据量超过PIPE_BUF字节大小时,那么就可以保证写入的数据不会出现 互相混合的情况。(多个写入进程,那么大的数据块写入可能会被拆分成 任意大小的数据段,导致会出现与其他进程 写入的数据交叉的现象, 因为并没有 原子性的写入)
    5. 管道的容量是有限的: 管道是内核内存中维护的一个缓冲器,这个缓冲器的存储能力是有限的。管道被填满之后,写入的操作会被阻塞知道读取操作从缓冲器中移除一些数据为止。(一个应用程序无需知道 管道的实际存储能力,使用较大的缓冲器会提升效率,每当缓冲器被充满时,内核需要执行调度来消耗管道中的数据,较大的缓冲器意味着更少的调度、进程上下文切换的需要)
  3. 创建使用管道:int pipe(int fields[2]), 成功会在数组中 filedes 中返回两个打开的文件描述符,0 为读端, 1 为写端,与所有 文件描述符一样,可以使用read、write 系统调用在管道上执行IO,也可以使用stdio 函数等(但是需要首先fdopen获取一个fileds文件描述符对应的文件流)常见的使用场景如下:
  4. 父进程同子进程从同一个管道 中读取和写入数据 的这种做法并不常见,因为会产生竞争 (当然 阻塞IO 的系统调用是一个原子操作,不会产生数据错乱,但是竞争条件是在所难免的,非阻塞IO中 则不容易判断 IO数据到底归属于那个进程)一个更简单的方法 则是: 创建两个管道,在两个进程间发送数据的两个方向上各使用一个,但是这种继续需要避免死锁问题(阻塞IO需要,因为两个端同时等待 IO 读取,而无法写入)
  5. 允许相关进程间的通信:管道可以在任意两个相关进程之间 通信,在 fork创建新的子进程 之前, 父进程创建管道 在 子进程之间使用。 shell所做的工作就是 父进程创建管道, 然后创建子进程,并在子进程中使用。(这里的相关进程存在一个特殊情况,即允许UNIX doamin 传递 文件描述符给其他的进程)
  6. 关闭未使用的管道文件描述符: 对于正确使用管道是非常重要的一环。读取进程需要关闭写入描述符,这样当写入进程完成输出并关闭写入描述符时候,读进程才能够看到读取到文件结尾。(也就是说在 文件结尾 的情况是 文件描述符完全被关闭?验证的逻辑的话: 需要3个进程,两个写入,一个读取, 读取一直读, 写入分别 间隔时间关闭, 然后观察情况,文件并不适合这种情况, 文件没有读写端的概念)写入进程关闭其持有的读取描述符的原因在于: 进程对一个没有 读取描述符 的管道 写入时候,内核会发送SIGPIPE信号,返回EPIPE错误(已损坏的管道),而这些是对进程有用的,为了保证 写入进程 能够收到这些消息、避免错误,所以需要主动关闭。其他的原因在于,只有写入进程持有读取描述符, 导致 没有接收端, 但是依然可以写入,最后管道缓冲区被填满,导致永远阻塞。
  7. 使用管道连接 过滤器: 管道被创建后, 为 读写端 分配的文件描述符是可用的描述符中数值最小的两个, 过滤器 约束规则是 从stdin 读取,写入到 stdout。所以shell需要保证 执行过滤器的 进程 标准输入、输出为 管道的读写端, 这里需要用到dup、dup2 系统调用
  8. popen()/ pclose(): 用于 执行一个shell命令并读取其输出或发送一些输入, 其大概是示意图如下: images
    fifo
  9. stdio 缓冲问题: popen调用返回的文件流指针 没有引用一个终端。 stdio库会对这种文件流应用块缓冲。意味着mode 为w时候 popen的调用,只有当stdio缓冲器被充满或者使用 pclose 关闭管道 之后才会将输出发送到另一端。如果要求子进程能够 从管道中立即收到数据,那么需要手动fflush,或者setbuf(fp, NULL) 禁用stdio缓冲来做到,pipe的创建的管道 同样。popen进程使用r调用时候,那么就需要子进程的编码中调用fflush, setbuf 来保证父进程 及时收到消息。(如果无法修改源代码, 可以使用伪终端来替换管道, 对进程来说是一个终端,stdio库 会逐行输出缓冲器的数据)

    FIFO

  10. 与管道类似, 最大的差别在于, FIFO在文件系统中存在一个名称(也被称为 命名管道 ), 并且与打开一个普通文件的方式是一样的,这样就可以用于非相关进程之间的通信,
  11. FIFO也有一个写入端和读取端, 并且从管道中 读取数据的顺序与写入的顺序是一样的,先入先出,所以被称为FIFO
  12. mkfifo [-m mode] pathname (shell命令), int mkfifo(const char * pathnamne, mode_t mode); (系统调用) 其中mode为权限(同chmod) ls -\l FIFO文件的类型为p, ls -F 在路径名后面附加一个 | 符号
  13. FIFO 被创建之后, 任何进程都可以打开它,只要符合权限 限制即可
  14. 推荐FIFO的使用方法: 一个写端,一个读端, 打开一个FIFO会同步读取进程和写入进程,即: 打开FIFO 以读取方式 会阻塞到 一个以写入方式 的打开 的产生, 以写入方式的打开 会阻塞到 一个 以读取方式的产生 (O_RDWR mode 打开会绕开 FIFO 的阻塞行为, 因为其不具有可移植性,开发人员应该避免使用, 而是应该是用O_NONBLOCK的方式进行)
  15. 使用 FIFO, tee 创建双重管道: shell管道线是线性的,每个进程读取前一个进程的产生的输出数据,然后产生输出数据,成为下一个进程的输入数据。使用FIFO就能创建新的管道线,需要使用到tee, tee 将输入数据复制成两份 分别输出, 将tee的一分输出 输入到FIFO中就可以产生两条管道线。 下面为大概的演示的命令行:具体的 tee的 参数参考
      mkfifo myfifo
      wc -l < myfifo &
      ls -l | tee myfifo | sort -n
    

    fifo_tee