☰
FIFO命名管道进程间通信:从使用到内核原理与C语言实战
2026/10/1 18:12:21 网站建设 项目流程

如果面试官让你说一种最简单、也最能看出“内核功底”的进程间通信方式,FIFO 基本是必选项。它不需要共享内存那样手动管理锁,不需要 socket 那样先建立连接,两个没有父子关系的进程,只要约定一个文件路径,就能把数据流对接起来。

这次我们从使用方式、阻塞规则、C 代码实操再到内核源码路径,把 FIFO(命名管道)进程间通信完整过一遍。整体按“能跑通 + 能排查 + 能说清原理”的顺序展开,最后给出一份可以直接套用的排错清单。适合正在学 Linux 系统编程的开发者、准备面试的人,以及在嵌入式或服务端项目里需要轻量级本地 IPC 的工程师。

先说一个直接结论:FIFO 适合在一台机器上,让两个没有血缘关系的进程传递字节流。它比匿名管道灵活,比 socket 轻量,比共享内存安全边界更清晰。下面从核心能力开始。

1. FIFO 进程间通信核心能力速览

1.1 FIFO 是什么

FIFO 全称 First In First Out,也叫命名管道(Named Pipe)。在 Linux 文件系统里,它是一个类型为p的特殊文件节点。进程对这个节点做open、read、write、close,数据就会在内核管道缓冲区里流动。

FIFO 和匿名管道pipe()最大的区别是:匿名管道只能在有亲缘关系的进程之间传递,比如父子进程;FIFO 有路径名,任何进程只要知道路径并且有权限,就能打开它参与通信。

1.2 核心能力表

能力项说明
类型命名管道,内核缓冲区实现的进程间通信
使用接口mkfifo / open / read / write / close / unlink
是否要求父子进程关系不要求,任意进程通过路径名访问
数据流向单向流,字节流式读取,无消息边界
阻塞行为默认阻塞,可切换 O_NONBLOCK
原子性单次写入不超过 PIPE_BUF 时原子
内核依赖inode + pipe_inode_info + pipe_buffer 数组
持久化FIFO 节点存在于文件系统,数据只在内核内存中流动
依赖外部库无,Linux 内核自带支持
适合场景同机进程任务分发、日志采集、模块间解耦
平台支持情况
Linux原生支持
Windows可通过 WSL、虚拟机或 MSYS 环境验证,原理一致
macOS / BSD支持 POSIX FIFO,行为基本相同

这里要强调:FIFO 节点虽然显示在文件系统里,但读写的数据不会写到磁盘,而是存放在内核页缓存构成的管道缓冲区中。所以它适合做高频短消息流转,不适合当持久化存储使用。

2. FIFO 的基本模型:从管道到命名管道

2.1 匿名管道和命名管道的区别

匿名管道由pipe()系统调用创建,内核返回两个文件描述符,一个读端一个写端。调用fork()之后,父子进程各自继承两个描述符,再各自关闭不需要的一端,形成单向数据流。

#include <unistd.h> int pipefd[2]; pipe(pipefd); // pipefd[0] 读端,pipefd[1] 写端

这个过程的问题在于:pipe()返回的描述符没有路径,其他进程无法通过文件系统找到它。两个没有共同祖先的进程,拿不到这对描述符,也就无法通信。

FIFO 解决的核心问题就是“路径可见性”。它先通过mkfifo在文件系统里创建一个节点,之后任意进程都能用open()打开。这相当于把原来只有父子进程能用的一对隐藏描述符,变成了一扇有门牌的公共大门。

2.2 FIFO 数据流模型

用一张图理解:

+----------------+ write() +-----------------------+ | 进程 A | -------------------> | | | (生产者) | | FIFO 文件节点 | +----------------+ | /tmp/my_fifo | | 关联内核管道缓冲区 | +----------------+ | | | 进程 B | <------------------- | | | (消费者) | read() +-----------------------+ +----------------+

进程 A 打开 FIFO 的写端,进程 B 打开 FIFO 的读端。A 写入的数据先进入内核管道缓冲区,B 通过read()读走。数据在内核空间走一圈,不经过磁盘,也不经过网络协议栈。

