IPC

进程间通信 IPC 的核心问题、常见机制与适用场景说明

#status / growing #type / concept

[!info] related notes

IPC

Overview

进程间通信IPC (InterProcess Communication) - 简书

操作系统中,多个独立进程之间交换数据、传递消息的机制。

  • 管道 pipe
  • 命名管道
  • 消息队列
  • 共享内存
  • socket
  • 信号
  • RPC

可以把**进程间通信(IPC, Inter-Process Communication)**先压成一句话:

因为进程彼此隔离,默认不能直接读写对方内存,所以操作系统必须提供一组“受控的交换数据和同步行为”的机制,这组机制就叫 IPC。

理解 IPC,最重要的不是先背 API,而是先抓住这三个问题:

  1. 数据怎么过去
  2. 双方怎么知道对方准备好了
  3. 内核怎么保证安全、顺序和隔离

1. 为什么一定需要进程间通信

前面讲过,进程最大的特点是地址空间隔离

这意味着:

  • 进程 A 的堆、栈、全局变量
  • 进程 B 默认都看不见
  • A 不能像调用普通函数那样直接访问 B 的对象

这带来两个后果:

好处是安全和稳定。
一个进程崩了,不该直接把另一个进程的内存踩坏。

代价是协作变麻烦。
现实程序又经常必须协作,比如:

  • shell 把一个程序的输出接到另一个程序输入
  • 浏览器主进程和渲染进程通信
  • 数据库和客户端通信
  • GUI 程序和后台 worker 通信
  • 微服务进程之间交换请求与结果

所以操作系统必须提供一些“官方通道”。


2. IPC 到底在解决什么问题

IPC 本质上同时解决两类问题:

2.1 数据传输

也就是:

  • 发字节流
  • 发结构化消息
  • 共享一块内存
  • 通知某个事件发生了

2.2 同步协调

也就是:

  • 我什么时候能读
  • 什么时候能写
  • 谁先做
  • 谁后做
  • 数据有没有写完
  • 会不会两个进程一起改同一份数据

很多人以为 IPC 就是“传数据”,其实不完整。
真正难的往往是同步。

比如共享内存本身传输极快,但如果没有同步机制:

  • 读进程可能读到一半数据
  • 两个写进程可能把内容写乱
  • 根本不知道新数据什么时候可用

所以 IPC 通常要同时考虑:

  • 通信
  • 同步

3. IPC 的大分类

可以把常见 IPC 分成几类:

3.1 基于内核缓冲区的数据通道

典型有:

  • 管道 pipe
  • 命名管道 FIFO
  • 消息队列
  • socket

特点:

  • 数据通常先进入内核
  • 再由另一个进程读出
  • 编程简单
  • 内核帮你做很多同步和边界管理
  • 但通常会有拷贝和系统调用开销

3.2 基于共享内存的直接交换

典型有:

  • 共享内存 shm
  • 内存映射 mmap

特点:

  • 多个进程映射同一块物理内存
  • 不必每次都经内核转发数据
  • 速度通常最快
  • 但同步要自己额外处理

3.3 基于事件/信号的通知机制

典型有:

  • 信号 signal
  • semaphore
  • eventfd
  • futex(更底层)
  • 条件变量类机制(通常要配共享内存)

特点:

  • 重点不是搬大量数据
  • 而是“通知对方某事发生了”或“做同步控制”

3.4 基于更高层协议的通信

典型有:

  • 本地 socket
  • TCP socket
  • RPC
  • gRPC
  • DBus 等总线机制

特点:

  • 更像“进程对进程的服务接口”
  • 往往更通用,也适合跨机器

4. 管道(pipe)

4.1 管道是什么

管道可以理解成一个内核里的字节流缓冲区,一端写,一端读。

最典型场景就是 shell:

ps aux | grep nginx

这里 ps 的输出通过管道流向 grep 的输入。

4.2 管道的特点

1)通常是半双工

经典 pipe 一般一端读、一端写。
如果要双向通信,往往要两根管道。

2)本质是字节流

它不天然保留“消息边界”。
你写了两次:

  • write("abc")
  • write("def")

对方可能一次读到 abcdef,也可能分两次读。

3)通常用于有亲缘关系的进程

比如父子进程。
因为 pipe 文件描述符常通过 fork 继承。

4.3 管道为什么好用

