Linux有名管道FIFO:创建、读写与进程间通信实战

发布时间:2026/10/26 3:26:47
Linux有名管道FIFO:创建、读写与进程间通信实战 1. 有名管道到底有名在哪从两个进程传数据的纠结说起两个独立进程之间要传一段数据第一反应往往是上 socket稍微懂点的会扯共享内存或者消息队列但如果只是让一个采集端把日志丢给一个处理端或者让一个控制脚本去唤醒一个常驻服务把 socket 那套 listen/accept/三次握手搬出来就有点杀鸡用牛刀了。我最早接触有名管道就是在这么一个重方案用不上、又不想让两个进程硬耦合的场景里——一个进程要通知另一个进程该干活了还顺带把几 KB 的参数递过去。当时试过写临时文件轮询CPU 空转和文件残留两个毛病都很难看试过用信号结果只能传一个整数还得处理信号丢失。最后落在有名管道上它就是一个存在于文件系统里的文件但读写走的是内核缓冲区两个毫无血缘关系的进程打开同一个路径就能像插了根管子一样对传数据。这个标题里三个关键词——创建、基本读写、进程间通信——恰好是有名管道从能看见到能用起来的完整链条。创建解决的是这东西在哪儿、叫什么基本读写解决的是打开它之后行为是什么样的、什么时候会卡住进程间通信解决的是两个进程怎么配合、谁先开谁后开、怎么收场。我见过不少人在第一步就卡住echo hi /tmp/fifo敲下去终端一动不动以为程序挂了其实是被内核按在那儿等读端。也见过有人在第三步翻车两个进程互相等着对方先打开造成死锁。这篇就把这条链从头到尾走一遍顺带把那些文档里一句blocking带过、实际能把人关在门外半小时的细节全拆开。1.1 匿名管道的那道坎亲缘关系在讲有名管道之前得先把匿名管道的问题说清楚不然你体会不到前者存在的必要性。pipe(int fd[2])创建出的管道本质是一块内核缓冲区加两个文件描述符数据从fd[1]进去、从fd[0]出来。问题在于这两个 fd 只能通过 fork 继承给子进程父进程和子进程天然共享这对 fd通信很顺畅可你要跨到一个跟它没任何关系的第三方进程就没法把它们递过去除非用 UNIX 域套接字做 fd 传递那就绕远了。有名管道绕开这道坎的办法非常粗暴把管道的入口挂在文件系统里。/tmp/myfifo这个路径就是一个公开的门牌号任何进程只要知道这个路径、有权限打开它就能接上这根管子。用stat看一眼会发现它是一个特殊的文件类型S_IFIFOls -l的第一个字符是p大小永远是 0——因为往里写的数据根本不落盘全在内核的那块缓冲区里打转。这个设计带来的直接好处是解耦。发数据的进程不需要知道接收方是谁、在不在线只要不是严格同步模式它只管往路径里写接收方也不需要知道数据是谁发的。两者之间唯一的契约就是那个路径名。这在做工具链串联、做进程编排的时候特别省事你完全可以让一个 Python 采集脚本往 FIFO 里吐数据另一个 C 写的处理程序从里面读语言、框架、启动顺序都不用对齐。1.2 FIFO在文件系统里留下的那个inode一个容易被忽略的事实是有名管道在磁盘上确实占了 inode但它不占数据块。mkfifo创建完之后du一下是 0stat的st_size也是 0你往里写 100 MB它还是 0。这点跟普通文件完全不同它的意义在于内核只需要为它记录一个管道对象的引用真正的数据通道是在open之后才建立的。理解了这一点很多事情就顺了。比如你在tmpfs上创建 FIFO重启就没了在 ext4 的普通目录上创建重启之后文件还在里面残留的数据却没了数据本来就在内存里。这也解释了为什么有些老系统上会出现管道文件存在但读不到任何东西的怪现象——文件是上次运行留下的管道对象早销毁了新开的进程接上去就是一根空的管子。所以我个人的做法是启动时用mkfifo失败返回EEXIST就直接复用但绝不复用里面可能残留的语义应用层自己做好握手。还有一点值得提FIFO 的路径不能跨文件系统共享它不像 socket 可以指向远端。它就是个本地节点跨机器传输这件事从设计上就没考虑过。1.3 一张表看清FIFO与匿名管道、普通文件、socket的边界很多人选型时会在几者之间犹豫我整理了一张对照表基本覆盖日常判断的大部分情况维度有名管道 FIFO匿名管道 pipe普通文件UNIX域socket是否需要亲缘关系否是否否数据是否落盘否否是否消息边界无字节流无无有可选支持双向否单向否是是支持多写端是需注意原子性是是是典型延迟极低极低较高磁盘低需要清理需要 unlink不需要需要删除需要 unlink从表里能看出 FIFO 的定位单向、无边界、低延迟、零配置的本地字节流通道。它不适合传结构化消息没有边界读端可能读到半个包也不适合需要回传应答的场景单向。但只要你的需求是一方推、一方收、不求应答它就是最轻的那把刀。真需要双向就开两条 FIFO一条上行一条下行这也是很多守护进程的经典做法。2. 创建FIFO的三种姿势与权限细节2.1 mkfifo命令调试脚本时最快的一把刀命令行里的mkfifo是验证思路最快的方式。最基本的用法就一行mkfifo /tmp/demo.fifo ls -l /tmp/demo.fifo第二条命令的输出大概长这样prw-r--r-- 1 user group 0 Oct 10 10:00 /tmp/demo.fifo开头那个p就是它的身份标识。要指定权限-m参数可以直接给八进制mkfifo -m 600 /tmp/secret.fifo这里有个细节值得说清楚-m 600给的是最终权限还是要经过 umask 过滤的权限实测答案是最终权限mkfifo命令内部会先umask(0)再设权限所以-m 600出来就是600。但如果你是自己在 C 代码里调mkfifo()系统调用传的那个mode参数是要跟 umask 做与运算的。这个差异坑过不少人——命令行测试好好的换到代码里权限就对不上八成就是这个原因。验证 FIFO 能不能用时标准的双终端操作是这样的终端 A 执行cat /tmp/demo.fifo会立刻卡住因为读端在等写端终端 B 执行echo hello /tmp/demo.fifo终端 A 立刻打印hello然后两条命令都返回。这个流程看起来简单但它把 FIFO 最核心的两个行为都演了一遍open 的配对阻塞和 write 到 read 的即时传递。2.2 mkfifo()系统调用C代码里的创建逻辑系统调用原型在sys/stat.h里#include sys/types.h #include sys/stat.h int mkfifo(const char *pathname, mode_t mode);返回值 0 表示成功-1 表示失败具体原因看errno。最常见的两个错误码是EEXIST路径已存在和EACCES父目录没写权限或ENAMETOOLONG路径太长。实际工程里我一般这么写创建逻辑#include stdio.h #include errno.h #include string.h #include sys/stat.h #include sys/types.h int ensure_fifo(const char *path, mode_t mode) { struct stat st; if (mkfifo(path, mode) 0) { return 0; /* 新建成功 */ } if (errno EEXIST) { /* 已存在必须确认它确实是 FIFO而不是同名普通文件 */ if (stat(path, st) -1) { perror(stat); return -1; } if (!S_ISFIFO(st.st_mode)) { fprintf(stderr, %s 已存在但不是 FIFO\n, path); return -1; } return 0; /* 复用已有 FIFO */ } perror(mkfifo); return -1; }这段代码里最关键的是那个S_ISFIFO检查。我见过一次线上事故就是启动脚本mkfifo失败后直接忽略错误结果路径上恰好是个上次留下的普通日志文件程序往里面写数据全落到磁盘上去了读端自然一个字节也收不到排查了两个小时才发现路径被占。忽略EEXIST而不校验类型是个非常典型的懒写法。2.3 mknod与权限位为什么我看到的是0644减去umask除了mkfifo还有一个更低层的系统调用mknod也能创建设备节点式的特殊文件mknod(path, S_IFIFO | 0666, 0);第三个参数是设备号对 FIFO 来说恒为 0。功能上跟mkfifo(path, 0666)等价但mknod能创建字符设备和块设备权限也更危险所以日常场景直接用mkfifo就好mknod知道有这么回事即可。权限这块再补一个实操点。mkfifo命令默认给的是0666被 umask通常是022过滤后变成0644。也就是说默认情况下只有所有者能写其他用户只读。如果你的通信双方是不同用户跑的进程写端就会收到EACCES报错信息也还算清晰Permission denied但如果你在容器里跑、umask 被设成077那就变成0600跨用户彻底玩不转。所以涉及跨用户的场景我一般显式写明权限mkfifo -m 666 /var/run/app/channel.fifo用-m给最终权限比去改 umask 更稳。当然666在生产上要慎用配合目录权限做隔离更合适——FIFO 的访问控制主要靠父目录的权限把目录设成750比给文件666更安全。2.4 已经存在怎么办EEXIST的正确处理姿势服务端程序通常需要启动即就绪所以创建逻辑要幂等。上面那段ensure_fifo已经覆盖了大部分情况但还有两个边角值得提。第一残留 FIFO 的脏状态问题。上次进程异常退出时FIFO 文件留在了文件系统上。新进程复用它是没问题的因为open之后会创建全新的管道对象里面的旧数据本来也不存在数据在内存里进程死了就没了。但如果有另一个进程还持有旧的 fd你unlink再重建就会出现两个不同管道共用同一个路径的诡异情况——老进程还在读老管道新进程读新管道互相都看不见对方。所以清理逻辑要跟生命周期绑定不能随手删。第二启动时到底该不该先 unlink。我倾向于不主动删而是用EEXIST分支复用同时约定清理责任归服务端服务端启动时如果需要确保干净先检查有没有进程在用比如fuser或者自己的 pid 文件确认没有才删。图省事直接unlink再mkfifo在多实例并存或者重启窗口很短的场景里很容易把正在收数据的客户端踢掉。3. 读写行为全解阻塞点到底卡在哪里3.1 open()为什么会被挂住内核那根两端到齐的标尺FIFO 最容易让人懵的地方就是open。普通文件open立刻返回FIFO 的open可能永远不返回。原因在于内核对 FIFO 的打开设了一条规则读端的 open 要等到至少有一个写端打开才返回写端的 open 要等到至少有一个读端打开才返回。两边都到齐了两个open同时放行。这条规则的设计意图挺好理解——它天然实现了一种同步。生产者不会在消费者还没起来的时候白写数据消费者也不会在生产者没起来的时候读到 EOF 就直接退出。但用起来就是个大坑因为你没法先开一端看看情况。绕开它的办法有两个都很实用。办法一用O_NONBLOCK打开读端。这样即使没有任何写端open也会立刻成功返回只是read会立刻返回-1并设置errno EAGAIN。但注意写端的O_NONBLOCK行为不一样如果没有任何读端open直接失败返回-1errno ENXIO。这就是两者不对称的地方很多人只记住了一半。办法二先O_RDONLY | O_NONBLOCK打开再用fcntl清掉O_NONBLOCK。这样open不阻塞之后的read又是阻塞语义兼顾了两边。这是很多服务端的标准写法int fd open(path, O_RDONLY | O_NONBLOCK); if (fd 0) { perror(open); return -1; } int flags fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags ~O_NONBLOCK); /* 恢复阻塞读 */还有个偏门写法是open(path, O_RDWR)。Linux 下它确实能成功且不阻塞因为进程自己既是读端又是写端两端都到齐的条件瞬间满足。但这是 Linux 的扩展行为POSIX 没有定义而且副作用很难受因为写端永远存在就是它自己这个 fd 永远读不到 EOF很多依赖 EOF 判断结束的逻辑会挂死。所以这个用法我建议只在明确的调试场景用生产代码别碰。3.2 read()的三种返回数据、0和-1read在 FIFO 上的返回值要分成三种情况看这个分类是理解整个读写模型的钥匙。返回值大于 0读到了数据。注意 FIFO 是字节流一次read读到的量完全取决于当时缓冲区里有多少。写端写了 100 字节读端缓冲区给 4096那一次读就是 100 字节写端分两次写 50 字节读端可能一次读到 100 字节也可能分两次读——没有任何消息边界保证。这条是新手最容易误解的地方以为一次 write 对应一次 read实际完全不是。要传结构化数据应用层必须自己做长度前缀或者分隔符。返回值等于 0表示 EOF。触发条件很严格——所有持有写端 fd 的进程都关闭了写端并且缓冲区里没有剩余数据。这两个条件缺一不可。缓冲区里还有数据的时候即使写端全关了read也会先把数据吐完再返回 0。返回值等于 -1出错看errno。阻塞模式下常见的是被信号打断EINTR非阻塞模式下是EAGAIN暂时没数据。EINTR这个要特别注意如果你程序里有SIGALRM之类的定时器read被反复打断是常态不处理EINTR就会出现读不到数据的假象。这里插一个真实踩过的坑一个进程用fork起了子进程去处理数据父进程负责写。结果父进程关了写端读端却一直不返回 EOF。查了半天才发现子进程 fork 的时候继承了父进程的写端 fd虽然子进程根本不用它但 fd 还开着内核就认为写端还有人在EOF 自然不来。fork 之后一定要把不用的那一端显式关掉这是使用管道类 IPC 的通用铁律。3.3 write()的原子性与PIPE_BUF的4KB线write这边最需要记住的是一个常量PIPE_BUF。在 Linux 上它是 4096 字节可以从limits.h里拿到也可以通过fpathconf(fd, _PC_PIPE_BUF)查询。这条线划出了两种行为写入长度缓冲区空间足够缓冲区空间不足≤ PIPE_BUF阻塞一次性写入原子阻塞等待足够空间≤ PIPE_BUF非阻塞一次性写入原子返回 -1errnoEAGAIN PIPE_BUF阻塞可能分多次写阻塞直到全部写完 PIPE_BUF非阻塞返回实际写入字节数可能不足返回实际写入字节数可能为 0 或部分所谓原子指的是多个写端并发时一次 ≤ PIPE_BUF 的写入不会被别的写端的数据插进来要么整段在前要么整段在后。一旦超过这条线内核可能会把这次写拆成若干段中间被其他写端插入数据读端就会读到交错的内容。所以如果你的场景是多生产者写同一个 FIFO要么把单次写入控制在 4096 字节以内要么自己在应用层加锁或者给每个生产者单独开一个 FIFO。我一般倾向后者多生产者场景与其纠结原子性不如一人一根管子读端用poll同时监听多个 fd逻辑反而更干净。还有一个容量问题FIFO 的内核缓冲区默认是 64 KB16 个内存页可以通过fcntl(fd, F_SETPIPE_SZ, size)调整上限由/proc/sys/fs/pipe-max-size决定默认 1 MB。这个参数决定了写端最多能领先读端多少。缓冲区满了之后阻塞模式的write就会卡住直到读端消费出空间。如果你的生产者是突发型的调大这个值能减少卡顿但别指望靠它做持久化进程一死里面的数据全丢。3.4 O_NONBLOCK下行为矩阵一张表覆盖大部分疑问非阻塞模式下的行为特别碎我用一张表把常见组合收拢一下排查问题的时候对着看比翻文档快操作状态阻塞模式非阻塞模式open 读无写端阻塞等待立即成功open 写无读端阻塞等待失败errnoENXIOread有数据返回数据返回数据read无数据写端在阻塞等待失败errnoEAGAINread无数据写端全关返回 0EOF返回 0EOFwrite有空间写入写入write无空间读端在阻塞等待失败errnoEAGAINwrite读端全关触发 SIGPIPE触发 SIGPIPE最后一行那个 SIGPIPE 值得单独说。读端全部关闭之后写端再write内核会给进程发SIGPIPE信号这个信号的默认动作是终止进程。也就是说你的程序会毫无征兆地死掉退出码是 141128 13。如果不想被杀有两个办法全局signal(SIGPIPE, SIG_IGN)忽略它这样write会返回-1并设置errno EPIPE你可以正常处理或者用send的MSG_NOSIGNAL但 FIFO 用不上那是 socket 的。我写任何涉及管道的代码都会在main开头加一句忽略 SIGPIPE这是成本最低的保险。4. 两个无亲缘关系进程的完整通信链路4.1 服务端与客户端的最小可跑Demo光看理论容易虚上一份可以直接编译跑通的代码。场景是一个采集端进程不断把时间戳写进 FIFO一个处理端进程读取并打印。两者毫无亲缘关系分别启动。先看写端/* writer.c —— 编译gcc -o writer writer.c */ #include stdio.h #include stdlib.h #include string.h #include unistd.h #include fcntl.h #include time.h #include errno.h #include signal.h #define FIFO_PATH /tmp/demo_channel.fifo int main(void) { signal(SIGPIPE, SIG_IGN); /* 防止读端退出后把自己写死 */ int fd open(FIFO_PATH, O_WRONLY); /* 没有读端时会阻塞在这里 */ if (fd 0) { perror(open for write); return 1; } char buf[128]; for (int i 0; i 5; i) { time_t now time(NULL); int n snprintf(buf, sizeof(buf), tick %d at %ld\n, i, (long)now); if (write(fd, buf, n) ! n) { perror(write); break; } sleep(1); } close(fd); return 0; }读端/* reader.c —— 编译gcc -o reader reader.c */ #include stdio.h #include unistd.h #include fcntl.h #include errno.h #include sys/stat.h #define FIFO_PATH /tmp/demo_channel.fifo int main(void) { if (mkfifo(FIFO_PATH, 0666) -1 errno ! EEXIST) { perror(mkfifo); return 1; } int fd open(FIFO_PATH, O_RDONLY); /* 没有写端时会阻塞 */ if (fd 0) { perror(open for read); return 1; } char buf[256]; ssize_t n; while ((n read(fd, buf, sizeof(buf) - 1)) 0) { buf[n] \0; printf([reader] %s, buf); fflush(stdout); } if (n 0) perror(read); printf([reader] 写端已关闭收到 EOF\n); close(fd); unlink(FIFO_PATH); /* 清理责任放在读端 */ return 0; }跑的顺序是先启动reader它会因为open卡住再启动writer两边的open同时放行数据开始流动writer写完 5 条退出关闭写端reader读到 EOF 后退出并删掉 FIFO 文件。这段代码刻意保留了阻塞式的open因为它最贴近两个进程天然同步的直觉。但实际工程里我更推荐读端用O_NONBLOCK打开再切回阻塞理由前面说过如果客户端起得比服务端早阻塞式open会让服务端启动时直接卡死管理脚本会以为它挂了。4.2 双向通信为什么必须两条FIFOFIFO 是严格单向的。同一个 fd 上O_RDONLY打开就只能读O_WRONLY打开就只能写。想双向标准做法是开两条 FIFO一条上行一条下行#define FIFO_UP /tmp/chan_up.fifo /* 客户端 - 服务端 */ #define FIFO_DOWN /tmp/chan_down.fifo /* 服务端 - 客户端 */服务端打开顺序要注意open(FIFO_UP, O_RDONLY)和open(FIFO_DOWN, O_WRONLY)。客户端则是反的open(FIFO_UP, O_WRONLY)和open(FIFO_DOWN, O_RDONLY)。但如果两边都用阻塞open且顺序一致就会出现经典的打开死锁服务端先阻塞在open(UP, O_RDONLY)等客户端写端客户端先阻塞在open(UP, O_WRONLY)等服务端读端——这俩其实是配对的会通过然后服务端走到open(DOWN, O_WRONLY)等客户端的读端客户端走到open(DOWN, O_RDONLY)等服务端的写端恰好又配上了。所以顺序一致反而能过。真正会死锁的是两边打开顺序相反的情况或者一方用了非阻塞而另一方用了阻塞且没有协调好。最稳妥的方案是双向场景下两端统一先打开上行再打开下行或者干脆全部用O_NONBLOCK打开再逐个fcntl清标志把配对的事情交给应用层的握手协议别依赖open的阻塞语义。我后来所有的双向实现都改成后者因为一旦加进第三个进程或者想动态重连阻塞open的配对关系就彻底失控了。4.3 退出与清理谁负责unlinkFIFO 文件不会因为进程退出就自动消失它一直躺在文件系统上。所以必须有一个明确的责任人负责unlink。我的经验是把它绑在生命周期最长的那一端——通常是服务端服务端启动时创建退出前删除。客户端只负责打开不创建也不删除。这么做有个好处客户端可以很早就启动发现 FIFO 不存在就重试或者等着而服务端的创建/清理时机可控。如果让客户端负责创建多个客户端并发创建会有一堆EEXIST需要处理还得小心 A 客户端把 B 客户端正在用的 FIFO 删了。清理的时机也有讲究。如果服务端要重启unlink之后立刻mkfifo中间有个极短的时间窗此时客户端可能打开失败。所以更好的做法是用信号处理 优雅退出收到SIGTERM之后先关闭 fd等到确认没有客户端在用或者干脆等一小段时间再unlink。粗暴地在atexit里unlink会导致程序崩溃时来不及清理残留文件留给下次启动的EEXIST分支处理这也没什么问题。4.4 用select/poll把单条FIFO接进事件循环单条 FIFO 用阻塞read就够了但一旦程序里还要处理别的 fd比如另一个 socket、一个定时器 fd就必须把 FIFO 塞进poll或者epoll里。FIFO 是完全支持poll的这一点很多人不知道以为只有 socket 能 poll。#include poll.h struct pollfd pfd { .fd fifo_fd, .events POLLIN, .revents 0 }; int ret poll(pfd, 1, 1000); /* 1 秒超时 */ if (ret 0) { if (pfd.revents POLLIN) { /* 有数据可读或者对端全部关闭此时 read 会返回 0 */ } if (pfd.revents POLLHUP) { /* 写端全部关闭 */ } }这里的关键是POLLIN在有数据可读时置位在写端全关时也会置位此时read返回 0。如果代码只判断POLLIN就无脑read遇到 EOF 会陷入读了 0 字节poll 又立刻返回就绪的忙循环CPU 直接飙到 100%。所以必须配合POLLHUP判断或者检查read返回 0 时主动移除 fd。这个忙循环坑我踩过至少两次症状是进程 CPU 拉满但日志里一条数据都没有第一次查了半天以为是数据源的问题。5. 踩坑实录我在FIFO上栽过的几次5.1 打开顺序不对导致的双向死锁最早做双向通信时我沿用了服务端先开读、客户端先开写的直觉结果两边都卡住了。用strace一看两个进程都停在open系统调用上谁也没往前走。原因就是我前面说的配对关系没对上服务端在open(DOWN, O_WRONLY)等读端客户端在open(UP, O_WRONLY)等读端两个都在等谁也不给。排查这类死锁最快的办法是strace -p pid或者cat /proc/pid/stack一眼就能看到卡在哪个open上。修复方案我最后选了全部非阻塞打开 应用层握手/* 统一流程非阻塞打开两个 fifo再恢复阻塞 */ int up open(FIFO_UP, O_RDONLY | O_NONBLOCK); int down open(FIFO_DOWN, O_WRONLY | O_NONBLOCK);注意O_WRONLY | O_NONBLOCK在没有读端时会直接返回ENXIO所以写端的打开要带重试int open_writer_retry(const char *path, int max_retry) { for (int i 0; i max_retry; i) { int fd open(path, O_WRONLY | O_NONBLOCK); if (fd 0) { int flags fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags ~O_NONBLOCK); return fd; } if (errno ! ENXIO) return -1; usleep(100 * 1000); } return -1; }这段重试逻辑看着笨但它把对端可能还没起来这个现实问题显式化了比依赖内核的阻塞语义靠谱得多。5.2 写端全关但读端读不到EOF的怪异现象这个坑前面提过一次值得展开。现象是写端进程明明退出了读端的read却一直不返回 0要么阻塞着要么读到一阵空。用lsof /tmp/xxx.fifo一查会发现有第三个进程持有写端 fd。最常见的三个来源多线程程序里某个线程 fork 出来没关 fd、程序自己复制了 fd 忘记关dup之后要关两份、systemd 或 shell 把 fd 传递给了子进程。尤其是最后一种在写启动脚本的时候特别容易中招——你用后台启动一个进程shell 会把一些 fd 继承下去。排查手法很直接ls -l /proc/*/fd/ 2/dev/null | grep fifo把所有持有该 FIFO 的进程列出来逐个看是谁。定位到之后在对应位置加close()或者在fork之后立刻把不需要的 fd 关掉。我现在写管道的代码fork之后的第一条语句永远是关闭本进程不需要的那一端这是肌肉记忆了。5.3 残留数据与旧管道文件导致的幽灵消息有一回测试环境出现读到重复消息的现象。查下来是两个原因叠加其一测试脚本里mkfifo失败被忽略路径上是个上次留下的 FIFO其二上次的进程还没完全退出还持有旧管道的 fd新进程和它共用同一个路径名却处在两个不同的管道对象上。这个问题的根子在于FIFO 文件的存在性和内核管道对象的生命周期是分离的。文件在管道对象可能早没了文件被删了再重建旧 fd 依然指向旧对象。所以我的处理原则是每次启动都要么复用一个确认干净的 FIFO要么显式 unlink 并保证没有老进程在用。判断有没有人在用可以用fuser命令fuser /tmp/demo_channel.fifo有输出说明有进程持有没输出才安全。这个检查加在启动脚本里成本很低能省掉一堆诡异的调试时间。另外如果消息本身有粘性比如状态同步应用层一定要做去重或者带序号别指望管道传一次就一次。5.4 SIGPIPE进程莫名其妙被杀掉这个是新手最常遇到的程序无声无息退出。症状是程序跑得好好的读端一退出写端进程直接消失日志里什么都没留下退出码 141。原因就是前面说的写端向已关闭读端的管道write会收到SIGPIPE默认动作是终止进程。修复只需要一行#include signal.h signal(SIGPIPE, SIG_IGN);加完之后write会返回-1errno EPIPE你可以打日志、可以重连、可以退出总之可控了。这行代码我建议放在main函数的第一行任何涉及管道、套接字的程序都值得加。有人担心忽略 SIGPIPE 会掩盖问题其实不会——EPIPE是显式的错误码比被信号杀掉清楚多了。顺带一提SIGPIPE在SIG_IGN状态下还能被子进程继承但fork exec之后就重置了所以如果是启动子进程的场景父进程忽略并不能保护子进程得在子进程里再设一次或者在exec之前恢复默认。这个细节大多数人不知道我是在一次守护进程带子进程的场景里踩出来的。6. 工程化使用建议什么时候该用FIFO什么时候别用6.1 适合FIFO的三类场景第一类是命令行工具串联。比如一个采集程序持续输出 JSON 行一个消费程序逐行处理中间用 FIFO 连起来两个程序可以独立启动、独立重启比写一个大的管道命令灵活得多。这种场景数据的产生是流式的不需要严格的请求-应答。第二类是进程唤醒与简单同步。往 FIFO 里写一个字节读端读到就表示有新任务了。这种用法比信号可靠不会丢比条件变量跨进程简单不需要共享内存。我做定时任务触发器的时候常用这招控制脚本往 FIFO 写一个字节守护进程读到一个字节就扫一遍任务目录。第三类是轻量级单向日志转发。程序把结构化日志写进 FIFO一个独立的收集进程读取并落盘或转发。好处是收集进程挂掉不会阻塞业务配合O_NONBLOCK和丢弃策略业务代码里也不需要考虑网络重连这些破事。但要注意缓冲区满了之后写入会阻塞或者失败所以这种场景下写端一定要用非阻塞模式并在失败时丢弃不能因为日志把主业务卡住。6.2 FIFO撑不住的场景与替代方案FIFO 的短板也很明确我列几个明确不该用的场景。需要消息边界和请求应答的。FIFO 是字节流没有边界也不方便做多路复用应答。这种场景直接上 UNIX 域套接字AF_UNIX它支持SOCK_SEQPACKET保边界也支持双向还能传 fd。代码量增加有限但语义清楚太多。多生产者高频并发的。虽然 ≤PIPE_BUF的写是原子的但一旦有并发写、数据量大就得自己处理交错和背压。这种场景要么一人一根 FIFO要么换成消息队列。需要跨机器或者需要持久化的。FIFO 只在本机文件系统上数据只在内存里进程一死全丢。需要跨机器就得走网络需要持久化就得落盘或者用带持久化的队列。对端可能长时间不在线的。阻塞式open会让你的进程卡死非阻塞式的重试逻辑写起来也不轻松。这种异步场景用 socket 配合epoll反而更自然。一个粗略的判断原则FIFO 适合两端都在、单向、流式、不求应答的短路径通信。只要需求偏离这几点就值得重新考虑方案。6.3 一组可以直接抄的封装骨架反复写过几次之后我把常用逻辑收敛成了一个小封装这里给出关键接口设计你可以照着改成自己项目里的样子函数作用关键点fifo_create(path, mode)幂等创建处理 EEXIST校验 S_ISFIFOfifo_open_reader(path, nonblock)打开读端非阻塞打开后可选切回阻塞fifo_open_writer(path, timeout_ms)打开写端带重试处理 ENXIOfifo_read_full(fd, buf, len)按需读满处理 EINTR 和部分读fifo_writer_safe(fd, buf, len)安全写处理 EPIPE、部分写fifo_cleanup(path)清理检查无使用者后再 unlink写端打开那一条最值得封装因为ENXIO重试逻辑散落在业务代码里特别难看。读端的fifo_read_full也要写好循环ssize_t fifo_read_full(int fd, void *buf, size_t len) { size_t got 0; char *p buf; while (got len) { ssize_t n read(fd, p got, len - got); if (n 0) { if (errno EINTR) continue; if (errno EAGAIN) break; /* 非阻塞模式有多少给多少 */ return -1; } if (n 0) break; /* EOF */ got n; } return (ssize_t)got; }注意这个函数在非阻塞模式下会读多少算多少而不是死等凑满这是因为管道本身不保证消息边界非要凑满反而可能永远凑不满。真要按固定长度解析得自己在应用层维护一个缓冲区把读到的碎片拼起来这是另一个话题了。6.4 性能与调试strace、/proc下的观察点调 FIFO 问题几个工具基本能覆盖九成场景。strace -e traceopen,openat,read,write,close -p pid能看到进程在哪个调用上卡住是open阻塞还是read没数据一目了然。注意strace会拖慢程序生产环境慎用测试环境随便玩。ls -l /proc/pid/fd/能看到进程打开了哪些 fd配合readlink能确认某个 fd 指向哪个 FIFO。查谁还持有写端的时候把这条命令在可疑进程上跑一遍比猜快得多。cat /proc/sys/fs/pipe-max-size看缓冲区上限fcntl(fd, F_GETPIPE_SZ)看当前缓冲区实际大小。如果是写入频繁卡顿的场景先看这两个值很多时候调大缓冲区就顺手了。性能上FIFO 的延迟确实低一次write 一次read的往返基本就是两次系统调用的开销几十微秒量级。但它只在单机有效而且每次传输都要过内核缓冲区拷贝一次数据量特别大的时候比如几百 MB 的文件传输就不是它的用武之地了那种场景应该考虑共享内存或者直接走文件。我的经验是单条消息在几十字节到几 KB 之间、QPS 在几千以内FIFO 完全够用再往上就得测一测了。最后分享一个我自己的使用习惯所有涉及 FIFO 的代码我都会在启动时打三行日志——FIFO 路径、当前进程的 pid、以及是读端还是写端。这三行看似废话但在多方联调、进程重启、清理残留的时候能省掉大量到底是谁先起来的这类扯皮。管道这种老东西功能几十年没变过坑也几十年没变过把这几条守住基本就不会出大事。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询