Linux命名管道FIFO详解:原理、阻塞行为与C语言实战

发布时间:2026/9/29 17:48:36
Linux命名管道FIFO详解:原理、阻塞行为与C语言实战 1. 为什么是命名管道先理解它和匿名管道的边界1.1 匿名管道不够用的场景大多数Linux开发者接触进程间通信IPC最早遇到的就是管道。ps aux | grep nginx、cat file.log | tail -50这些命令背后的|符号就是匿名管道。匿名管道用起来确实爽但它有一个硬伤管道本身没有名字只能由fork出来的父子进程使用因为子进程复制了父进程的文件描述符表才能共享那根管道。一旦涉及两个没有亲缘关系的进程匿名管道就抓瞎了。比如你有一个常驻的服务进程A另一个独立启动的客户端工具B想往A里喂数据这两者之间没有任何父子关系匿名管道根本搭不上桥。我早期做过一个采集程序想把设备上报的数据喂给另一个独立运行的统计服务第一反应就是管道结果发现pipe()只能在自己的进程树里用跨进程根本传不过去。换成共享内存又太复杂还要处理同步问题最后查了一圈资料才意识到命名管道才是这个场景的正解。命名管道在文件系统里有一个可见的路径名任何进程只要知道这个名字就能打开这根管道参与通信。它把匿名管道的父子血缘限制彻底解除了这是它存在的最核心理由。1.2 命名管道的本质带名字的管道文件命名管道在Linux里有个专门的称呼叫FIFOFirst In First Out一听这名字就知道它的行为特征先进先出像排队一样先写进去的数据先被读到。你在终端执行ls -l fifo_test看到的类型标识是p而不是普通的-或d这就是管道文件的标记。FIFO本质上是个特殊的文件类型但要注意它和普通文件有本质区别。普通文件的数据实实在在写在磁盘块上你写进去1MB就占1MB磁盘空间。FIFO的数据恰恰相反它不占磁盘空间写入的数据直接进入内核缓冲区读方取走之后缓冲区就释放了。你可以ls -lh看一眼FIFO文件的大小永远是0因为文件大小这个属性对它没有意义它只是一个通信入口真正流转的数据藏在内核空间。打个比方FIFO文件更像是小区门口的信箱。信箱本身不存储信件内容只是一个写着门牌号的投递口邮递员把信塞进去收件人掏出钥匙取走。信件在运输链路上流转但不属于信箱的一部分。1.3 内核缓冲机制数据管道的工作方式命名管道在内核里维护一块缓冲区默认大小通常是64KB具体值你可以用ulimit -a看pipe size那一行多数发行版是65536字节。写入方调用write()时数据先落到这块内核缓冲读取方调用read()时再从缓冲里取走。这个机制带来几个非常关键的行为特征缓冲区满的时候写入方的write()会被阻塞直到读取方取走部分数据腾出空间缓冲区空的时候读取方的read()会被阻塞直到写入方送进来新数据管道中的数据是一次性的读取方读走之后这些字节就消失了不会像普通文件那样留在那里等你反复读。最后这一点特别重要。很多人第一次用FIFO下意识把它当成文件来理解总觉得数据写进去就能一直查看。实际上管道就是水流式的通信流过去就没了。如果你想同时给多个进程发同一份数据必须自己设计广播机制FIFO本身不提供这个能力。理解了这条底层逻辑后面遇到的各种阻塞、空转、数据丢失现象就都好解释了。2. 动手前先把这几个阻塞行为吃透2.1 open的阻塞规则打开阶段就能卡住我见过不少人栽在这个坑里程序写好了运行时卡在某一行排查半天发现是open()函数没返回。这里有个很重要的规则open一个FIFO默认是阻塞式的而且阻塞逻辑分两种情况如果只以只读方式open(fifo, O_RDONLY)这个调用会一直等到有另一个进程以写方式打开同一FIFO才返回如果只以只写方式open(fifo, O_WRONLY)会一直等到有另一个进程以读方式打开同一FIFO才返回。换句话说打开FIFO这件事本身就需要配对。你用只读方式打开它内核会判断现在还没有写方接入你一个读方在这等着有什么用于是把你挂起。等到写方出现的那一刻双方才算握手成功各自的open()同时返回通信通道建立。这个设计和TCP的握手有异曲同工之妙都是为了确保两边都准备好再开始干活。想跳过等待直接返回也不是没办法在open时加上O_NONBLOCK标志内核就不会在打开阶段阻塞你了。但用非阻塞模式要格外小心后续的read()和write()行为也会跟着改变这点后面细说。2.2 read和write的阻塞表现数据流量的红绿灯打开成功后正式进入数据读写阶段。这里有个容易混淆的地方我专门用一个表整理清楚操作缓冲区状态默认行为write缓冲区有空间立即写入并返回写入字节数write缓冲区已满阻塞直到读方腾出空间read缓冲区有数据立即读走并返回字节数read缓冲区为空阻塞直到写方送来数据read写方已全部关闭返回0相当于读到EOFwrite读方已全部关闭收到SIGPIPE信号进程默认终止注意最后两行这是管道通信和文件读写最大的区别。普通文件读到最后到EOFread()返回0你好好的但管道里如果所有写方关闭了read()返回0意味着断流了你得主动break退出循环。反过来如果读方全部关闭了还往管道里写内核直接给你的进程发一个SIGPIPE信号默认动作就是把进程杀掉。很多服务进程莫名其妙挂掉日志里也没有异常最后发现是SIGPIPE惹的祸就是这个原因。2.3 非阻塞模式的行为差异加了O_NONBLOCK之后上面这套规则会完全变样open只读时如果没有写方open()立即返回成功不会阻塞等待open只写时如果没有读方open()直接返回-1错误码是ENXIO表示设备不存在read时缓冲区为空立即返回-1错误码EAGAIN意思是现在没数据你过会儿再来write时缓冲区满同样立即返回-1错误码EAGAIN。非阻塞模式适合那种需要在等待期间去做别的事情的程序比如一个服务进程同时监控多个FIFO不可能为一个管道死等。但如果只是简单的两个进程互相收发统一用默认阻塞模式反而更省心逻辑也直白。3. 完整实战命令行收发数据3.1 创建FIFO的三种方式先不谈代码我们从终端入手把FIFO的脾性摸清楚。创建FIFO最常用的是mkfifo命令mkfifo /tmp/myfifo执行完ls -l /tmp/myfifo你会看到prw-r--r-- 1 user user 0 ... /tmp/myfifo开头那个p就是FIFO专用的文件类型标记。如果你想在C代码里创建FIFO调mkfifo()函数就行签名很直接int mkfifo(const char *pathname, mode_t mode);mode和普通文件权限一样比如0666表示所有人可读写。还有一个mknod()函数也能创建FIFO但那个太底层了一般用不到。3.2 手动收发体验阻塞行为在一个终端里执行cat /tmp/myfifo你会发现终端卡住了光标在闪烁但什么都不输出。这不是程序死了而是cat以只读方式打开FIFO后内核在等待写方出现。这时候打开另一个终端echo hello fifo /tmp/myfifo再回头看一眼第一个终端你会惊讶地发现hello fifo已经打印出来了同时第二个终端的echo命令也正常退出了。这就是一次完整的FIFO通信写方把数据送入内核缓冲读方取走打印。整个过程没走磁盘也没经过网络就是内核里的一次数据接力。这里还有个细节值得注意。如果你先执行的是写命令比如直接echo test /tmp/myfifo同样会卡住因为内核还在等读方出现。所以你可以在另一个终端执行cat /tmp/myfifo把数据接走。谁先启动不重要重要的是读方和写方最终要配对成功。3.3 覆盖真实场景跨终端持续收发只传一句话不过瘾我们来模拟一个持续通信的场景。终端A运行while true; do cat /tmp/myfifo; done这个循环会反复打开FIFO读取数据。注意每次cat读取后如果发现写方关闭cat就会退出所以需要无限循环来不断重新打开。终端B运行echo data 1 /tmp/myfifo echo data 2 /tmp/myfifo echo data 3 /tmp/myfifo终端A会依次打印出这三条数据。这说明FIFO完全可以承担多次通信的任务只是每次通信的连接建立和连接断开都会发生一次和TCP的短连接模式非常相似。如果想模拟长连接就保持读写双方都不关闭比如写方用一个循环持续写入for i in $(seq 1 100); do echo msg $i /tmp/myfifo; sleep 1; done读方用一个持续运行的cat即可这段期间管道连接一直保持数据源源不断流过去。4. 写一个C语言收发程序4.1 发送端源码与逐行解释命令行只能验证机制要在真实项目里用FIFO还得写代码。先给一个发送端#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include sys/stat.h #include errno.h #define FIFO_PATH /tmp/ipc_fifo int main(int argc, char *argv[]) { if (argc ! 2) { fprintf(stderr, Usage: %s message\n, argv[0]); return 1; } // 确保FIFO存在。如果已经存在且是FIFO则忽略错误否则报错退出 if (mkfifo(FIFO_PATH, 0666) 0 errno ! EEXIST) { perror(mkfifo); return 1; } int fd open(FIFO_PATH, O_WRONLY); if (fd 0) { perror(open); return 1; } if (write(fd, argv[1], strlen(argv[1])) 0) { perror(write); close(fd); return 1; } close(fd); return 0; }这段代码的流程很直接先确保FIFO存在再以只写方式打开写入用户传入的消息最后关闭。有个细节值得展开mkfifo调用如果返回EEXIST说明文件已经存在这未必是错误——只要那个已存在的文件是FIFO就能继续用。但如果同名文件是一个普通文件或者目录那就危险了open打开后会向一个普通文件里写数据行为完全跑偏。严谨一点的做法是在EEXIST之后用stat()确认文件类型确实是FIFO再决定是否继续。生产环境里我一般会加这个检查避免因为脏数据导致程序写错地方。4.2 接收端源码与退出逻辑接收端稍微复杂一点因为要处理反复读取直到断流#include stdio.h #include stdlib.h #include unistd.h #include fcntl.h #include sys/stat.h #include string.h #include errno.h #define FIFO_PATH /tmp/ipc_fifo #define BUF_SIZE 4096 int main(void) { int fd open(FIFO_PATH, O_RDONLY); if (fd 0) { perror(open); return 1; } char buf[BUF_SIZE]; ssize_t n; while (1) { memset(buf, 0, BUF_SIZE); n read(fd, buf, sizeof(buf) - 1); if (n 0) { printf([recv] %s\n, buf); } else if (n 0) { // 所有写方已关闭正常退出 printf([info] all writers closed, exit\n); break; } else { perror(read); break; } } close(fd); return 0; }这段代码里最关键的是n 0的分支判断。普通文件的read到EOF也是返回0你会觉得这是正常读完了但在FIFO场景中read返回0意味着所有写入端描述符都关闭了相当于对端主动断开连接。如果你不退出循环read会反复返回0程序无限空转CPU占用率一路飙升。4.3 编译运行与现象验证编译两个程序gcc -o sender sender.c gcc -o receiver receiver.c先在一个终端运行接收端./receiver程序会卡在open处因为现在还没有写方打开FIFO。另一个终端运行./sender first message接收端立刻打印[recv] first message然后继续阻塞在read等待下一条。再运行一次./sender second message接收端再打印一条。当你CtrlC杀掉发送端或者发送端自然退出后接收端的read会返回0程序打印退出信息并结束。这个调试过程完美呈现了FIFO的整个生命周期open握手、数据流转、断流退出。4.4 为什么不用普通文件实现通信看到这里你可能会问既然FIFO本身不占磁盘空间那两个进程直接用普通文件传数据不行吗我确实见过不少初学者这么干——进程A往文件里写进程B轮询读取。表面上也能工作但有几个致命缺陷同步问题A还没写完B就开始读读到半截数据轻则显示乱码重则解析崩溃。FIFO的内核缓冲和阻塞机制天然解决了这个问题读写双方自动同步轮询浪费B要反复读文件检测新数据CPU空转严重。FIFO的read会阻塞等待等数据来了才唤醒零轮询开销文件增长问题普通文件越写越大B读完还要手动清理否则磁盘被占满。FIFO的数据流过后自动消失不存在日志文件无限膨胀的问题。所以FIFO本质上是带同步能力的传输通道而普通文件是存储介质两者解决的不是一类问题。凡是传输型需求优先选FIFO只有需要持久化保存的场景才考虑普通文件。5. 多读多写场景与阻塞陷阱5.1 一个写方多个读方的数据分发问题实际项目中经常遇到这种场景一个数据采集进程持续产生数据但后面可能同时有两三个消费者进程都想要这份数据。直觉告诉你是不是每个消费者都去read同一个FIFO就行了理论上是但实际有一个残酷的问题FIFO的数据被一个读方读走后其他人就再也读不到了。比如你有3个消费者进程同时在read同一个FIFO采集进程写入一条数据这3个读方里只有1个会抢到这条数据另外2个继续阻塞等待下一条。这个行为被称为读者竞争内核不保证均匀分配完全看调度时机。如果你需要广播语义——同一份数据发给所有订阅者——FIFO就不够了。可靠的方案有几种一是每个消费者单独建一根FIFO生产者往所有FIFO各写一份二是用共享内存加互斥锁消费者各自去取三是用消息队列POSIX mq。第一种方案实现最简单缺点是消费者数量多了以后生产者要写很多次性能线性下降。我自己做过一个配置分发模块就是消费者数量固定为2于是顺手为每个消费者建了独立的FIFO生产者写入时循环写两遍。逻辑清晰也没遇到性能问题业务量再大再考虑消息队列。5.2 多个写方一个读方的收敛场景反过来多个进程往同一个FIFO写一个消费者统一读取这个场景FIFO表现得相当可靠。内核保证每次write()是原子操作的——只要写入的数据量不超过PIPE_BUFLinux上通常是4096字节一次write的数据不会被其他写方的数据穿插打断。这是一个非常重要的特性。比如日志采集场景3个模块同时往一个FIFO写入日志行每条日志都在4096字节以内读方读出来的数据一定是完整的一行行不会出现A进程写了一半B进程把数据插进去这种错乱。一旦单次写入超过PIPE_BUF原子性就没了多条写入会交错在一起读方拿到的数据可能错位。所以多写方场景下务必遵守一条纪律每次write的数据量控制在PIPE_BUF以内。你可以在编译期用limits.h里的_POSIX_PIPE_BUF宏确认这个值通常就是4096。5.3 缓冲区满导致的生产者阻塞再讨论一个动态场景。生产者写入速度远大于消费者的读取速度比如采集程序一秒钟写10MB数据处理程序每秒只能消费1MB。这时内核缓冲区很快被填满生产者的write会被阻塞程序表现就是卡在生产的那一行。这种背压效应其实是FIFO的一个优点它天然提供流控防止数据无限堆积。但在实际项目里这也会带来连锁反应生产者被阻塞后上游数据源可能因为等待而堆积。处理策略上要根据业务场景取舍要么提高消费者处理能力要么让生产者丢弃部分非关键数据要么改用更大的缓冲区工具比如消息队列不能任由阻塞把整个链路的吞吐拖垮。我在一次数据采集优化中遇到的就是这种问题最后方案是生产端加了一个环形缓冲和丢弃策略FIFO写不进去时先把数据放进内存队列队列满了再丢最旧的保证采集线程不被锁死。这样系统的其他功能不会因为管道堵塞而全线瘫痪。6. 我最常踩的几个坑和完整排查链路6.1 程序莫名其妙卡死先怀疑open还是read第一次用FIFO写项目时程序运行起来就卡住了没有任何报错。我那时候的习惯是到处打printf结果发现连第一行日志都没打出来说明卡得非常早。于是猜测是open环节出了问题。排查链路是这样的用strace ./program跑一遍会看到程序卡在open(/tmp/myfifo, O_WRONLY)这一行系统调用没有返回这说明open在等待读方接入但我的读方进程压根没起来回到设计问题程序启动顺序能不能保证读方一定先启动如果启动顺序不可控就得在open时加O_NONBLOCK或者用超时机制。这个问题看起来简单但在多进程协同启动的场景里非常容易踩。我后来养成了一个习惯所有FIFO的open统一封装要么加超时要么加非阻塞标志绝不让open无限期等待。6.2 read返回0引发的死循环空转还有一个坑更隐蔽。程序正常退出逻辑写得不好read返回0后没有break而是继续循环读取。结果是FIFO所有写方都关闭了read反复返回0程序变成一个死循环CPU单核直接拉满top一眼就能看到某个进程占了100%。排查这个问题的线索很清晰ps aux看到进程CPU 100%先想到死循环strace -p PID附加到进程上看到系统调用序列是一连串的read返回0对照代码找出循环退出条件里漏掉了n0的分支。后来我的读端代码模板统一长这样while (1) { n read(fd, buf, sizeof(buf)); if (n 0) { break; // 写方全部关闭正常退出 } if (n 0) { if (errno EINTR) continue; perror(read); break; } process_data(buf, n); }EINTR也要处理这是信号中断导致的read返回-1不能当成错误直接退出否则程序被信号打断一次就崩了。6.3 SIGPIPE导致的进程静默退出写方往一个已经没有任何读方的FIFO里写入数据内核会给写方进程发SIGPIPE信号默认行为是终止进程。这个设计有时候很让人抓狂因为进程挂了但没有任何日志连错误提示都看不到。排查过程通常是这样程序跑了一段时间后突然消失dmesg里看不到段错误信息日志文件里也没有异常记录。这时候要怀疑信号问题用gdb或者给进程加一个SIGPIPE处理器把信号捕获住再打印日志。业务上如果不希望进程因为SIGPIPE退出最简单的做法在程序启动时忽略它signal(SIGPIPE, SIG_IGN);但忽略之前要想清楚这样write的返回码会变成EPIPE错误而不是直接杀进程你得在代码里判断这个错误码并做相应处理。我一般建议服务端程序都要处理SIGPIPE因为对端随时可能退出不能把进程的生命周期绑在对端的连接行为上。客户端短生命周期程序倒是可以不处理挂了就挂了反正是单次任务。6.4 清理FIFO文件的时机程序退出后FIFO文件本身还在文件系统里躺着。下次启动时mkfifo会发现EEXIST只要能确认是FIFO类型就继续用。但如果程序非正常退出下次启动open时可能遇到一个老旧的FIFO文件里面的数据早已清空没有任何残留。这带来一个设计选择题FIFO文件是创建时初始化还是启动时清理我的习惯是让程序启动时主动mkfifo一次并忽略EEXIST错误不清除旧文件。因为FIFO文件本身不占空间留着也无妨反而避免了一些竞态——万一另一个进程正在用它通信你把它删了再重建那个进程打开的是旧inode两边就断开了。这一点和普通文件很不一样普通文件删了重建不影响后续使用但FIFO删了重建等于把连接入口换掉了正在通信的进程会莫名其妙断流。运行期间千万别去动FIFO文件本身这是一个非常朴素但极其重要的教训。7. 在Shell脚本和项目里的典型应用思路7.1 脚本之间的异步解耦FIFO在纯Shell场景里也很有用。比如你有一个定时任务脚本它要调一个耗时的数据导出工具又不想等它跑完才继续做别的事。这时候可以用FIFO做个简单的异步通道。#!/bin/bash FIFO/tmp/export_job.fifo # 创建FIFO [ -p $FIFO ] || mkfifo $FIFO # 后台启动一个工作进程持续读取FIFO while read -r line; do echo job received: $line do_export $line done $FIFO # 主脚本继续执行其他任务随后触发导出任务 echo export-2025-01-15 $FIFO这个写法把任务下发和任务执行通过FIFO解耦了。主脚本只需要往FIFO里写一行数据就能触发后台任务不需要自己fork子进程也不用等待结果。类似的思路可以用来实现一个简单的任务队列。7.2 C程序里的观察者模式替代方案在C/C项目里我常用FIFO实现一个轻量级的事件通知替代部分观察者模式。某个核心模块状态变化时往FIFO里写一个事件码管理进程通过select或epoll监听FIFO的可读事件收到就处理。这比直接用信号量或者回调函数更直观而且FIFO还天然带缓冲短时间内爆发的事件不会丢失——只要缓冲区没满。涉及多个事件合并的场景也方便连续写入多条状态读方一次性读出来批量处理减少上下文切换开销。我之前实现的一个内置监控模块就开了一根FIFO作为日志事件通道。核心模块把警告级别以上的事件写进FIFO监控进程那边用select挂了一个读端有数据可读就取出来拼接成告警写入文件。比起用syslog自定义协议这套实现简单可靠跨进程边界清晰出了问题也好排查。7.3 和select/poll配合实现多路复用最后提一个进阶技巧。真实的服务器程序不可能只在一个FIFO上死等还要处理网络连接、定时器、信号等。这时候不要把read直接放在主循环里阻塞调用而是把FIFO的读端文件描述符挂到select或poll上。struct pollfd fds[2]; fds[0].fd fifo_fd; fds[0].events POLLIN; fds[1].fd socket_fd; fds[1].events POLLIN; int ret poll(fds, 2, timeout); if (ret 0) { if (fds[0].revents POLLIN) { handle_fifo_data(); } if (fds[1].revents POLLIN) { handle_socket_data(); } }需要注意FIFO的读端在加入poll前最好确认有写方存在否则打开阶段的阻塞问题依然存在。配合之前提到的O_NONBLOCK可以让读端的open立即返回然后用poll统一管理读写时机。这个方案特别适合那种需要同时接收外部网络命令和内部进程命令的服务程序网络命令走socket内部模块的数据走FIFO在同一个事件循环里统一处理架构清晰而且易于扩展。8. 命名管道和真实项目选型的一点心得用命名管道写过几个项目之后我对它有了一个比较明确的定位它是Linux IPC机制里最朴素、最可靠、最低依赖的选择之一。相比共享内存它不需要关心锁和同步问题相比socket它不需要IP、端口和网络协议栈相比消息队列它不需要引入额外的库或守护进程。只要两个进程在同一台机器上通信需求是流式的、单向的FIFO基本都是最顺手的那把螺丝刀。可替代它也有天花板跨机器通信完全指望不上广播分发做不了大数据块传输因为内核缓冲限制吞吐上不去多对多的复杂拓扑里维护FIFO文件列表本身就是一种负担。在这些地方消息队列、共享内存、socket才应该是主角。做技术选型的时候我习惯先问三个问题进程是否在同一台机器方向是否基本单向流量是否可控三个答案都是肯定就放心用FIFO。否则就换个方案不要在管道上硬凑。按照这个标准命名管道在嵌入式设备、本地服务模块通信、脚本工具链里还能发光发热很久尤其是在追求最少外部依赖的场景中它那点操作系统自带的朴素能力反而成了最大的优点。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询