因为它简单:

  • 内核提供缓冲
  • 读空会阻塞
  • 写满会阻塞
  • 天然适合生产者-消费者模型

4.4 管道的问题

  • 一般不适合复杂双向协议
  • 没有消息边界
  • 只适合本机
  • 通常更适合简单流式数据传递

5. 命名管道(FIFO)

5.1 和普通管道的区别

普通 pipe 通常依赖亲缘关系和继承的文件描述符。
命名管道 FIFO 则是在文件系统里有一个名字,互不相关的进程也可以通过这个路径打开同一个 FIFO。

你可以把它理解成:

  • 行为像管道
  • 入口像文件

5.2 适合什么场景

适合:

  • 本机上两个不相关进程做简单通信
  • 想利用文件路径作为“约定好的通信端点”

5.3 局限

它本质上仍然是:

  • 字节流
  • 本机通信
  • 协议能力有限

所以它更像“可被陌生进程访问的本地管道”。


6. 消息队列(message queue)

6.1 消息队列的核心思想

和管道最大的不同是:

消息队列强调“消息”而不是纯字节流。

也就是说,发送方放进去的是一条条消息,接收方取出来时也按消息取。
消息边界通常由内核帮助维护。

6.2 它解决了什么问题

相比管道,消息队列更适合:

  • 一条一条发命令
  • 发结构化小数据
  • 多生产者/多消费者
  • 异步解耦

比如:

  • “启动任务 42”
  • “用户 1001 登录”
  • “刷新缓存”
  • “退出”

这种天然是离散消息,不是连续字节流。

6.3 优点

  • 保留消息边界
  • 比字节流更适合命令/事件型通信
  • 往往支持优先级、类型等能力
  • 读写双方解耦更强

6.4 缺点

  • 通常不适合超大数据块
  • 仍经过内核管理
  • 性能通常不如共享内存直传大块数据

所以消息队列很适合“控制面”,不一定适合“数据面”。


7. 共享内存(shared memory)

7.1 共享内存是什么

共享内存是 IPC 里最值得重点讲的,因为它最接近“两个进程直接看同一块内存”。

本质上是:

  • 内核创建或管理一块物理内存
  • 多个进程把它映射进自己的虚拟地址空间
  • 于是各自看起来都能直接读写那块区域

这样一来:

  • 进程 A 写进去
  • 进程 B 立刻能从同一块物理内存看到

7.2 为什么共享内存快

因为它避免了很多“数据搬运”。

和 pipe / 消息队列 / socket 相比,后者常见模式是:

  • 发送方把数据拷到内核
  • 接收方再从内核拷到用户空间

而共享内存通常是:

  • 双方都映射同一块内存
  • 读写直接在共享页上发生

所以对于大数据量交换,通常共享内存最快。

7.3 共享内存最大的问题:同步

共享内存的难点不是“共享”,而是“怎么不写乱”。

比如:

  • A 正在写结构体
  • B 同时开始读
  • 结果 B 读到一半新一半旧

再比如:

  • 两个写者同时修改链表
  • 指针结构被破坏

所以共享内存几乎总要配合同步机制:

  • mutex
  • semaphore
  • eventfd
  • futex
  • 自旋锁
  • 原子变量
  • ring buffer 协议

也就是说:

共享内存只解决“数据在哪里”,不解决“什么时候安全地读写”。

7.4 共享内存适合什么

特别适合:

  • 大块数据交换
  • 高频低延迟通信
  • 多媒体缓冲区
  • 数据采集
  • 高频交易/低延迟系统
  • 进程间共享缓存

7.5 共享内存常见设计模式

环形缓冲区 ring buffer

一个写指针、一个读指针:

  • 写者推进写指针
  • 读者推进读指针

适合单生产者单消费者。

共享队列

多个进程在共享内存里维护任务队列。
需要更严格同步。

控制区 + 数据区

小块元数据描述大块共享区域:

  • 长度
  • 状态
  • 序号
  • 校验

8. 信号(signal)

8.1 信号是什么

信号更像一种异步通知机制,而不是大数据传输机制。

它的语义通常是:

  • 某件事发生了
  • 赶紧处理一下
  • 或者至少知道一下

例如:

  • 子进程退出了
  • 进程应当终止
  • 定时器到期
  • 非法内存访问
  • 用户按了 Ctrl+C

