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