Skip to content
Go back

进程间通讯IPC全览:七种方式的原理与选型

一句话结论(30s)

IPC 选型只取决于两件事——数据量和通讯模式。因为消息传递型(管道/消息队列/Socket)都要经过「用户态→内核→用户态」两次拷贝,只有共享内存能零拷贝,所以大数据量必须选共享内存;而共享内存本身不提供同步、必须配合信号量,所以没有「最好」的 IPC,只有「最合适」的 IPC。

核心原理(2min)

底层深入(5-10min)

IPC 分类总览

进程间通讯(Inter-Process Communication, IPC)是操作系统提供的让独立进程交换数据与同步的机制。按通讯方式可分为三大类:

思考:为什么”共享内存”天生最快?因为消息传递型数据都要走”用户态 → 内核 → 用户态”两次拷贝,而共享内存让多个进程直接把同一块物理页映射进各自地址空间,之后读写就像访问自己的普通内存,完全不经过内核。省掉两次拷贝 + 系统调用,就是它”零拷贝、极快”的来源——代价是内核不再替你保证同步,必须自己配信号量。

管道(Pipe)

匿名管道

系统调用 pipe(int fd[2]) 在内核中创建一块环形缓冲区(通常 4KB/16KB),返回两个文件描述符:fd[0] 用于读,fd[1] 用于写。数据在内核缓冲区中按字节流传输,不支持随机访问,没有消息边界。

关键特性:

经典应用场景是 Shell 管道:ps aux | grep nginx。Shell 调用 pipe 创建管道,然后 fork 出两个子进程,一个将标准输出重定向到管道写端执行 ps,另一个将标准输入重定向到管道读端执行 grep

思考:为什么匿名管道是”半双工”、还要有亲缘关系?半双工是因为内核只给了一根单向的环形缓冲区,数据只能朝一个方向流,双向就得开两根管道;亲缘关系是因为匿名管道没有名字,唯一的传递途径是 fork 时子进程继承文件描述符,所以天然只能父子/兄弟进程用。这两个”限制”不是缺陷,而是”简单”的代价——换来的是创建成本极低。

命名管道(FIFO)

通过 mkfifo 创建一个文件系统中的特殊文件(类型为 p),任何知道路径的进程都可以通过 open 这个文件来建立连接。

解决了匿名管道”必须有亲缘关系”的限制,但仍然是半双工字节流。适合简单的非亲缘进程间一对一通讯。

消息队列

System V 消息队列

消息队列在内核中维护一个链表结构,每条消息带有类型标识(mtype)。发送方指定消息类型,接收方可以按类型选择性读取(先入先出或按类型优先)。

与管道相比的关键区别:

但消息队列也有明显不足:每条消息有大小限制(Linux 默认 8192 字节,可调),队列总容量有限,且拷贝开销不可避免(发送方用户态 → 内核缓冲区 → 接收方用户态,两次拷贝)。

POSIX 消息队列

mq_open / mq_send / mq_receive,接口更简洁,支持消息优先级和异步通知(信号或线程),但不支持按类型选择性读取。

共享内存

原理

共享内存是最快的 IPC 方式。它的核心思路很简单:内核将同一块物理内存映射到多个进程的虚拟地址空间中,从此进程间的数据交换就像读写自己进程的普通内存一样,完全不需要内核参与。

进程 A 虚拟地址空间           物理内存            进程 B 虚拟地址空间
  [0x7f...]  ─────映射────→  [共享页框]  ←────映射─────  [0x7f...]

System V 共享内存

shmget 创建或获取一块共享内存区域,返回一个标识符。shmat 将这块内存附加(attach)到调用进程的地址空间,之后就可以像普通内存一样 memcpy、直接访问。shmdt 断开连接,shmctl 用于删除。

核心优势与代价

优势:零拷贝。管道、消息队列都需要两次拷贝(用户→内核→用户),共享内存只在初始的 attach 时建立映射,后续读写完全在用户态完成。在高频大数据量场景(如视频帧传输、数据库共享缓冲区)下性能优势巨大。

