土法炼钢兴趣小组的算法知识备份

【操作系统百科】管道、FIFO、socketpair

文章导航

分类入口
os
标签入口
#pipe#fifo#socketpair#splice#scm-rights#pipe-buf#vmsplice

目录

管道是 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 塞。

这意味着:

二、pipe(2) / pipe2(2)

int fds[2];
pipe2(fds, O_CLOEXEC | O_NONBLOCK);
// fds[0] 读端,fds[1] 写端

永远用 pipe2O_CLOEXEC——pipe(2) 不带是 fork-exec 漏 fd 的经典原因。

半双工:每个 fd 只能一向。需要双向用 socketpair 或 pipe 两条。

2.1 shell 的 cmd1 | cmd2

shell 逻辑:

  1. pipe2(fds)
  2. fork
  3. 子 1:dup2(fds[1], 1); close(fds[0]); close(fds[1]); exec(cmd1)
  4. 子 2:dup2(fds[0], 0); close(fds[0]); close(fds[1]); exec(cmd2)
  5. 父:close(fds[0]); close(fds[1]); wait...

任何一端忘关就会 hang——经典 “cat 不终止” 的源头。

三、读写语义的细节

3.1 EOF / 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 非阻塞

非阻塞 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:

成本: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 桥。

相关:

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,客户端写触发命令”。问题:

现代替代:

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 Cpopen 子进程有时僵尸 原因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)

十一、小结

下一篇 B-15 讲共享内存:SysV shm、POSIX shm、memfd + seal 的现代范式。


参考文献

工具


上一篇信号:Unix 最拧巴的抽象 下一篇共享内存:SysV vs POSIX vs memfd

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-05-22 · os

【操作系统百科】splice/tee/vmsplice

splice 在内核 pipe buffer 间移动数据——不经过用户态。本文讲 splice/tee/vmsplice 原理、pipe_buffer 与 page 生命周期、sendfile 的前世、CVE-2022-0847 Dirty Pipe 复盘。

2026-04-27 · os

【操作系统百科】内存回收

Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。

2026-04-28 · os

【操作系统百科】交换

swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。

2026-05-03 · os

【操作系统百科】Slab/SLUB 分配器

buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。


By .