注意一个关键字:单向。FIFO 本身是单向数据流。如果两个进程需要互发数据,通常要建两个 FIFO,一个方向一个。

3. 环境准备与前置条件

FIFO 是 Linux 内核自带机制,不需要安装额外动态库。只要是一个正常工作的 Linux 发行版,都能直接用。

操作系统层面,建议准备:

  • Linux 发行版,例如 Ubuntu、Debian、CentOS、openEuler 等。
  • Windows 环境可以用 WSL 或者虚拟机跑,命令和编译过程一致。
  • 内核版本没有硬性要求,常见发行版默认内核均可。

编译工具链方面,需要有gcc和标准 C 库头文件。先检查一下:

gcc --version uname -a ls -l /usr/include/fcntl.h /usr/include/sys/stat.h

如果gcc不存在,安装即可:

# Debian / Ubuntu sudo apt update sudo apt install -y build-essential # RHEL / CentOS sudo yum install -y gcc glibc-devel

准备一个干净的工作目录:

mkdir -p ~/fifo-lab cd ~/fifo-lab

后续的源码文件和 FIFO 节点都放在这个目录里。注意:不要直接在产品环境的全局临时目录里反复创建和删除 FIFO,容易造成路径冲突。

4. 创建 FIFO:命令与系统调用

4.1 使用 mkfifo 命令

Linux 自带mkfifo命令,一行命令创建 FIFO 节点:

mkfifo /tmp/my_fifo ls -l /tmp/my_fifo

输出类似:

prw-r--r-- 1 user user 0 Jun 10 10:00 /tmp/my_fifo

看第一个字符,p表示这是一个管道(pipe)文件。普通文件的第一个字符是-,目录是d。文件大小显示为 0,因为数据不在文件节点里存放。

如果要指定权限,可以在后面加 mode:

mkfifo -m 0644 /tmp/my_fifo

这个 mode 会受进程umask影响。如果 umask 是 0022,那么0644创建出来的实际权限是0644 & ~0022,也就是0644。可以用umask命令查看当前值:

umask

4.2 使用 mkfifo() 系统调用

在 C 代码里创建 FIFO,用mkfifo()函数:

#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #define FIFO_PATH "/tmp/my_fifo" int main(void) { if (mkfifo(FIFO_PATH, 0644) < 0) { perror("mkfifo"); return 1; } printf("create fifo: %s\n", FIFO_PATH); return 0; }

编译运行:

gcc -Wall -o create_fifo create_fifo.c ./create_fifo ls -l /tmp/my_fifo

注意:第二次运行时,因为/tmp/my_fifo已经存在,mkfifo()会返回 -1,errno设置为EEXIST。实际工程里,要么先unlink,要么处理EEXIST情况。

mkfifo底层最终会走到内核的mknod相关路径,现代内核对应的是mknodat系统调用。glibc 的mkfifo()封装了这层逻辑,把S_IFIFO和传入的权限位拼到一起,交给内核创建节点。

4.3 如何确认创建成功

可以通过stat命令确认文件类型:

stat /tmp/my_fifo

输出里有File: /tmp/my_fifo,类型字段会显示fifo。

也可以通过 C 代码里的stat()判断:

struct stat st; stat(FIFO_PATH, &st); if (S_ISFIFO(st.st_mode)) { printf("it is a fifo\n"); }

5. FIFO 读写规则与阻塞行为

这是 FIFO 最容易出错的部分。很多进程卡住,问题都出在打开和读写的阻塞规则上。下面分阶段拆开讲。

5.1 打开阶段的阻塞

FIFO 的open()行为,取决于打开方向、是否设置O_NONBLOCK、以及对端是否存在。

打开方式没有对端进程时的行为
open(path, O_RDONLY)默认阻塞,直到有写者打开该 FIFO
open(path, O_WRONLY)默认阻塞,直到有读者打开该 FIFO
open(path, O_RDONLY | O_NONBLOCK)不阻塞,立即成功返回
open(path, O_WRONLY | O_NONBLOCK)如果没有读者,open 失败,errno 为 ENXIO

这个规则非常关键。比如你只启动写进程:

./writer