8.2 信号的特点

  • 很轻量
  • 很适合“通知”
  • 不适合携带大量数据
  • 是异步到达的

8.3 为什么信号难用

因为它打断正常控制流。
异步信号处理里能安全做的事情很少。

所以信号常用于:

  • 中断/终止控制
  • 唤醒
  • 简单事件通知

不适合做复杂业务协议。


9. 信号量(semaphore)

9.1 信号量不是“传数据”的主力

信号量更偏向同步原语
它本质上是一个计数器,用来表示“有多少个资源可用”或“允许多少次进入”。

常见操作:

  • P / wait:减一,若不可减则等待
  • V / post:加一,并唤醒等待者

9.2 在 IPC 里的作用

常和共享内存配合:

  • 共享内存负责放数据
  • 信号量负责协调什么时候可读可写

例如经典生产者-消费者模型:

  • empty 表示还有多少空槽
  • full 表示已有多少数据
  • mutex 保护共享队列

9.3 为什么它重要

因为很多 IPC 真正麻烦的不是“拷贝数据”,而是“别同时乱动共享状态”。
信号量正是拿来解决这个问题的。


10. socket

10.1 socket 为什么也算 IPC

很多人一提 IPC 只想到 pipe 和共享内存,其实 socket 是最通用的 IPC 之一

因为它可以用于:

  • 同一台机器上的进程通信
  • 不同机器上的进程通信

所以它是非常统一的一种模型。

10.2 本地 socket 和网络 socket

Unix Domain Socket

用于本机进程通信。
走的是本地内核路径,不经过真实网络。

特点:

  • 比 TCP loopback 通常更轻
  • 可以传文件描述符
  • 适合本机服务间通信

TCP/UDP Socket

用于跨机器,也能本机用。
适合 client-server 模型。

10.3 socket 的优点

  • 通用
  • 双向通信方便
  • 易于构建协议
  • 本机/跨机模型统一
  • 生态成熟

10.4 socket 的缺点

  • 相比共享内存,拷贝和协议开销通常更大
  • 纯本地高频小延迟场景未必是最优

10.5 为什么很多系统喜欢 socket

因为它抽象得非常好:

  • 连接
  • 收发
  • 双工
  • 阻塞/非阻塞
  • 多路复用

而且以后想跨机器扩展时,模型几乎不用推翻。


11. 内存映射文件(mmap)

11.1 mmap 和共享内存的关系

mmap 可以把文件或匿名内存映射到进程地址空间。
如果多个进程映射同一个底层对象,并且是共享映射,那么它也能成为 IPC 手段。

你可以把它看成共享内存的一种非常重要的实现方式。

11.2 它的特点

  • 可以把文件内容直接映射进内存
  • 多进程可共享同一个映射区域
  • 既能做 IPC,也能做文件 I/O 优化

11.3 典型用途

  • 共享缓存
  • 大文件处理
  • 数据库页缓存
  • 零拷贝风格访问

但它同样需要同步控制。


12. IPC 里的“同步”为什么经常比“传输”更难

假设你有一块共享内存,很快。
但马上就会遇到这些问题:

12.1 可见性问题

A 写完后,B 什么时候一定能看到?

12.2 原子性问题

A 正在更新一个复合结构,B 会不会看到中间态?

12.3 顺序问题

A 先写数据,再写“完成标志”;
CPU 或编译器会不会重排?

12.4 唤醒问题

B 怎么知道 A 已经写完了?
忙等?阻塞?事件通知?

所以真正工程化的 IPC,往往是组合拳:

  • 传输机制:共享内存 / socket / 管道
  • 同步机制:锁 / 信号量 / 条件变量 / eventfd / futex
  • 协议设计:状态字段、序号、长度、校验、内存屏障

13. 几种 IPC 的直观对比

可以粗略这样理解:

管道 / FIFO

像一根内核里的水管。

  • 简单
  • 流式
  • 本机
  • 更适合线性数据流

消息队列

像一个内核维护的信箱/任务箱。

  • 一条条消息
  • 易于事件驱动
  • 适合命令、通知、任务分发

共享内存

像两个人共用一张白板。

  • 最快
  • 最灵活
  • 但也最容易写乱
  • 必须配同步机制

信号

像“拍你一下”。

  • 通知快
  • 数据少
  • 控制流打断强
  • 不适合复杂协议

socket

