管道是 Doug McIlroy 1972 年的设计,让 Unix 程序能用
| 串起来。50 多年后,它仍然是 Linux
上用得最多的 IPC。但”管道是一条 FIFO
字节流”这个抽象藏着几个陷阱,生产代码里经常踩。
本文讲管道家族三兄弟:匿名管道、命名管道(FIFO)、socketpair;再讲 splice 这个管道的原生加速器。
一、先看图:管道内部是环形页
flowchart LR
W[write end<br/>struct file] -->|pipe_write| H[pipe_inode_info]
R[read end<br/>struct file] -->|pipe_read| H
H --> B0[pipe_buffer 0<br/>→ page]
H --> B1[pipe_buffer 1<br/>→ page]
H --> B2[pipe_buffer 2<br/>...]
H --> BN[pipe_buffer N-1]
S[splice from fd] -.零拷贝.-> B1
classDef k fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef z fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class W,R,H,B0,B1,B2,BN k
class S z
pipe_inode_info 持有一个
pipe_buffer 环(默认 16 个 slot,可
fcntl(F_SETPIPE_SZ) 调,上限
/proc/sys/fs/pipe-max-size),每个 slot
指向一页内存。read 从 head slot 起消费、write 往 tail
塞。
这意味着:
- 管道容量 = slot 数 × 页大小(默认 64KB @ 16 × 4KB)
- 页本身可以是从别的 inode 借来的(splice 场景)
- 写入小于 PIPE_BUF(Linux 上 4KB)的数据是原子的——不会被别的 writer 交织
二、pipe(2) / pipe2(2)
int fds[2];
pipe2(fds, O_CLOEXEC | O_NONBLOCK);
// fds[0] 读端,fds[1] 写端永远用 pipe2 带
O_CLOEXEC——pipe(2) 不带是
fork-exec 漏 fd 的经典原因。
半双工:每个 fd 只能一向。需要双向用 socketpair 或 pipe 两条。
2.1 shell 的
cmd1 | cmd2
shell 逻辑:
pipe2(fds)fork- 子
1:
dup2(fds[1], 1); close(fds[0]); close(fds[1]); exec(cmd1) - 子
2:
dup2(fds[0], 0); close(fds[0]); close(fds[1]); exec(cmd2) - 父:
close(fds[0]); close(fds[1]); wait...
任何一端忘关就会 hang——经典 “cat 不终止” 的源头。
三、读写语义的细节
3.1 EOF / SIGPIPE
- read 端:所有 write 端关闭 → read 返回 0(EOF)
- write 端:所有 read 端关闭 → write 返回 EPIPE 并且内核送 SIGPIPE
前面 B-13 讲过,网络服务要 SIG_IGN SIGPIPE。子进程/管道链同理。
3.2 PIPE_BUF 原子写
POSIX 规定:小于等于 PIPE_BUF 字节的 write 到管道是原子的(Linux 上 PIPE_BUF = 4096)。多 writer 并发写 < 4KB 不会交织。
大于 PIPE_BUF 就可能被切,读端看到的是任意拼接。日志聚合(每条日志 < 4KB)就靠这个。
3.3 阻塞 vs 非阻塞
- 阻塞 read 在空管道上挂起
- 阻塞 write 在满管道上挂起
- 非阻塞:返回 EAGAIN
非阻塞 write 到满管道:返回 EAGAIN;不会写一部分(除非 > PIPE_BUF 的部分写)。
3.4 读写的边界
管道是字节流,不保留消息边界。每次 read 可能读到任意字节数。要消息边界自己加分隔符或 length-prefix。
socketpair(SOCK_DGRAM) 保留消息边界。
四、FIFO:命名管道
mkfifo(2):在文件系统里建一个 FIFO
节点。不同进程 open 同一路径,读写通道就连上。
mkfifo /tmp/log
cat /tmp/log & # reader
echo hello > /tmp/log # writer坑:默认 open(O_RDONLY)
会阻塞直到有 writer;open(O_WRONLY) 阻塞到有
reader。用 O_NONBLOCK 避免。
FIFO 在容器日志采集、简单 IPC、debug dump 里偶尔用。大规模系统一般直接 socket。
五、socketpair(2)
socketpair(AF_UNIX, SOCK_STREAM, 0, fds)
创建一对双向连接的 Unix socket。
优势于 pipe:
- 双向(每端都能读写)
- 可送 SOCK_DGRAM 保留消息边界
- 支持 SCM_RIGHTS 传 fd(见 5.1)
- 支持 credentials 传递(SO_PASSCRED)
成本:Unix socket 栈比 pipe 重。不过 sendmsg/recvmsg 已经优化到很接近。
5.1 SCM_RIGHTS:跨进程传 fd
Unix socket 独占能力:通过 SCM_RIGHTS
辅助数据传 fd,内核把 fd 号复制到接收端(是内核对象 struct
file 的新引用,不是号码翻译)。
这是 systemd socket activation、podman、WSL2、docker-containerd 控制面、Chromium 多进程架构的基石。
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &send_fd, sizeof(int));
sendmsg(sock, &msg, 0);接收端 recvmsg 得到新 fd 号,二者指向同一
struct file(同一 offset、flags)。
六、splice:管道的原生加速
splice(2) 把数据从一个 fd 移到另一个
fd,至少一端必须是 pipe。在 kernel
里它直接”转移页引用”,零拷贝。
splice(file_fd, NULL, pipe_w, NULL, len, SPLICE_F_MOVE);
splice(pipe_r, NULL, socket_fd, NULL, len, SPLICE_F_MOVE);典型场景:sendfile
的通用版。nginx、ffmpeg、bpftrace 的 pipe 桥。
相关:
tee(2):从一个 pipe 复制到另一个 pipe(页引用复制,不 copy 字节)vmsplice(2):用户内存 → pipe(危险——解决 aliasing 困难,后被 CVE 推动修订)
splice/tee 在 storage/网络百科 “零拷贝” 专题里深入,不重复。
七、pipe 容量与背压
默认 16 页 = 64KB。写快读慢 → 写端阻塞或返回 EAGAIN。这是天然 back-pressure:
slow_producer | fast_consumer # 消费者一直等
fast_producer | slow_consumer # 生产者被阻塞在 write,自然限速F_SETPIPE_SZ 可把容量扩到几百
MB,但默认不建议调大——大 buffer
延迟抖动大,内存压力也大。
八、PIDFD vs FIFO 通知
传统写法:“守护进程 mkfifo,客户端写触发命令”。问题:
- 没有 ACL 细粒度
- 要解析命令字符串
- 没消息边界
现代替代:
- Unix socket + JSON/protobuf(大多数守护进程)
systemd的 socket activation + sd_notify- d-bus
FIFO 在现代代码里几乎只在极简 shell 脚本留用。
九、性能基准(典型)
(参考:Linux 6.1、Xeon 8360Y、glibc 2.36、同 NUMA 节点)
| 机制 | 吞吐 | 延迟(RT) |
|---|---|---|
| pipe 1KB | ~4 GB/s | ~3 μs |
| socketpair STREAM 1KB | ~3 GB/s | ~4 μs |
| socketpair DGRAM 1KB | ~2 GB/s | ~5 μs |
| splice file→pipe→socket | 达到磁盘/网卡上限 | — |
注意:微基准易误导,真实程序里 syscall 数量、上下文切换、cgroup 开销往往决定瓶颈。
十、常见 bug
bug A:写端从不关管道,读端 hang
原因:缺少
close(fds[1])(shell pipeline 也一样)
bug B:两个 writer 并发写 > 4KB,读端看到拼接 原因:PIPE_BUF 原子边界 解决:自己加锁或限每次 write ≤ 4KB
bug C:popen 子进程有时僵尸
原因:pclose 不调 → 不 reap
解决:显式 pclose
bug D:O_CLOEXEC 漏了,子进程继承 fd
导致父关了对端还收不到 EOF 原因:fork-exec
间 fd 泄漏;新子也持 write 端,read 端永远收不到 EOF
解决:pipe2(..., O_CLOEXEC) +
exec 前 close 不必要的 fd
bug E:往 FIFO 写触发 SIGPIPE
原因:reader 断了
解决:signal(SIGPIPE, SIG_IGN)
或 send(..., MSG_NOSIGNAL)
十一、小结
- 管道在内核里是环形 pipe_buffer,不是字节流
- PIPE_BUF 原子写、阻塞写背压是设计特性
- FIFO 便宜但粗糙;socketpair 功能全面;splice 是零拷贝加速
- SCM_RIGHTS 传 fd 是现代多进程架构的基石
- 所有场景都要
O_CLOEXEC与SIGPIPE处理
下一篇 B-15 讲共享内存:SysV shm、POSIX shm、memfd + seal 的现代范式。
参考文献
- Kerrisk, M. The Linux Programming Interface, Ch. 44-46, 53-55
- Linux source:
fs/pipe.c、fs/splice.c、net/unix/af_unix.c - Bovet & Cesati, Understanding the Linux Kernel, Ch. 19 “Process Communication”
- Corbet, J. “Splicing and iovs.” LWN.net 2006
- Corbet, J. “Passing file descriptors with SCM_RIGHTS.” LWN.net 2016
pipe(7)、fifo(7)、unix(7)man pages
工具
lsof -p <pid>—— 看 fd 类型(pipe/unix-socket)ss -xp—— Unix socket 全局视图strace -e %desc—— fd 操作追踪perf trace—— syscall 带延迟pipemeter、pv—— 管道吞吐观察
上一篇:信号:Unix 最拧巴的抽象 下一篇:共享内存:SysV vs POSIX vs memfd
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】splice/tee/vmsplice
splice 在内核 pipe buffer 间移动数据——不经过用户态。本文讲 splice/tee/vmsplice 原理、pipe_buffer 与 page 生命周期、sendfile 的前世、CVE-2022-0847 Dirty Pipe 复盘。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。