如果没有任何读者打开 FIFO,writer 会一直卡在open()里。这不是程序死锁,而是 FIFO 设计上要求“读端和写端都就位,才开始通信”。

所以调试的时候,先开一个终端启动 reader,再开另一个终端启动 writer,这是最稳的顺序。

5.2 读写阶段的阻塞

打开成功之后,读写阶段的阻塞规则:

  • read():管道为空且仍有写端打开时,阻塞等待数据。
  • read():所有写端关闭后,返回 0,表示 EOF。
  • write():管道缓冲区满时,阻塞等待读者消费。
  • write():没有任何读者打开时,进程收到SIGPIPE信号,默认终止进程;如果用signal(SIGPIPE, SIG_IGN)忽略信号,write()返回 -1,errno为EPIPE。

管道的缓冲区大小通常有限。很多发行版上默认是 64KB,具体以当前内核配置为准:

cat /proc/sys/fs/pipe-max-size

5.3 原子性与 PIPE_BUF

FIFO 不是消息队列,它是字节流。两个写者同时向同一个 FIFO 写数据,数据可能交错。

但 POSIX 保证:只要单次写入长度不超过PIPE_BUF,这次写入就是原子的。在 Linux 上,PIPE_BUF通常为 4096 字节。可以查看系统定义:

getconf PIPE_BUF /tmp/my_fifo
写入方式原子性
单次写入 <= PIPE_BUF原子,不会与其它写者交错
单次写入 > PIPE_BUF可能与其他写者写入的数据交错

这意味着:如果多个进程并发往同一个 FIFO 写消息,每条消息必须小于等于 4096 字节,才能保证消息不会互相穿插。更长的数据,建议加锁,或者使用单独的 FIFO。

5.4 非阻塞模式

非阻塞模式的用途是:进程不想干等对端出现,或者需要把 FIFO 接进poll/epoll事件循环。

int fd; int flags; fd = open(FIFO_PATH, O_RDONLY | O_NONBLOCK); if (fd < 0) { perror("open nonblock"); return -1; } // 也可以打开后再设置非阻塞 flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK);

非阻塞模式下:

  • 读端没有数据时,read()返回 -1,errno为EAGAIN。
  • 写端缓冲区满时,write()返回 -1,errno为EAGAIN。

注意这里要区分ENXIO和EAGAIN。ENXIO出现在打开阶段,表示“对方不存在”;EAGAIN出现在读写阶段,表示“资源暂时不可用,再试一次”。

6. C 语言源码实操

下面用完整的可编译代码,演示单向通信、双向通信和非阻塞读取。所有源码都放在~/fifo-lab里。

6.1 单向通信:写端与读端

写端程序writer.c:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #define FIFO_PATH "/tmp/my_fifo" int main(void) { int fd; fd = open(FIFO_PATH, O_WRONLY); if (fd < 0) { perror("open"); exit(EXIT_FAILURE); } const char *msg = "hello fifo"; if (write(fd, msg, strlen(msg)) < 0) { perror("write"); close(fd); exit(EXIT_FAILURE); } printf("writer send: %s\n", msg); close(fd); return 0; }

读端程序reader.c:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #define FIFO_PATH "/tmp/my_fifo" int main(void) { int fd; char buf[1024]; ssize_t n; fd = open(FIFO_PATH, O_RDONLY); if (fd < 0) { perror("open"); exit(EXIT_FAILURE); } n = read(fd, buf, sizeof(buf) - 1); if (n < 0) { perror("read"); close(fd); exit(EXIT_FAILURE); } buf[n] = '\0'; printf("reader recv: %s\n", buf); close(fd); return 0; }

6.2 编译与运行

gcc -Wall -o writer writer.c gcc -Wall -o reader reader.c

先创建 FIFO:

mkfifo /tmp/my_fifo

接着打开终端一,启动读端:

cd ~/fifo-lab ./reader

此时 reader 会卡在open(),因为还没有写者。这时不要急着关,打开终端二,启动写端:

cd ~/fifo-lab ./writer

回车之后,writer 输出:

writer send: hello fifo

reader 输出:

reader recv: hello fifo

