进程间通信
为什么需要进程间通信
每个进程拥有独立的地址空间,进程 A 无法直接读写进程 B 的内存。但多个进程之间经常需要协作(如生产者-消费者模型),因此操作系统提供了多种进程间通信(Inter-Process Communication, IPC) 机制。
IPC 方式总览
| 方式 | 方向 | 是否需要内核中转 | 适用场景 |
|---|---|---|---|
| 管道 | 单向 | 是 | 父子进程间的简单数据流 |
| 命名管道 | 单向 | 是 | 无亲缘关系进程间通信 |
| 消息队列 | 双向 | 是 | 结构化消息传递 |
| 共享内存 | 双向 | 否(最快) | 大量数据共享 |
| 信号量 | 同步 | 是 | 控制并发访问 |
| 信号 | 异步通知 | 是 | 通知进程发生了某事件 |
| Socket | 双向 | 是 | 跨机器网络通信 |
管道(Pipe)
管道是最简单的 IPC 方式,本质是内核中的一块缓冲区(通常 64KB),一端写入,另一端读出。
父进程 内核缓冲区 子进程
┌────────┐ ┌──────────────┐ ┌────────┐
│ │──write──▶│ ████████ │──read──▶ │ │
│ fd[1] │ │ 管道缓冲区 │ │ fd[0] │
└────────┘ └──────────────┘ └────────┘
写端 读端
特点:
- 半双工:数据单向流动(要双向通信需要两根管道)
- 仅限有亲缘关系的进程(
fork()创建的父子进程) - 面向字节流:没有消息边界
Shell 中的 | 就是管道:ls | grep ".txt" 创建了一个管道,ls 的输出通过管道传递给 grep。
命名管道(FIFO)
命名管道在文件系统中有一个路径名,因此无亲缘关系的进程也能使用:
# 创建命名管道
mkfifo /tmp/my_pipe
# 终端 1: 写入
echo "hello" > /tmp/my_pipe
# 终端 2: 读取
cat /tmp/my_pipe # 输出: hello
消息队列(Message Queue)
消息队列是内核中的一个链表,进程可以向队列中发送和接收结构化的消息。
进程 A 消息队列 进程 B
┌────────┐ ┌─────┬─────┬─────┬────┐ ┌────────┐
│ msgsnd │───▶│msg1 │msg2 │msg3 │ │───▶│ msgrcv │
└────────┘ └─────┴─────┴─────┴────┘ └────────┘
每条消息有类型(type)和数据(data)
与管道的区别:
- 消息有类型,接收方可以按类型选择接收
- 消息有边界,不会出现粘包问题
- 可以被多个进程共享
- 生命周期独立于进程(进程退出后消息队列仍在)
共享内存(Shared Memory)
共享内存是最快的 IPC 机制,因为数据不需要在用户态和内核态之间复制。
进程 A 的地址空间 物理内存 进程 B 的地址空间
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │共享区域 │──┼──────┼▶│共享页面 │◀─┼─────┼──│共享区域 │ │
│ └────────┘ │ │ └────────┘ │ │ └────────┘ │
│ │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
两个进程的虚拟地址
映射到同一块物理内存
为什么最快? 管道和消息队列需要将数据从进程 A 复制到内核缓冲区,再从内核缓冲区复制到进程 B,共 2 次拷贝。共享内存直接映射到同一块物理页面,0 次拷贝。
缺点:需要进程自行解决同步问题(通常配合信号量使用),否则会出现数据竞争。
信号量(Semaphore)
信号量是一个计数器,用于控制多个进程对共享资源的访问(不直接传递数据,而是做同步)。
信号量 S = 3(表示有 3 个可用资源)
进程 A: P(S) → S=2, 获得资源
进程 B: P(S) → S=1, 获得资源
进程 C: P(S) → S=0, 获得资源
进程 D: P(S) → S=0, 阻塞等待...
进程 A: V(S) → S=1, 释放资源 → 唤醒进程 D
- P 操作(wait):S > 0 则 S-- 并继续;S == 0 则阻塞
- V 操作(signal):S++ 并唤醒一个等待的进程
当 S 的最大值为 1 时,信号量退化为互斥锁(Mutex)。
信号(Signal)
信号是一种异步通知机制,用于通知进程发生了某个事件。类似于手机的通知推送——不管你在做什么,通知到了就会弹出。
| 信号 | 编号 | 默认行为 | 说明 |
|---|---|---|---|
| SIGTERM | 15 | 终止 | 礼貌地请求进程退出(可捕获) |
| SIGKILL | 9 | 终止 | 强制杀死进程(不可捕获) |
| SIGINT | 2 | 终止 | Ctrl+C 触发 |
| SIGSEGV | 11 | 终止+core | 段错误(非法内存访问) |
| SIGCHLD | 17 | 忽略 | 子进程状态变化(退出/停止) |
| SIGSTOP | 19 | 停止 | 暂停进程(不可捕获) |
| SIGCONT | 18 | 继续 | 恢复被暂停的进程 |
Socket
Socket 是最灵活的 IPC 方式,不仅支持同一机器上的进程通信,还支持跨网络通信。
- Unix Domain Socket:同一机器内的进程通信,不走网络协议栈,性能接近共享内存
- TCP/UDP Socket:跨机器的网络通信
# Unix Domain Socket 示例
# 服务端监听一个文件路径,客户端连接该路径
# 比 TCP loopback 更快,因为不经过网络协议栈
各 IPC 方式对比
| 方式 | 速度 | 容量 | 是否跨机器 | 复杂度 |
|---|---|---|---|---|
| 管道 | 中 | 64KB | ❌ | 低 |
| 消息队列 | 中 | 可配置 | ❌ | 中 |
| 共享内存 | 最快 | 可配置 | ❌ | 高(需同步) |
| 信号量 | — | — | ❌ | 中 |
| 信号 | 快 | 极小(编号) | ❌ | 低 |
| Socket | 较慢 | 无限 | ✅ | 高 |
生产高频题
进程间通信的方式有哪些?
管道(匿名/命名)、消息队列、共享内存、信号量、信号、Socket。其中共享内存最快(零拷贝),Socket 最灵活(可跨网络)。
管道和共享内存有什么区别?
管道通过内核缓冲区中转数据(2 次拷贝),共享内存让两个进程的虚拟地址映射到同一块物理内存(0 拷贝)。共享内存更快但需要自行处理同步问题。