为什么 IPC 值得单独一章:隔离是有价的
进程是资源隔离的单位(第 1 篇讲过 fork 与 CoW),隔离带来了崩溃安全与权限边界,但也带来了代价:两个进程的地址空间互不可见,想交换数据就必须经过内核中转。所有 IPC 机制的本质区别,就是数据在中转时被拷贝几次、同步用什么原语。理解了这一点,六类机制就不用死记——它们只是「拷贝次数 × 同步方式」坐标系上的不同点。
一个实际场景把它串起来:你写一个日志采集器,多个业务进程把日志交给它。用什么 IPC?往下看,答案藏在数据路径里。
六类机制速览:一张坐标系
| 机制 | 数据拷贝 | 容量/形态 | 同步 | 典型场景 |
|---|---|---|---|---|
| 匿名管道 | 2 次(用户→内核→用户) | 字节流,64KB 缓冲 | 阻塞读写 | 父子进程,shell 的 | |
| 命名管道 FIFO | 2 次 | 同上 | 阻塞读写 | 无亲缘进程的字节流 |
| System V / POSIX 消息队列 | 2 次 | 离散消息,有类型 | 可选阻塞 | 少量结构化消息 |
| 信号 | 无数据,仅通知 | 一个编号 | 异步打断 | 生命周期控制(kill) |
| 信号量(semget/sem_open) | 无数据 | 计数器 | 阻塞 P/V | 与共享内存配套做同步 |
| 共享内存(shm/mmap) | 0 次(映射后直接读写) | 大块内存 | 需自建(futex/信号量) | 大数据量高频交换 |
| Unix 域套接字 UDS | 2 次,但支持 fd 传递 | 字节流/数据报 | 阻塞/epoll | 本机服务间通信的事实标准 |
两个容易忽略的点。其一,管道的 64KB 缓冲意味着读写两端速度不匹配时会阻塞——ps aux | grep x 这类命令能工作,靠的是内核替你做了流控。其二,信号只传编号不传数据(sigqueue 例外,也只有一个 int),它是通知机制而不是传输机制,别拿它搬数据。
共享内存:唯一零拷贝的通道
共享内存是坐标系里的极端点:shmget/shm_open 申请一块物理内存,映射进两个进程的虚拟地址空间(第 2 篇的页表机制保证两次映射指向同一物理页),之后两端的读写就是普通内存操作,内核完全不参与数据传递。
代价是内核不再替你做同步:A 进程写完了,B 进程怎么知道?答案是自建同步原语——要么 System V 信号量,要么把 POSIX 信号量放在共享内存区里,要么用 futex 直接在用户态协调。现代高性能场景(如 DPDK 的多进程收包、机器学习推理服务的共享权重)通常用「共享内存 + 原子变量 + futex 等待队列」的组合,把整条数据通路压在用户态。
// 最小共享内存示例:POSIX shm + mmap
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0600);
ftruncate(fd, SIZE);
void *addr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
// 两个进程各自 mmap 后,addr 指向同一块物理内存
// 同步需自建:futex / POSIX 信号量 / 原子变量
零拷贝不是免费的:你要自己处理内存布局、对齐、字节序、读写指针的内存序(这就引出第 32 篇的内存屏障)。数据量小就别用共享内存——同步代码的复杂度会吞掉省下的拷贝时间。
Unix 域套接字:本机通信的默认答案
UDS 的接口与 TCP socket 完全一致(socket/bind/listen/accept),但内核知道两端在同一台机器,数据路径大幅缩短:不需要协议栈逐层封装,不需要校验和与重传,直接在两个 socket 的接收队列之间搬数据。实测吞吐通常是本机 loopback TCP 的 1.5–2 倍,延迟更低且没有端口占用问题。
但它真正的杀手锏是 fd 传递:SCM_RIGHTS 辅助消息可以把「一个打开的文件描述符」从进程 A 发给进程 B——B 拿到的不是数据的拷贝,而是同一个内核对象的引用。这个能力是很多系统设计的基石:nginx 的 master 进程打开 80 端口的监听 socket 后传给 worker(本系列第 25 篇讲 epoll 惊群时会回到这个机制);文件服务器把已打开的 fd 派发给无权限的工作进程;沙箱架构(第 6 篇 Agent 工程的执行隔离同理)由高权限 broker 代持资源、按需发放。
flowchart LR
A[进程 A<br>open 文件得到 fd=5] -->|"sendmsg + SCM_RIGHTS"| K[内核 socket 缓冲]
K -->|"recvmsg 收到 fd=9"| B[进程 B<br>fd=9 指向同一文件对象]
B --> R[read fd=9<br>与 A 读写同一偏移量/对象]
选型结论因此很清晰:本机服务间的控制面通信,UDS 是默认答案——比 TCP 快、可文件权限管控访问、能传 fd。Docker 的 /var/run/docker.sock、systemd 的 socket activation、数据库的 unix socket 模式,都是这个共识的体现。
消息队列与管道:各自的位置
System V 消息队列(msgget)与 POSIX 消息队列(mq_open)提供「有边界的消息 + 类型/优先级」语义,看起来比字节流的管道好用,但工程上地位尴尬:容量上限小(默认几 KB 到几十 KB)、msgrcv 的类型过滤在高并发下退化为 O(n) 扫描、调试工具贫乏。它适合低频、离散、需要消息边界的场景(如配置广播);高频大数据流请直接看共享内存或 UDS。
匿名管道的优势在 shell 生态与父子进程的简单场景:pipe2 一对 fd、fork 后各持一端,语义简单到不用文档。命名管道(FIFO,mkfifo)补上了无亲缘场景,配合 O_NONBLOCK 也能做事件驱动——但同样只是字节流,想要消息边界得自己分包。
一个反直觉的实测经验:管道在默认 64KB 缓冲下的单管道吞吐上限约 3–5 GB/s(拷贝两次的成本),如果你的「管道打不通」不是因为缓冲满,多半是另一端没在读——O_NONBLOCK + epoll 管理管道两端,和管 socket 的姿势完全一样。
现代 IPC 选型清单:三条规则
- 控制面(低频指令、状态查询)走 UDS,接口标准、可 epoll、可传 fd、可用文件权限做准入。本机微服务间通信优先 UDS 而非 loopback TCP。
- 数据面(高频大块数据)走共享内存 + 自建同步,拷贝次数归零,但把内存序与回收策略当作正式工程问题对待(第 32 篇的内存屏障是必修前置)。
- 事件通知走 eventfd/信号量,别用信号。
eventfd是一个可 epoll 的 64 位计数器,天生适合集成进事件循环;信号是异步打断,处理函数里能安全做的事极少,只保留给进程生命周期控制。
memfd 与现代共享内存:无文件的共享通道
传统 System V 共享内存要在 /dev/shm 或文件系统上留一个名字,进程死后还要手动清理——泄漏的共享内存段是老系统的经典遗产。Linux 4.17 的 memfd_create 给出了现代答案:创建一个「只存在于内存、没有磁盘路径」的匿名文件,拿到 fd 后 mmap 即可共享;配合 SCM_RIGHTS(上一节的 fd 传递)把 fd 发给对端进程,名字与生命周期管理一并消失——fd 关闭即释放,不留垃圾。GTK、Chromium 的零拷贝图像传输都已迁移到 memfd。另一个现代趋势是 io_uring 的 registered buffers:进程把一块内存(常与 memfd/mmap 共享区配合)预先注册给内核,后续 IO 直接在这块内存上完成,避免每次 IO 的缓冲注册开销——共享内存与异步 IO 两条线在「零拷贝数据面」上会合。工程记忆点:新的共享内存设计一律用 memfd_create 加 fd 传递,System V shm 只在维护存量系统时才碰。
共享内存的容错:进程崩溃后的状态恢复
共享内存通道的死穴是状态归属:同步原语(信号量、互斥锁)与数据读写指针都在共享区里,一个进程崩溃时可能带着锁死——POSIX 信号量不提供进程死亡时的自动释放(robust futex 是 futex 特有的补丁,glibc 的 PTHREAD_MUTEX_ROBUST 让持有者死亡时锁被标记、下一个加锁者收到 EOWNERDEAD 自行恢复)。容错设计的标准组合:其一,锁选 robust 属性的 futex 或「文件锁」(flock 随文件描述符关闭自动释放,进程死即释放,语义天然容错但性能低);其二,读写指针与数据区附带序号加校验(seqlock 思想,第 32 篇),消费端发现不一致就重同步或重建;其三,心跳与仲裁——共享区里放双方的心跳时间戳,恢复方检测对端死亡后执行定义好的接管流程(重置指针、清理半成品消息)。Chromium 的多进程架构、DPDK 的主从进程模式都内建了这套机制。工程提醒:共享内存的容错必须在设计期写进协议,事后打补丁几乎不可能——「双方都在时很快,一方死了全乱」的共享内存系统,缺的不是性能设计而是崩溃协议。
零拷贝审计的方法:把「拷贝了几次」变成可测量的数
「零拷贝」的宣称要能被验证,审计方法比口号重要。第一工具是 strace/perf trace 的读写计数:对比「应用逻辑处理的字节数」与「syscall 层搬运的字节数」,比值接近一说明路径直通,差距大说明有隐藏的缓冲层。第二工具是性能剖析里的 memcpy 热点:perf record 后看 memcpy/memmove 在火焰图的占比——高频 IPC 系统(数据库、消息队列)的这个占比是架构效率的直接指标,超过百分之十通常有可消除的拷贝。第三是逻辑层的计数:在收发两端各计一次字节数,中转层(代理、桥接)的放大系数直接暴露多次拷贝。三个方法共同校准「零拷贝」的真实成色。一条工程经验:宣称零拷贝的框架(某些 RPC 与序列化库),其「零」通常只覆盖热路径的一个环节(网络到用户缓冲),序列化层与对象转换层的拷贝往往还在——端到端的拷贝次数要按「业务对象到对端业务对象」的全链路数,这才是有意义的口径。
小结
IPC 的六类机制放在「拷贝次数 × 同步方式」坐标系里一目了然:信号只传通知,管道与 UDS 拷贝两次但 UDS 拥有 fd 传递这个杀手锏,共享内存零拷贝但同步自建。现代多进程架构的惯例组合是控制面 UDS、数据面共享内存、事件通知 eventfd。nginx 的 master-worker 与 Chromium 的多进程架构都是这套组合的现实注脚。下一章的 mmap 会把共享内存的底层机制——文件映射——单独展开,那是理解 Page Cache、零拷贝与内存占用监控的共同基础。
读者留言
COMMENTS 暂无还没有留言,来说第一句?