然后两个进程都退出。这就是 FIFO 最基础的“先读后写”流程。

如果反过来,先启动 writer,writer 会一直卡在open(FIFO_PATH, O_WRONLY)。这是预期行为,不是 bug。

6.3 双向通信:双 FIFO 方案

FIFO 是单向的。实现两个进程互发数据,最干净的做法是建两个 FIFO,一个方向一个。

#define FIFO_A2B "/tmp/fifo_a2b" // 进程 A 向进程 B 发数据 #define FIFO_B2A "/tmp/fifo_b2a" // 进程 B 向进程 A 发数据

进程 A:

int fd_write = open(FIFO_A2B, O_WRONLY); int fd_read = open(FIFO_B2A, O_RDONLY);

进程 B:

int fd_read = open(FIFO_A2B, O_RDONLY); int fd_write = open(FIFO_B2A, O_WRONLY);

这里最容易踩的坑是打开顺序。两个进程如果都先等对方出现,可能互相卡死。比如进程 A 先执行open(FIFO_A2B, O_WRONLY),会等待 B 打开FIFO_A2B的读端;如果 B 还没执行到这一步,A 就卡住,后面的open(FIFO_B2A, O_RDONLY)永远不会执行。

实际操作里,建议统一约定:先打开读端,再打开写端。或者用非阻塞方式打开写端,失败就重试。还有一种取巧方式是open(path, O_RDWR),但这种方式对 FIFO 的阻塞语义在不同平台上有差异,测试环境能用,生产环境不建议依赖它。

6.4 非阻塞读取与 poll 超时

服务端程序经常需要做超时控制,不能无限阻塞。一个常见模式是 O_NONBLOCK + poll。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <poll.h> #include <errno.h> #include <sys/types.h> #include <sys/stat.h> #define FIFO_PATH "/tmp/my_fifo" int main(void) { int fd; struct pollfd pfd; char buf[1024]; ssize_t n; fd = open(FIFO_PATH, O_RDONLY | O_NONBLOCK); if (fd < 0) { perror("open"); exit(EXIT_FAILURE); } pfd.fd = fd; pfd.events = POLLIN; int ret = poll(&pfd, 1, 5000); // 5 秒超时 if (ret < 0) { perror("poll"); close(fd); exit(EXIT_FAILURE); } if (ret == 0) { printf("timeout, no data\n"); close(fd); return 0; } if (pfd.revents & POLLIN) { n = read(fd, buf, sizeof(buf) - 1); if (n >= 0) { buf[n] = '\0'; printf("recv: %s\n", buf); } } close(fd); return 0; }

当写端全部关闭后,poll()会返回POLLHUP事件,此时read()会返回 0。做长驻服务时,要区分“暂时没数据”和“对方已经退出”两种情况。

7. 内核源码视角:FIFO 在内核中怎么工作

标题里带了“源码”,所以这里必须把内核路径讲清楚。不需要背完整函数,但关键文件、关键结构体和调用流程值得了解。

7.1 文件系统层面的创建路径

用户层调用mkfifo()后,glibc 封装最终走到内核的mknodat系统调用。内核创建 inode 时,会根据文件类型做特殊初始化。对于 FIFO,VFS 会调用init_special_inode(),把 inode 的文件操作表设置为fifo_fops。

这一步非常关键。它决定了之后所有open、read、write操作走进哪套逻辑。普通文件的f_op指向 ext4、tmpfs 等文件系统的实现;FIFO 的f_op直接指向管道专用实现。

对应源码位置:

  • fs/fifo.c:FIFO 的打开逻辑。
  • fs/pipe.c:管道读写逻辑。
  • include/linux/pipe_fs_i.h:关键结构体定义。

7.2 fifo_open 与管道对象

当进程open()一个 FIFO 时,内核会进入fifo_open()。核心流程大概如下:

  1. 如果这个 inode 上还没有管道对象,调用alloc_pipe_info()分配一个struct pipe_inode_info,挂到 inode 的i_pipe字段。
  2. 根据本次打开方向,更新管道对象里的readers、writers计数。
  3. 如果打开读端但当前没有写者,默认阻塞模式下进程会进入等待队列,直到一个写者出现。
  4. 如果打开写端但当前没有读者,默认阻塞模式下同样进入等待队列;非阻塞写打开则返回ENXIO。

