十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux进程间通信:FIFO命名管道原理、阻塞与原子写实践

Linux进程间通信:FIFO命名管道原理、阻塞与原子写实践 这次我们来看 Linux 进程间通信里一个非常基础、但常常被忽略的角色FIFO命名管道。和匿名管道相比FIFO 最大的优势是它有一个文件系统入口可以让两个没有亲缘关系的进程通过同一个文件名完成数据传递。很多做嵌入式 Linux、服务端中间件、运维脚本的人都会在某个时刻遇到它但真正能把“创建、打开、阻塞、原子写、双向通信、内核实现”一条链路讲清楚的文章并不多。这篇文章的目标是把 FIFO 讲透同时给你一套可以直接编译运行的 C 代码示例。内容覆盖从mkfifo命令、mkfifo()系统调用、阻塞与非阻塞模式、PIPE_BUF原子性边界到双向通信和多进程任务分发。文末还会从源码层面说明 FIFO 在内核里是怎么工作的以及一份常见问题排查清单。先说结论FIFO 的机制很简单但它给出的设计约束非常明确。单条 FIFO 是单向字节流默认读写都是阻塞的单次写入不超过PIPE_BUF的时候才保证原子。如果你理解这三点FIFO 用起来基本不会踩大坑。1. FIFO 核心能力速览先把 FIFO 的关键能力放在前面方便你快速判断它适不适合你的场景。能力项说明机制类型命名管道Named PipeUnix/Linux IPC 中经典的本地进程间通信方式数据方向单向字节流先进先出双向通信需要创建两条 FIFO通信范围仅限同一主机上的进程不能跨网络创建方式mkfifo命令或mkfifo()系统调用打开特性默认阻塞可指定O_NONBLOCK切换为非阻塞模式原子性单次写入不超过PIPE_BUF通常为 4096 字节时写操作是原子的存储位置数据只经过内核管道缓冲区不占用普通磁盘文件空间适用场景小数据量、无固定消息边界、生产消费模型、多进程任务分发FIFO 与匿名管道最直观的区别在“命名”这两个字上。匿名管道只能由pipe()创建通常用于父子进程之间FIFO 通过文件系统路径暴露给所有进程所以两个完全无关的进程也能通过同一个路径建立连接。你可以把 FIFO 理解成“带文件名的管道”但它仍然是一段内核内存缓冲区不是普通的磁盘文件。2. 适用场景与使用边界FIFO 适合做这几类事情同一台机器上的两个进程之间传小数据量消息。生产者和消费者模型比如一个日志采集进程写 FIFO另一个进程消费。多个 worker 进程共同消费一个任务队列例如批量任务分发。临时把两个本来没有关联的程序串起来类似 shell 管道但不需要同时启动。FIFO 不适合做的事情也很明确跨主机通信。FIFO 没有网络协议栈不提供 TCP/IP 那样跨节点能力跨机器应该用 Socket。大流量高并发。FIFO 基于内核缓冲区默认缓冲区容量有限频繁读写会出现阻塞。大数据量传输用共享内存 信号量更合理。复杂结构化消息。FIFO 只是字节流没有消息边界复杂协议需要自己定义头部、长度、校验。多消费者精确路由。多个进程同时读同一个 FIFO 时数据会被内核随机分配给其中一个读者不是广播也不能精确指定路由。使用边界上还要注意合规和权限问题。FIFO 是本地机制文件权限由mkfifo时的 mode 位决定。如果权限设置过宽其他非授权用户也能打开这个管道读写数据。涉及敏感信息时FIFO 本身不加密传输内容应该由上层做加密处理。如果项目使用场景涉及用户数据、版权内容或生产环境务必先确认授权和使用边界不要在未确认的环境里传输敏感数据。3. 环境准备与前置条件FIFO 不需要安装额外软件包Linux 内核直接支持。按以下清单检查环境即可。3.1 操作系统任何主流 Linux 发行版都可以比如 Ubuntu、Debian、CentOS、openEuler。Windows 下通过 WSL 也能运行但更推荐在原生 Linux 或虚拟机里测试避免文件系统和进程模型上的差异影响验证结果。3.2 编译工具本文的代码都用 C 语言编写需要安装 gcc。gcc --version如果没有安装在 Debian/Ubuntu 上执行sudo apt update sudo apt install -y gcc make在 CentOS/RHEL 上执行sudo yum install -y gcc make3.3 辅助工具下面这些工具不是必需的但在排查和性能观察时很有用。which mkfifo strace timeout dd od如果缺少strace按发行版安装。Ubuntu 使用sudo apt install -y strace3.4 内核与权限FIFO 依赖内核的管道实现主线内核默认打开不需要加载额外内核模块。测试目录建议放在/tmp下需要确认当前用户对目录有读写权限。ls -ld /tmp4. 创建与删除 FIFO先让文件系统里出现一个管道FIFO 的使用第一步是创建管道文件。注意这里说的“文件”只是入口实际数据不落盘。4.1 用 mkfifo 命令创建先看命令行方式mkfifo /tmp/my_fifo创建后查看文件类型ls -l /tmp/my_fifo预期输出类似prw-r--r-- 1 user user 0 Jan 1 12:00 /tmp/my_fifo注意第一个字符是p表示 named pipe。普通文件是-目录是d。文件大小显示为 0这是正常的因为 FIFO 不把数据保存到磁盘。4.2 用 mkfifo() 系统调用创建在 C 代码里使用mkfifo()#include stdio.h #include stdlib.h #include sys/types.h #include sys/stat.h #include errno.h #define FIFO_PATH /tmp/my_fifo int main(void) { if (mkfifo(FIFO_PATH, 0666) -1) { if (errno EEXIST) { printf(FIFO already exists: %s\n, FIFO_PATH); } else { perror(mkfifo); exit(EXIT_FAILURE); } } else { printf(FIFO created: %s\n, FIFO_PATH); } return 0; }编译运行gcc -o create_fifo create_fifo.c ./create_fifomkfifo()的第二个参数是权限位0666表示所有用户可读写但最终权限还要受到进程umask的影响。如果想让权限更精确先调用umask(0)再mkfifo()。4.3 删除 FIFOFIFO 在使用完后应该清理。删除方式和普通文件一样rm /tmp/my_fifoC 代码里用unlink()#include stdio.h #include unistd.h int main(void) { if (unlink(/tmp/my_fifo) -1) { perror(unlink); return 1; } printf(FIFO removed\n); return 0; }在服务端或长驻进程里建议在退出时统一清理临时 FIFO 文件避免下次启动时因为文件残留导致状态混乱。5. 单向通信一读一写的最基础模型FIFO 最常见的使用方式是一条管道配一个读端和一个写端数据从写端流向读端。------------- write(fd) ------------- | writer 进程 | -------------------- | /tmp/my_fifo | -------------------- 读端 read(fd) ------------- ------------- reader 进程5.1 写端代码// fifo_write.c #include fcntl.h #include stdio.h #include string.h #include unistd.h #include sys/stat.h #define FIFO_PATH /tmp/my_fifo int main(void) { const char *msg Hello from FIFO writer!\n; int fd open(FIFO_PATH, O_WRONLY); if (fd -1) { perror(open write); return 1; } ssize_t len write(fd, msg, strlen(msg)); if (len -1) { perror(write); close(fd); return 1; } printf(writer wrote %zd bytes\n, len); close(fd); return 0; }5.2 读端代码// fifo_read.c #include fcntl.h #include stdio.h #include unistd.h #include sys/stat.h #define FIFO_PATH /tmp/my_fifo int main(void) { char buf[128]; ssize_t n; int fd open(FIFO_PATH, O_RDONLY); if (fd -1) { perror(open read); return 1; } while ((n read(fd, buf, sizeof(buf) - 1)) 0) { buf[n] \0; printf(read: %s, buf); } close(fd); return 0; }5.3 编译与运行顺序编译两个程序gcc -o fifo_write fifo_write.c gcc -o fifo_read fifo_read.c先启动读端./fifo_read此时读端会阻塞在open()调用上因为还没有进程以写模式打开 FIFO。再开一个终端启动写端./fifo_write写端打开 FIFO 后写出的字符串被读端读取并打印随后读端继续阻塞在read()上等待下一次数据。如果没有更多数据并且所有写端 fd 关闭读端read()会返回 0也就是 EOF循环结束。5.4 必须理解的阻塞行为FIFO 默认是阻塞的具体表现是如果当前没有进程以读模式打开 FIFO那么写方open(O_WRONLY)会阻塞直到有读方出现。如果当前没有进程以写模式打开 FIFO那么读方open(O_RDONLY)会阻塞直到有写方出现。所以上面的测试先启动读端能让流程更顺畅。如果先启动写端写端会一直挂在open()上看起来像死循环这就是最常见的“FIFO 卡住”现象。另外注意如果只有读端打开 FIFO写端一直没有启动那么读端读取时也会阻塞等待数据。read()只有在写端完全关闭后才会返回 0不能把 “阻塞中” 和 “EOF” 混淆。6. 阻塞与非阻塞模式默认的阻塞模式逻辑简单但工程上需要避免进程永久挂起。非阻塞模式通过O_NONBLOCK标志控制。6.1 open 时的非阻塞行为使用O_NONBLOCK后open()的行为变化如下open 标志只有读端没有写端只有写端没有读端O_RDONLY默认open 阻塞等待写端-O_RDONLY | O_NONBLOCKopen 立即成功-O_WRONLY默认-open 阻塞等待读端O_WRONLY | O_NONBLOCK-open 立即失败errno 为 ENXIO这个行为在 POSIX 中有明确规定多平台通用。6.2 非阻塞读当读端以O_RDONLY | O_NONBLOCK打开 FIFO 后如果没有数据可读read()会立即返回 -1errno 设置为EAGAIN。程序可以通过循环轮询也可以配合select、poll、epoll等待可读事件。下面是一段非阻塞读的简化示例// fifo_read_nonblock.c #include fcntl.h #include stdio.h #include unistd.h #include errno.h #define FIFO_PATH /tmp/my_fifo int main(void) { char buf[128]; int fd open(FIFO_PATH, O_RDONLY | O_NONBLOCK); if (fd -1) { perror(open); return 1; } for (int i 0; i 5; i) { ssize_t n read(fd, buf, sizeof(buf) - 1); if (n 0) { buf[n] \0; printf(read %zd bytes: %s, n, buf); } else if (n -1 errno EAGAIN) { printf(no data yet, retry %d\n, i 1); } sleep(1); } close(fd); return 0; }这种写法在测试环境里可以看到周期性的“no data yet”说明非阻塞模式生效。实际生产环境不建议用轮询而是用poll或epoll挂载事件读取端只在有数据时被唤醒。6.3 非阻塞写的注意事项写端也可以使用O_NONBLOCK。当管道缓冲区已满时非阻塞写会立即返回 -1 和EAGAIN。这在批量写入大消息时很有用可以避免某个进程因为另一个进程消费慢而长期卡住。7. PIPE_BUF 与原子写多个写者会冲突吗FIFO 虽然简单但在多写者场景下有一个重要边界PIPE_BUF。PIPE_BUF在 Linux 头文件/usr/include/linux/limits.h中通常定义为 4096即#define PIPE_BUF 4096含义是当写进程单次写入的字节数不超过PIPE_BUF时这次写操作是原子的。多个写进程同时写入 FIFO数据不会互相穿插。也就是说一次不超过 4096 字节的写操作要么完整写入其他写者的数据不会夹在中间。如果单次写入超过PIPE_BUF内核允许把这次写分成多段。多个写者并发写入时读取方可能看到不同写者数据交错无法通过 FIFO 本身区分数据来自谁。7.1 验证原子性可以写两个写进程一个持续写入“AAAA...”另一个持续写入“BBBB...”每次写入 4096 字节观察读取方拿到的数据块是否完整全是 A 或全是 B。再把每次写入改成 8192 字节重复测试大概率会看到 A 和 B 混杂在一个数据块里。结论对协议设计的影响很大如果多个进程共享同一个 FIFO建议把单条消息控制在PIPE_BUF以内并且使用固定长度消息或自带长度字段的协议结构比如| 4 字节消息长度 | 消息体 |这样读取方可以先读长度再读对应字节数从而正确切分消息。8. 双向通信与多进程任务分发单条 FIFO 是单向的。如果要让两个进程互相通信至少需要两条 FIFO一条正向一条反向。8.1 双向通信拓扑进程 A 写端 - /tmp/fifo_c2s - 进程 B 读端 进程 B 写端 - /tmp/fifo_s2c - 进程 A 读端创建两条 FIFOmkfifo /tmp/fifo_c2s /tmp/fifo_s2c8.2 服务端代码服务端先读客户端发来的数据再向客户端回复。需要注意打开顺序避免两个进程互相等待。// fifo_server.c #include fcntl.h #include stdio.h #include string.h #include unistd.h #define C2S /tmp/fifo_c2s #define S2C /tmp/fifo_s2c int main(void) { char buf[256]; ssize_t n; int rd open(C2S, O_RDONLY); if (rd -1) { perror(open C2S); return 1; } int wd open(S2C, O_WRONLY); if (wd -1) { perror(open S2C); return 1; } while ((n read(rd, buf, sizeof(buf) - 1)) 0) { buf[n] \0; printf(server got: %s, buf); const char *ack ack from server\n; if (write(wd, ack, strlen(ack)) -1) { perror(write ack); break; } } close(rd); close(wd); return 0; }8.3 客户端代码客户端先打开发送方向的写端再打开发送方向的读端// fifo_client.c #include fcntl.h #include stdio.h #include string.h #include unistd.h #define C2S /tmp/fifo_c2s #define S2C /tmp/fifo_s2c int main(void) { int wd open(C2S, O_WRONLY); if (wd -1) { perror(open C2S write); return 1; } int rd open(S2C, O_RDONLY); if (rd -1) { perror(open S2C read); return 1; } const char *msg hello server\n; write(wd, msg, strlen(msg)); char buf[64]; ssize_t n read(rd, buf, sizeof(buf) - 1); if (n 0) { buf[n] \0; printf(client got: %s, buf); } close(wd); close(rd); return 0; }编译并测试gcc -o fifo_server fifo_server.c gcc -o fifo_client fifo_client.c ./fifo_server另一个终端执行./fifo_client预期服务端打印hello server客户端打印ack from server。注意这里的打开顺序是设计过的服务端先open(C2S, O_RDONLY)被阻塞后客户端open(C2S, O_WRONLY)可以成功返回之后双方继续打开反向 FIFO。如果两个进程都先打开自己的读方向再打开写方向就可能互相等待形成死锁。8.4 多进程批量任务分发FIFO 非常适合做简单的任务队列。一个 master 进程写任务多个 worker 进程读任务并处理。典型结构mkfifo /tmp/jobsmaster 负责写入任务for i in $(seq 1 100); do echo task-$i /tmp/jobs doneworker 读取任务// fifo_worker.c #include fcntl.h #include stdio.h #include unistd.h #include string.h #define JOBS /tmp/jobs int main(void) { int fd open(JOBS, O_RDONLY); if (fd -1) { perror(open jobs); return 1; } char line[256]; ssize_t n; while ((n read(fd, line, sizeof(line) - 1)) 0) { line[n] \0; // 简单处理按行切分任务 char *save NULL; char *token strtok_r(line, \n, save); while (token ! NULL) { printf(worker %d process: %s\n, getpid(), token); token strtok_r(NULL, \n, save); } } close(fd); return 0; }这里需要特别理解多个 worker 同时读取同一个 FIFO 时FIFO 是竞争读不是广播。内核会把管道缓冲区中的数据分给不同的读取进程一个进程读到一条消息后其他进程不会再读到同一条消息。也就是说FIFO 天然支持“一条任务只被一个 worker 处理”的分发逻辑但不支持“所有 worker 都收到同一份消息”。9. 源码视角FIFO 在内核里是怎么工作的FIFO 在内核中的实现并没有单独的复杂算法它和匿名管道共用一套管道机制。9.1 主要源码位置Linux 内核源码中与 FIFO 相关的核心文件是fs/fifo.c负责 FIFO 打开和相关逻辑。fs/pipe.c匿名管道和命名管道共用的管道读写、缓冲区管理逻辑。include/linux/pipe_fs_i.h管道相关数据结构定义。9.2 FIFO 打开过程当进程调用open()打开一个 FIFO 文件时内核会识别 inode 类型为 FIFO走fifo_open()。它会为这个 FIFO 初始化一个pipe_inode_info结构里面包含管道缓冲区、读写等待队列、读写进程计数等信息。如果打开时没有指定O_NONBLOCKfifo_open()会根据当前是读端还是写端等待另一个方向的进程打开。这个等待过程就是前面测试时看到的open()挂住。9.3 读写与阻塞唤醒写入数据时写路径最终进入pipe_write()。如果管道缓冲区有空间数据被拷贝到缓冲区如果缓冲区满了写进程进入等待队列睡眠等读进程取走数据后唤醒。读取数据时读路径进入pipe_read()。如果缓冲区为空读进程进入等待队列睡眠等写进程写入数据后唤醒。这种“睡眠等待 事件唤醒”的模型就是 Linux 内核里基于等待队列wait queue的经典用法。理解这一点再看poll、epoll对 FIFO 的支持就会更容易它们本质上是把“FIFO 可读、可写”这个事件注册到进程自己关注的事件集合里。9.4 可观察的内核参数系统中管道容量的上限可以通过下面这个文件查看cat /proc/sys/fs/pipe-max-size单个管道当前的实际缓冲区大小可以在程序里用fcntl查询#include fcntl.h #include stdio.h #include sys/stat.h #include unistd.h int main(void) { int fd open(/tmp/my_fifo, O_RDONLY | O_NONBLOCK); if (fd -1) { perror(open); return 1; } int size fcntl(fd, F_GETPIPE_SZ); printf(pipe size: %d bytes\n, size); close(fd); return 0; }不同内核和发行版默认值可能不同常见的默认值是 64 KiB 左右。可以用F_SETPIPE_SZ修改但不能超过pipe-max-size的限制。这个知识点在排查“FIFO 写入慢、读取慢”的时候非常有用如果生产者写入速度快消费者读取速度慢那么缓冲区很快被打满写进程会持续阻塞。10. 资源占用与性能观察手法FIFO 的数据不落盘所以观察它不能用看普通文件大小的办法。10.1 文件大小始终为 0当你执行ls -l /tmp/my_fifo看到大小是 0这不是异常。即使有数据正在流动文件大小也是 0因为数据存在于内核内存缓冲区中不进入文件系统存储层。10.2 用 strace 观察阻塞用strace可以看清楚阻塞发生在哪个系统调用上strace -f -e traceopen,read,write ./fifo_read预期输出中会看到open(/tmp/my_fifo, O_RDONLY一直停在等待状态。这说明进程阻塞在open()上等待写端打开 FIFO。10.3 用 dd 做简单的读写测试FIFO 写数据timeout 5 dd if/dev/zero of/tmp/my_fifo bs4096 count100FIFO 读数据timeout 5 dd if/tmp/my_fifo of/dev/null bs4096 count100注意timeout可以避免命令因为没有对端而永久挂起。测试时可以先启动读取进程再启动写入进程。如果需要观察实时吞吐可以对比dd输出的拷贝速率。瓶颈通常来自读进程消费速度不够或者单次写入超过了PIPE_BUF导致的分段处理。10.4 降低阻塞影响的通用思路写端单次写入控制在PIPE_BUF内。读端使用poll/epoll事件驱动而不是忙轮询。如果消息体积较大可以先用一个小控制 FIFO 发通知再用共享内存或普通文件传大块数据。所有打开 FIFO 的进程要保证成对出现否则会互相阻塞造成服务假死。11. 常见问题与排查方法下面表格整理了 FIFO 使用过程中的典型问题、现象、原因和解决方式。问题现象可能原因排查方式解决方案open()一直卡住对端进程没有打开 FIFO或打开顺序不对用strace看卡在哪个系统调用检查对端是否启动调整打开顺序或用O_NONBLOCK避免永久阻塞write()导致进程退出读端已经全部关闭写进程收到SIGPIPE检查读端是否退出用ps查看进程状态signal(SIGPIPE, SIG_IGN)再处理write()返回的EPIPEread()返回 0所有写端 fd 已关闭FIFO 读到 EOF检查写端是否正常关闭如果需要持续通信写端不要关闭 fd或重新打开 FIFO 等待新的写端FIFO 文件大小一直为 0正常FIFO 数据不落盘用strace观察数据是否流动无需处理多写者数据互相穿插单次写入超过了PIPE_BUF检查单次写入长度控制消息长度在 4096 字节以内或用自定义消息头非阻塞读一直返回EAGAIN当前没有数据可读确认写端是否写入、是否被缓存配合select/poll/epoll等待事件重启后 open 报错FIFO 文件残留但文件类型不对ls -l查看文件类型删除旧文件后重新mkfifo权限不够创建 FIFO 时 mode 被umask限制查看umask和文件权限调整umask或使用chmod这里要特别强调SIGPIPE。写端往一个已经没有读端的 FIFO 里写数据时内核会向写进程发送SIGPIPE信号默认行为是终止进程。很多程序“莫名其妙退出”实际上就是忽略了这层语义。正确做法是在服务端程序里忽略SIGPIPEsignal(SIGPIPE, SIG_IGN);忽略之后write()会失败并返回 -1errno为EPIPE程序可以据此做清理。12. 最佳实践与使用建议12.1 消息结构要提前设计FIFO 不保留消息边界。多进程写入时如果不做协议设计读端拿到的只是字节流没有“哪几个字节属于一条消息”的概念。最简单的方案是固定长度消息每条消息长度相同。更通用的是“长度前缀 消息体”先读固定 4 字节长度再读对应内容。12.2 打开顺序要固定多进程打开同一个 FIFO 时建议统一约定服务端先读客户端先写避免双方都在等待对方打开 FIFO 造成死锁。如果确实无法控制顺序就使用O_NONBLOCK加重试机制。12.3 生产环境加入超时与清理在工程代码里不要把 FIFO 的阻塞行为当成默认可靠行为。长期运行的进程应使用poll/epoll做超时控制避免闲置连接把资源占住。程序退出时要清理临时 FIFO 文件避免下次启动误判状态。12.4 权限与安全边界创建 FIFO 时建议明确权限位不要无脑使用0666。如果只允许当前用户使用使用0600mkfifo(/tmp/my_fifo, 0600);涉及生产数据、用户内容或版权素材时FIFO 本身不提供加密和鉴权。本地管道只适合可信任环境内的进程间协作。如果数据敏感应在应用层加密并严格控制访问权限。13. 总结与下一步FIFO 是理解 Linux IPC 的一个很好的起点。它不复杂但把“文件系统入口 内核缓冲区 阻塞唤醒”这几个概念串在了一起。读完这篇文章你最先应该验证的是两个行为读端先启动时open()的阻塞表现以及多写者不超过PIPE_BUF时数据的原子性。这两个实验跑通FIFO 的核心语义就掌握了一大半。最容易踩的坑有两个一是忽略open()的阻塞匹配导致进程看起来“挂死”二是忽略SIGPIPE读端关闭后写进程直接退出。把这两个问题提前处理掉FIFO 在工程里基本不会出大错。接下来的学习方向可以考虑匿名管道与pipe()的对比、poll/epoll多路复用 FIFO 事件、Unix domain socket 在本地双向通信中的优势以及共享内存和消息队列分别适合哪种数据规模。每一步都能和 FIFO 的知识形成对照建议在实际项目里各写一个小工具验证比只看理论记忆要牢得多。
返回列表