代价

  1. 同步问题:共享内存本身不提供任何同步机制,必须配合信号量或互斥锁使用
  2. 并发安全:多进程并发读写同一块共享内存,需要应用层保证原子性(如用 __sync_bool_compare_and_swap
  3. 指针问题:如果共享内存中存储指针,不同进程中该指针指向的虚拟地址不同,需要转换为偏移量

思考:为什么共享内存”自己不做同步”?因为内核给共享内存的定位就是”把一块物理页映射给多个进程”,剩下的事一概不管——谁在写、谁在读、会不会并发踩踏,都是应用层的事。这正是”快”的另一面:内核少管一步,就少一层开销;同步责任转移到了使用方,所以共享内存几乎总要搭配信号量或原子操作,否则就是数据竞争的重灾区。

信号量

信号量本质上是一个受保护的内核计数器,不是用来传递数据的,而是用来实现进程/线程间的互斥与同步。

核心操作

PV 操作的原子性由操作系统保证——在单核时代通过关中断,多核时代通过硬件原子指令(lock cmpxchglock xadd)实现。

使用场景

信号

信号是异步通知机制,一个进程可以向另一个进程(或进程组)发送一个特定的信号编号,目标进程收到后执行预先注册的处理函数(或默认动作)。

与前面几种 IPC 不同,信号传递的数据量极少——只能传递信号编号(如 SIGKILL=9, SIGTERM=15, SIGUSR1=10)。sigqueue 可以附带一个 union sigval(int 或指针),但仍然无法传递复杂数据。适合做”通知”而非”通讯”。

Socket

Socket 最常与网络编程关联(TCP/UDP over IP),但它也是强大的本地 IPC 工具。

Unix Domain Socket

创建时指定 AF_UNIX(或 AF_LOCAL)地址族,数据通过内核传递而不经过网络协议栈。Unix Domain Socket 支持两种模式:

相比 TCP loopback(127.0.0.1),Unix Domain Socket 性能更高:不需要走完整 TCP/IP 协议栈(没有 IP 分片重组、没有拥塞控制窗口计算、没有校验和),延迟低 20-30%,吞吐量高 50-80%。

独有的高级特性

选型决策指南

IPC 方式数据量速度同步支持持久化适用场景
匿名管道流式阻塞同步父子进程单向流
命名管道流式阻塞同步非亲缘进程单向流
消息队列小/中异步内核级多对多、类型选择
共享内存极快需配合需配合高频大数据量
信号量无数据原生内核级互斥/限流/同步
信号极小异步通知进程控制/事件通知
Unix Socket不限丰富本地C/S架构

选择 IPC 的核心决策因素:数据量(大数据选共享内存)、通讯模式(1对1 还是多对多)、是否跨网络(跨网络必须 Socket)、同步需求(决定是否需要额外配合信号量)。

章末提问

追问 1:为什么共享内存是所有 IPC 里最快的?

回答思路:结论先行——因为它是零拷贝,数据不经过内核中转。管道、消息队列、Socket 都要走”用户态 → 内核缓冲区 → 用户态”两次拷贝,而共享内存让多个进程把同一块物理页直接映射进各自地址空间,后续读写就像访问自己的普通内存,完全不进内核。代价是内核不提供同步,必须自己配信号量或原子操作。

追问 2:管道和消息队列有什么区别?

回答思路:结论先行——管道是”无边界字节流”,消息队列是”有边界、可分类的消息”。因为管道按字节流传输、半双工、没有消息边界,读时可能粘连;消息队列每条消息独立带 mtype 类型,接收方可按类型选择性读取,天然不粘包、支持多对多、内核持久化。但两者都要两次拷贝,大数据量下都不如共享内存。

追问 3:一个进程要往另一个进程传大量视频帧数据,该选哪种 IPC?为什么?

回答思路:结论先行——选共享内存,搭配信号量做同步。因为视频帧数据量大、频率高,消息传递型的两次拷贝会成为瓶颈,而共享内存零拷贝、直接读写内存,性能最优;但共享内存本身没有同步,需要信号量(如 empty/full)做生产者-消费者同步,并用原子操作保证并发安全、用偏移量规避指针问题。


Share this post on:

Previous Post
零拷贝——为什么Kafka和Nginx这么快?
Next Post
进程调度算法详解:从FCFS到多级反馈队列