数据结构关系可以这样看:

进程 A open() 后 进程 B open() 后 +-------------+ +-------------+ | struct file | | struct file | +-------------+ +-------------+ | f_op = fifo_fops | f_op = fifo_fops +----------------+-----------------+ v +----------------------+ | inode (FIFO 节点) | | i_pipe ------------+----> struct pipe_inode_info +----------------------+ | v +---------------------+ | pipe_buffer 数组 | | head / tail | | readers / writers | | 等待队列 | +---------------------+

struct pipe_inode_info里的几个核心字段:

  • bufs:指向struct pipe_buffer数组,每个 buffer 对应一个页缓存页。
  • head、tail:读写位置的环形索引。tail表示读位置,head表示写位置。
  • readers、writers:当前读端、写端的打开数量。
  • wr_wait、rd_wait:写者等待队列、读者等待队列。
  • mutex:管道对象的内核互斥锁,保护并发读写。

7.3 读写数据的内核路径

写入数据时,内核走pipe_write():

  1. 检查管道缓冲区是否有可用空间。如果没有,写进程进入wr_wait等待队列。
  2. 有空间时,从页缓存分配一个 page,或者复用已有的 pipe_buffer。
  3. 将用户态数据通过copy_from_user()拷贝到页缓存。
  4. 更新head索引,唤醒等待在rd_wait上的读者。

读取数据时,内核走pipe_read():

  1. 检查tail和head是否重合。重合表示缓冲区为空,读者进入rd_wait等待队列。
  2. 有数据时,将页缓存数据通过copy_to_user()拷贝到用户态缓冲区。
  3. 更新tail,唤醒等待在wr_wait上的写者。

所以 FIFO 的数据流本质上是一份内核页缓存拷贝。写入不直接落盘,读取也不经过磁盘 IO。这也是为什么它比“写文件 + 另一个进程读文件”的方实要高很多。

读端全部关闭后,写端下一次写入时,内核逻辑会判定“没有读者”,触发SIGPIPE信号,同时write()返回EPIPE。这个行为也是pipe_write()里处理pipe->readers == 0分支产生的。

8. 性能观察与资源占用

8.1 数据流向与磁盘 IO

FIFO 读写只以内核内存为媒介,不触发块设备 IO。测试时可以用iostat观察磁盘利用率,正常情况下读写 FIFO 不会增加磁盘繁忙度。

iostat -d 1

如果你看到 FIFO 测试过程中磁盘 IO 没有明显变化,说明数据确实走的是内核缓冲区,这符合预期。

8.2 管道缓冲区与调整

管道缓冲区默认大小通常由内核参数控制。查看方法:

cat /proc/sys/fs/pipe-max-size ulimit -a | grep pipe

Linux 还支持通过fcntl()调整单个管道的大小:

#include <fcntl.h> #include <unistd.h> int fd; int new_size = 65536; fd = open("/tmp/my_fifo", O_RDONLY | O_NONBLOCK); fcntl(fd, F_SETPIPE_SZ, new_size);

注意:F_SETPIPE_SZ设置的值不能超过/proc/sys/fs/pipe-max-size,否则内核会按上限限制。实际可用值以内核返回为准。

8.3 用系统调用观察行为

使用strace可以直接看到进程在系统调用层面是如何阻塞和唤醒的:

strace -e trace=open,read,write,close ./reader strace -e trace=open,read,write,close ./writer

读端启动后,strace会停在open("/tmp/my_fifo", O_RDONLY )这一步。写端启动后,读端的open返回,随后阻塞在read()。这些输出对排查“为什么卡住”非常有帮助。

测量吞吐量可以用dd加time:

time dd if=/dev/zero of=/tmp/my_fifo bs=4096 count=100000 & time dd if=/tmp/my_fifo of=/dev/null bs=4096 count=100000

第一个命令在后台往 FIFO 写,第二个命令前台读。最终time输出的耗时是本机实际测量结果,不同机器差异会很大,因此这里不给固定预期值,以你本机跑出来的结果为准。