像标准化电话/网络链路。

  • 双向
  • 通用
  • 本机/跨机统一
  • 适合服务化通信

14. 实际工程里怎么选

14.1 小而简单的父子进程数据流

选:

  • pipe

例如 shell pipeline、进程串联过滤器。

14.2 不相关本地进程,简单通信

选:

  • FIFO
  • Unix Domain Socket

通常 UDS 更通用。

14.3 命令、事件、任务分发

选:

  • 消息队列
  • socket
  • 更高层消息总线

14.4 高频大块数据交换

选:

  • 共享内存 + 同步原语

14.5 本机服务之间长期双向通信

选:

  • Unix Domain Socket

14.6 未来可能跨机器

选:

  • socket / RPC

因为迁移成本最低。


15. 一个经典例子:共享内存为什么快但难

假设有两个进程:

  • A 采集视频帧
  • B 做编码

如果用 pipe 或 socket:

  • A 写入内核缓冲
  • B 再从内核读出
  • 大帧频繁搬运,开销明显

如果用共享内存:

  • A 直接把帧写进共享缓冲区
  • B 直接从共享缓冲读

这会快很多。
但同步马上成了问题:

  • B 怎么知道哪一帧写完了
  • A 会不会覆盖 B 还没处理的帧
  • 多帧环形缓冲如何管理
  • 需要几个缓冲槽
  • 用锁还是无锁 ring buffer

这就是 IPC 的经典权衡:

越高性能,往往越需要你自己处理更多同步细节。


16. 一个经典例子:为什么 socket 那么常用

假设你有:

  • 主进程
  • 多个 worker 进程
  • 以后还想拆成独立服务

如果你现在用共享内存,短期可能很快。
但以后跨机器就完全不是一套东西了。

如果你一开始就用 socket:

  • 本机先用 Unix Domain Socket
  • 以后拆机再换 TCP
  • 应用层协议可以保持大体一致

所以很多工程选择 socket,不是因为它最快,而是因为它:

  • 通用
  • 稳定
  • 易扩展
  • 易维护

17. IPC 的几个核心设计维度

选 IPC 机制时,通常要看这些维度:

17.1 数据量大不大

  • 大:共享内存更有优势
  • 小:管道/消息队列/socket 也足够

17.2 是流还是消息

  • 流:pipe/socket
  • 离散消息:消息队列/socket/自定义协议

17.3 本机还是跨机器

  • 只本机:pipe/FIFO/共享内存/UDS
  • 可能跨机器:socket/RPC

17.4 是不是高频低延迟

  • 是:共享内存往往优先
  • 否:socket 常更省心

17.5 同步复杂度能不能接受

  • 能接受复杂同步:共享内存
  • 希望内核多帮忙:消息队列、socket、pipe

17.6 是否需要强隔离和清晰协议

  • 需要:socket / 消息式接口通常更好维护

18. 常见误区

误区 1:共享内存一定最好

不是。
它吞吐和延迟可能最好,但同步复杂、出 bug 难查、协议设计难。

误区 2:IPC 就是“传数据”

不对。
很多时候真正的难点是:

  • 同步
  • 唤醒
  • 顺序
  • 错误恢复
  • 生命周期管理

误区 3:管道很低级,所以没用

不对。
很多 Unix 工具链就是靠 pipe 组合出强大能力的。

误区 4:socket 只是网络通信

不对。
Unix Domain Socket 是本机 IPC 非常重要的机制。

误区 5:消息队列和共享内存能互相替代

不完全能。
一个更偏“消息”,一个更偏“共享数据区”。


19. 一句话总结所有 IPC 机制

可以这样记:

  • 管道:简单的字节流通道
  • FIFO:有名字的本地管道
  • 消息队列:按消息收发的内核邮箱
  • 共享内存:最快的数据共享方式
  • 信号:异步通知
  • 信号量:同步计数器
  • socket:最通用的双向通信机制
  • mmap:共享映射,常用于高性能共享数据

再压缩成一句:

IPC 的本质,就是在“进程隔离”前提下,用操作系统提供的受控机制,完成数据交换和执行协调。

如果你愿意,我下一条可以继续讲两种很实用的方向之一:
“详细讲讲共享内存 + 信号量怎么配合”,或者 “详细讲讲 socket 和 pipe 的底层区别”

创建于 2026/3/13 更新于 2026/5/27