第13讲 进程间通信

第13讲 进程间通信
agsd第13讲 进程间通信:共享内存、消息队列与同步语义
本文根据《第13讲 进程间通信.md》的对话转录整理而成。核心问题是:进程间通信 IPC 不只是“两个进程能传数据”,还要解决数据放在哪里、谁来读取、何时等待、如何避免覆盖和读空,以及通信双方是否需要直接认识彼此。
1. 为什么需要进程间通信
进程默认拥有独立的地址空间。一个进程不能随意访问另一个进程的内存,这是操作系统提供隔离性和安全性的基础。
但现实系统中,进程之间经常需要协作。例如:
- 浏览器进程和渲染进程交换页面数据;
- shell 通过管道把一个命令的输出交给另一个命令;
- 服务器主进程把请求分发给 worker 进程;
- 多个进程共享同一批计算结果;
- 客户端进程向服务端进程发送请求。
因此,操作系统需要提供 IPC,Inter-Process Communication,即进程间通信机制。
IPC 的核心问题可以分为三类:
数据如何传递
- 是放在共享内存中,还是通过内核转发?
通信双方如何建立关系
- 是直接指定对方进程,还是通过中间对象通信?
通信操作如何等待
- 是同步等待,还是异步返回?
2. IPC 的两条主线:共享内存与消息传递
IPC 机制可以大致分成两类:
| 类型 | 基本思想 | 特点 |
|---|---|---|
| 共享内存 | 多个进程映射同一块内存区域 | 快,但同步复杂 |
| 消息传递 | 通过内核或中间对象传递消息 | 更安全、结构清晰,但开销可能更高 |
共享内存的优势是速度快。数据放在同一块内存中,进程之间不必每次都通过内核复制大量数据。
但共享内存的困难也很明显:
- 发送者不能覆盖接收者还没读的数据;
- 接收者不能读取还没有写入的数据;
- 多个发送者不能同时写同一个位置;
- 多个接收者不能重复读或读错位置;
- 如果一直等待,可能浪费 CPU。
消息传递则把这些问题更多交给内核或消息队列来管理。发送者发送消息,接收者接收消息,通信语义更清晰,但可能需要更多系统调用和数据复制。
❗ 本节核心结论: 共享内存追求速度,但需要程序员处理同步;消息传递牺牲一部分性能,换来更清晰的通信结构和更强的隔离性。
3. 共享内存:快,但必须处理同步
共享内存通信的基本模型是:
- 操作系统创建或映射一块共享内存区域;
- 两个或多个进程都能访问这块区域;
- 一个进程向共享区域写入数据;
- 另一个进程从共享区域读取数据。
flowchart LR
A[进程 A] --> M[共享内存区域]
M --> B[进程 B]
B --> M
M --> A
这张图强调:共享内存不是“进程 A 直接进入进程 B 的私有内存”,而是操作系统提供一块双方都能访问的公共区域。
共享内存最大的问题是:
共享数据本身不会自动同步。
也就是说,操作系统可以让两个进程看到同一块内存,但不会自动保证它们读写顺序正确。
例如:
- 发送者写得太快,可能覆盖接收者还没读的数据;
- 接收者读得太快,可能读到尚未写入的数据;
- 多个发送者同时写,可能写到同一位置;
- 多个接收者同时读,可能重复消费同一数据。
因此,共享内存通常必须配合锁、信号量、条件变量、原子操作等同步机制使用。
4. 环形缓冲区:共享内存中的数据排队方式
环形缓冲区 circular buffer 是共享内存通信中常见的数据组织方式。
它的作用不是直接“解决所有同步问题”,而是解决三个更基础的问题:
- 新数据应该写到哪里?
- 旧数据应该从哪里读?
- 缓冲区什么时候空,什么时候满?
典型结构如下:
1 | item buffer[BUFFER_SIZE]; |
其中:
| 变量 | 含义 |
|---|---|
buffer |
共享数据区 |
in |
下一个写入位置 |
out |
下一个读取位置 |
发送者写入后:
1 | in = (in + 1) % BUFFER_SIZE; |
接收者读取后:
1 | out = (out + 1) % BUFFER_SIZE; |
% BUFFER_SIZE 是环形缓冲区的关键。它让下标到达数组末尾后重新回到开头。
例如 BUFFER_SIZE = 10:
1 | 0 -> 1 -> 2 -> ... -> 9 -> 0 -> 1 -> ... |
这使得固定大小的数组可以循环复用。
5. 环形缓冲区如何判断空和满
环形缓冲区通过 in 和 out 判断状态。
5.1 缓冲区为空
当:
1 | in == out |
表示没有新数据可读。
接收者此时不能继续读,否则会读到无效数据。
5.2 缓冲区为满
常见设计中,会空出一个位置来区分“空”和“满”。
当:
1 | (in + 1) % BUFFER_SIZE == out |
表示再写就会追上接收者,覆盖尚未读取的数据。
发送者此时不能继续写。
flowchart TD
A[发送者 Producer] --> B{缓冲区是否满}
B -->|满| C[等待]
B -->|未满| D[写入 buffer[in]]
D --> E[in 前进]
F[接收者 Consumer] --> G{缓冲区是否空}
G -->|空| H[等待]
G -->|非空| I[读取 buffer[out]]
I --> J[out 前进]
这张图说明:环形缓冲区提供了读写位置和空满条件,使发送者和接收者可以围绕这些状态进行协作。
6. 环形缓冲区不能单独完成同步
必须注意:环形缓冲区不是锁,也不是信号量。
它只是提供了数据结构和状态判断条件。真正的同步仍然需要额外机制。
例如,有两个发送者同时执行:
1 | buffer[in] = item; |
它们可能同时看到同一个 in,然后都写入同一个位置,造成覆盖。
所以,环形缓冲区只能解决:
- 数据放哪里;
- 数据从哪里取;
- 空和满如何判断;
- 存储空间如何循环复用。
但它不能单独保证:
- 多个发送者并发写入一定安全;
- 多个接收者并发读取一定安全;
in和out的更新一定原子;- 等待过程不浪费 CPU。
完整的共享内存通信通常应当是:
1 | 共享内存 + 环形缓冲区 + 同步原语 |
| 组成部分 | 作用 |
|---|---|
| 共享内存 | 让多个进程访问同一块区域 |
| 环形缓冲区 | 管理数据位置、复用空间、判断空满 |
| 同步原语 | 保证并发访问安全,避免忙等或竞争 |
❗ 本节核心结论: 环形缓冲区解决的是共享内存中的“数据排队和空间复用”问题,不是完整的并发同步方案。
7. 忙等问题:为什么不能一直 while
最基础的环形缓冲区实现可能会使用忙等:
1 | while (((in + 1) % BUFFER_SIZE) == out) |
或:
1 | while (in == out) |
这种写法逻辑简单,但问题是:等待进程会一直占用 CPU 检查条件。
这叫 busy waiting,忙等。
忙等的问题包括:
- 浪费 CPU 时间;
- 降低系统整体吞吐;
- 对低优先级任务不友好;
- 在多进程系统中可能放大调度压力。
更合理的做法是使用:
- semaphore;
- mutex;
- condition variable;
- blocking system call;
- event notification。
这样,当缓冲区满或空时,进程可以进入阻塞状态,让 CPU 去执行其他任务。条件满足后再被唤醒。
8. 消息传递:通过发送和接收完成通信
与共享内存不同,消息传递不要求进程直接共享地址空间。
它通常抽象为两个基本操作:
1 | send(message) |
消息传递的优势是:
- 通信结构清晰;
- 进程隔离性更好;
- 不需要双方直接共享内存;
- 更容易跨机器扩展;
- 内核可以帮助管理队列、权限和阻塞。
缺点是:
- 可能需要系统调用;
- 可能发生数据复制;
- 性能通常不如共享内存直接;
- 大数据传输成本较高。
消息传递可以进一步分为:
- 直接通信;
- 间接通信。
9. 直接通信:明确发给某个进程
直接通信 direct communication 的特点是:发送者明确知道接收者是谁。
形式上可以写成:
1 | Send(P, message) |
含义是:
Send(P, message):把消息发给进程 P;Recv(Q, message):从进程 Q 接收消息。
直接通信的关系是:
1 | 发送者 -> 接收者 |
它的优点是直观,通信对象明确。
但它也有缺点:
- 发送者和接收者耦合较强;
- 发送者必须知道接收者身份;
- 接收者变化时,发送者可能也要修改;
- 不适合多个发送者和多个接收者灵活协作的场景。
10. 间接通信:通过中间对象连接双方
间接通信 indirect communication 的核心是:
发送者不是直接把消息发给某个进程,而是发给一个中间对象;接收者也不是直接从某个进程那里取消息,而是从这个中间对象取。
这个中间对象可以是:
- mailbox;
- pipe;
- message queue;
- channel;
- topic;
- broker。
形式上可以写成:
1 | Send(M, message) |
其中 M 是中间对象。
通信关系变成:
1 | 发送者 -> 中间对象 -> 接收者 |
flowchart LR
A[发送进程] --> Q[消息队列 / 信箱]
Q --> B[接收进程 1]
Q --> C[接收进程 2]
这里的“间接”不是说通信一定很慢,也不是说消息绕了很多层,而是说:
通信双方不是靠彼此的进程 ID 建立连接,而是靠共同访问同一个中间对象建立连接。
11. 为什么消息队列是间接通信
消息队列 message queue 是典型的间接通信机制。
它的形式是:
1 | 发送者 -> 消息队列 -> 接收者 |
发送者把消息放入队列,接收者从队列取出消息。发送者不一定关心最后由谁接收,接收者也不一定关心消息由谁发送。
它们只需要共享同一个队列,并具有相应权限。
消息队列通常具备以下特征:
| 特征 | 含义 |
|---|---|
| 队列组织 | 消息按队列形式保存 |
| FIFO | 默认先进入的消息先被读取 |
| 消息边界 | 每条消息是独立单位 |
| 类型字段 | 消息可以带类型 |
| 权限控制 | 只有有权限的进程可以访问 |
| 多发送者/接收者 | 可支持多个进程共同使用 |
消息队列相比普通字节流更结构化。它传递的是“消息”,而不是单纯的字节序列。
例如,消息可能包含:
1 | type: 1 |
接收者可以按顺序读取,也可以按类型读取。
❗ 本节核心结论: 消息队列之所以是间接通信,是因为发送者和接收者通过共同访问队列建立连接,而不是直接指定彼此通信。
12. 管道与消息队列的区别
管道 pipe 和消息队列都可以作为间接通信方式,但它们的抽象不同。
| 对比项 | 管道 Pipe | 消息队列 Message Queue |
|---|---|---|
| 数据形式 | 字节流 | 一条条消息 |
| 消息边界 | 通常不保留明确消息边界 | 保留消息边界 |
| 组织方式 | 固定缓冲区更常见 | 队列或链表式组织 |
| 读取方式 | 按字节顺序读取 | 可按顺序或类型读取 |
| 通信关系 | 常用于父子进程或相关进程 | 可用于多个无关进程 |
| 结构化程度 | 较低 | 较高 |
管道更像水流:写进去的是连续字节,读出来也是连续字节。
消息队列更像信箱:每封信有边界,甚至可以带类型。
13. 同步与异步:发起操作后要不要等结果
同步和异步讨论的是:
发起通信操作之后,调用者要不要等待结果。
13.1 同步通信
同步 communication 的特点是:发起操作后等待结果。
例如:
1 | Send(message) |
如果消息没有成功发出,发送者就等待。
或者:
1 | Recv(message) |
如果还没有消息到来,接收者就等待。
可以类比为打电话:打过去之后,要等对方接通或回应,才能继续当前事情。
同步的优点是:
- 逻辑简单;
- 结果明确;
- 程序控制流容易理解;
- 不容易漏处理返回结果。
缺点是:
- 可能卡住;
- 对方不回应时,自己无法继续;
- 大量同步等待会降低并发能力。
13.2 异步通信
异步 communication 的特点是:发起操作后不等待结果,调用者可以继续做别的事情。
例如:
1 | AsyncSend(message) |
发送请求提交后直接返回。
或者:
1 | TryRecv(message) |
如果当前没有消息,也不等待,直接返回“没有消息”。
可以类比为发消息:消息发出去后,不必一直盯着对方是否立即回复,可以先去做别的事情。
异步的优点是:
- 不容易被单个通信操作卡住;
- 吞吐量更高;
- 适合高并发;
- 可以同时处理大量请求。
缺点是:
- 编程复杂;
- 需要回调、事件、轮询或状态记录;
- 错误处理更麻烦;
- 可能出现消息已发出但未及时处理的情况。
❗ 本节核心结论: 同步就是“发起后等待结果”,异步就是“发起后不等待结果”。
14. 阻塞与非阻塞:当前执行流会不会停住
阻塞和非阻塞关注的是:
当前执行流会不会因为某个操作停住。
| 概念 | 含义 |
|---|---|
| 阻塞 | 当前执行流停住,直到条件满足 |
| 非阻塞 | 当前执行流不等待,立即返回 |
在 IPC 中:
- 阻塞发送:如果消息暂时发不出去,发送者等待;
- 非阻塞发送:如果消息发不出去,立即返回失败或状态码;
- 阻塞接收:如果没有消息,接收者等待;
- 非阻塞接收:如果没有消息,立即返回“无消息”。
在很多操作系统教材中,可以先近似理解为:
| 通信语义 | 执行表现 |
|---|---|
| 同步 | 通常表现为阻塞 |
| 异步 | 通常表现为非阻塞 |
但严格来说,两组概念关注角度不同:
| 概念组 | 关注点 |
|---|---|
| 同步 / 异步 | 调用者与结果之间的关系 |
| 阻塞 / 非阻塞 | 当前执行流是否停住 |
例如,某些异步操作也可能在提交阶段短暂阻塞;某些同步操作也可能通过轮询方式非阻塞地检查结果。因此,在初学阶段可以先记成:
同步就是等,异步就是不等;阻塞就是停住,非阻塞就是不停住。
但进一步学习时要知道:它们不是完全同义词。
15. 超时机制:同步与异步之间的折中
如果一直同步等待,可能永远卡住。
如果完全异步不等,又可能让控制逻辑变复杂。
因此,系统常提供 timeout,超时机制。
形式上可以写成:
1 | Send(A, message, Time-out) |
含义是:
我愿意等,但最多等
Time-out这么久。超过时间仍未完成,就返回错误或超时状态。
超时机制相当于一种折中:
| 模式 | 行为 |
|---|---|
| 无限等待 | 阻塞 / 同步 |
| 完全不等 | 非阻塞 / 异步 |
| 等一段时间 | 带超时的同步 |
例如:
1 | Recv(message, 5s) |
表示最多等待 5 秒。如果 5 秒内收到消息,就正常返回;如果没有收到,就返回超时。
这样既避免了无限卡死,也保留了同步等待的简单控制流。
16. 三个核心问题的统一理解
本讲中的三个重点问题可以统一起来看:
16.1 环形缓冲区回答“数据放哪里”
共享内存只是提供了一块共同区域,但没有规定数据如何排队。环形缓冲区通过 in 和 out 管理写入位置和读取位置,并通过空满条件防止读空和覆盖。
它回答的是:
- 新数据写在哪里?
- 旧数据从哪里读?
- 空间如何循环复用?
- 什么时候不能写?
- 什么时候不能读?
16.2 消息队列回答“通信双方如何连接”
消息队列不是让发送者直接找接收者,而是让双方共享一个中间队列。
它回答的是:
- 发送者是否必须知道接收者?
- 多个发送者和接收者如何共享通信通道?
- 消息如何排队?
- 消息是否有类型?
- 由谁负责保存未读消息?
16.3 同步/异步回答“调用者是否等待”
同步和异步讨论的是通信操作发起后的控制流问题。
它回答的是:
- 发起发送后是否要等?
- 发起接收后是否要等?
- 没有消息时是否阻塞?
- 消息发不出去时是否返回?
- 是否需要超时机制?
flowchart TD
A[IPC 设计问题] --> B[数据放在哪里]
A --> C[通信双方如何连接]
A --> D[发起操作后是否等待]
B --> B1[共享内存]
B1 --> B2[环形缓冲区]
C --> C1[直接通信]
C --> C2[间接通信]
C2 --> C3[消息队列 / 管道 / 信箱]
D --> D1[同步 / 阻塞]
D --> D2[异步 / 非阻塞]
D --> D3[超时机制]
这张图展示了 IPC 的三条理解线索:存储位置、连接方式和等待语义。
17. 易混概念整理
| 易混点 | 正确理解 |
|---|---|
| 环形缓冲区是否等于同步机制 | 不等于。它是数据组织方式,需要锁、信号量等配合 |
| 共享内存是否自动安全 | 不安全。共享只是能访问,同步还要额外处理 |
in == out 表示什么 |
缓冲区为空 |
(in + 1) % BUFFER_SIZE == out 表示什么 |
缓冲区满,继续写会覆盖未读数据 |
| 消息队列为什么是间接通信 | 因为通信双方通过共享队列连接,而不是直接指定彼此 |
| 直接通信和间接通信区别 | 直接通信指定进程,间接通信指定中间对象 |
| 管道和消息队列区别 | 管道偏字节流,消息队列保留消息边界和类型 |
| 同步和阻塞是否完全一样 | 不完全一样,但初学可近似理解为同步常表现为阻塞 |
| 异步和非阻塞是否完全一样 | 不完全一样,但初学可近似理解为异步常表现为非阻塞 |
| 超时机制属于什么 | 在无限等待和完全不等之间折中 |
18. 复习清单
学完本讲后,应能回答:
- 为什么进程默认不能直接访问彼此内存?
- IPC 主要解决哪几类问题?
- 共享内存通信为什么快?
- 共享内存为什么同步复杂?
- 环形缓冲区为什么要设计成“环形”?
in和out分别表示什么?- 如何判断环形缓冲区为空?
- 如何判断环形缓冲区为满?
- 环形缓冲区为什么不能单独保证并发安全?
- 忙等为什么浪费 CPU?
- 消息传递和共享内存有什么区别?
- 什么是直接通信?
- 什么是间接通信?
- 为什么消息队列是间接通信?
- 管道和消息队列有什么区别?
- 同步和异步的核心区别是什么?
- 阻塞和非阻塞的核心区别是什么?
- 为什么说同步/异步与阻塞/非阻塞不是完全同一组概念?
- 超时机制解决了什么问题?
最终总结
进程间通信的核心不是单纯“传数据”,而是围绕三个问题展开:
数据放在哪里
- 共享内存把数据放在共同映射区域;
- 环形缓冲区负责在共享区域中组织数据位置和复用空间。
通信双方如何连接
- 直接通信要求发送者明确知道接收者;
- 间接通信通过消息队列、管道、信箱等中间对象建立联系。
调用者是否等待结果
- 同步通信等待结果,逻辑简单但可能卡住;
- 异步通信不等待结果,并发能力强但编程复杂;
- 超时机制在二者之间提供折中。
因此,IPC 的学习重点不是背诵机制名称,而是理解每种机制在解决什么问题,又引入了什么新问题。共享内存快,但同步难;消息队列清晰,但可能有额外开销;同步简单,但会等待;异步高效,但控制复杂。真正掌握 IPC,就是能围绕这些取舍判断某个场景下应该选择哪种通信方式。