8.4 与其他进程间通信方式对比

方式通信范围数据边界复杂度典型场景
FIFO同机无,字节流低日志采集、任务分发
匿名管道同机,需父子关系无最低命令管道
System V 消息队列同机有消息边界中小消息按类型分发
共享内存同机无高大数据量高性能共享
Unix domain socket同机有,支持 SOCK_STREAM / SOCK_DGRAM中本地 RPC、服务通信
TCP socket跨机器无高网络通信

FIFO 的优势在于简单和路径可见。它不适合跨机器通信,也不适合传递大对象,因为每次数据都要经过内核缓冲区拷贝。需要超大吞吐时,共享内存更合适;需要消息边界时,Unix domain socket 通常更方便。

9. 常见问题与排查方法

下面是一份可以直接对照的排错清单。

问题现象可能原因排查方式解决方法
open(O_RDONLY)一直不返回没有写进程打开 FIFO用 ps 查看对端进程;用 strace 跟踪 open 调用先启动写端进程;或改用 O_NONBLOCK
open(O_WRONLY | O_NONBLOCK)报错当前没有读端打开检查 errno 是否为 ENXIO先启动读端;代码里对 ENXIO 做重试
read()返回 0所有写端已经关闭检查写进程是否退出按 EOF 处理;业务需要时重启写端
write()进程被信号杀死读端全部关闭,触发 SIGPIPE日志或终端显示 Broken pipe忽略 SIGPIPE,并检查 write 返回 EPIPE
多个写者数据互相穿插单次写入超过 PIPE_BUF,或并发写同一 FIFO统计每次写入长度控制单次写入 <= 4096 字节;或加锁
再次运行报 File existsFIFO 节点没有被清理ls -l /tmp/my_fifo确认启动前unlink(),或处理 EEXIST
写端写不进去,进程挂住缓冲区满,读端消费太慢观察读进程是否在运行;查看阻塞位置加速读端;调整管道大小;改用非阻塞
双 FIFO 程序互相等待open 顺序错误导致互相等对端strace 打开顺序统一先读后写;或非阻塞打开加重试
权限不足无法打开FIFO 权限位限制了其他用户ls -l查看属主和权限调整 mkfifo mode,或使用相同用户运行

还有一个高频神坑:程序正常收发一次后,第二次运行 reader 时发现read()立即返回 0。原因往往是上一步的close(fd)关掉了读端,且此时没有新的写者。这不代表 FIFO 坏了,而是“所有写端关闭后读端会收到 EOF”这个语义在起作用。

10. 最佳实践与使用建议

10.1 进程启动顺序

先启动服务端,服务端 open 读端(可以阻塞等待),再启动客户端写端。这样最不容易出现启动顺序导致的“假死”。如果启动顺序无法保证,服务端读端可以用O_RDONLY | O_NONBLOCK,避免被 open 卡住。

10.2 信号与异常处理

写端需要处理SIGPIPE。建议在主函数里忽略该信号,然后通过write()的返回值和errno判断对端是否关闭:

#include <signal.h> signal(SIGPIPE, SIG_IGN);

这样写端不会因为对端退出而被默认信号动作杀死,而是得到一次处理异常的机会。

10.3 工程目录与清理

FIFO 节点建议放在固定目录,例如/var/run/myapp/或/tmp/myapp/。进程启动时先unlink再mkfifo,避免上次残留节点造成EEXIST。进程退出时可以再次unlink。如果服务崩溃,残留的 FIFO 节点会在下次启动时被覆盖,这比依赖手动清理更强。

10.4 生产使用边界

多写者场景,所有消息必须控制在PIPE_BUF以内,否则要加锁。多读者场景,每条数据只会被其中一个读者读走,数据不是广播。需要广播时,用多个 FIFO 或者换用消息队列。

写端和读端速率不匹配时,建议设置超时,避免长时间占住进程。接入事件循环时,用O_NONBLOCK+poll/epoll,不要用阻塞模式硬等。

权限方面,注意 FIFO 节点对文件系统内所有有权限的用户可见。传输敏感数据时,要确认 FIFO 的 mode 和运行用户,避免其他用户能读到本应受限的数据

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询