什么是进程间通信(IPC)?管道、消息队列、共享内存全解析
这是“操作系统系列教程”的第二十一篇。前面几篇我们讲了进程、线程、调度、同步、死锁,知道了进程是操作系统分配资源的基本单位。但有一个关键问题一直没有回答:进程之间如何交换数据?一个进程产生的数据,怎么传给另一个进程?答案就是——进程间通信(Inter-Process Communication,IPC)。
进程之间的“墙”
先来想一个问题。在操作系统中,每个进程都有自己独立的虚拟地址空间。进程 A 看不到进程 B 的内存,进程 B 也看不到进程 A 的内存。这堵“墙”是操作系统为了保护进程而刻意设置的:
- 防止一个进程意外或恶意修改另一个进程的数据;
- 防止一个进程崩溃影响其他进程;
- 让每个进程都以为自己独占内存。
但这堵“墙”也带来了一个麻烦:进程之间无法直接交换数据。
现实中,进程之间经常需要协作。比如:
- 浏览器进程需要把数据传给渲染进程,才能显示页面;
- 数据库进程需要把查询结果传给应用进程;
- Shell 需要把
ls命令的输出传给grep命令进行过滤。
如果进程之间不能通信,这些功能都无法实现。于是,操作系统提供了进程间通信机制,在隔离的“墙”上开了一扇扇“门”。
这篇文章,我们就来把“进程间通信”这件事讲清楚。
说明:本文涉及管道、消息队列、共享内存、信号量等概念。初学者如果遇到暂时看不懂的术语,可以先跳过细节,重点理解“进程之间需要交换数据,操作系统提供了多种通信方式”这一核心思想。
一、为什么需要进程间通信?
1.1 进程隔离的代价
进程隔离是操作系统的核心设计之一。它保证了:
- 安全性:一个进程不能读取另一个进程的密码、密钥等敏感数据;
- 稳定性:一个进程崩溃不会导致其他进程崩溃;
- 独立性:每个进程都以为自己独占内存。
但隔离也意味着:进程之间不能直接读写对方的数据。如果两个进程需要协作,就必须通过操作系统提供的特殊机制来传递数据。
1.2 进程间通信的定义
给出一个正式的定义:
进程间通信(IPC)是操作系统提供的、允许不同进程之间交换数据和协调行为的机制。
这个定义里有三个关键点:
- 不同进程:IPC 用于进程之间,而不是同一进程内的线程(线程共享内存,通信更简单);
- 交换数据:IPC 的核心目的是传递数据;
- 协调行为:IPC 也可以用于同步,比如一个进程等待另一个进程完成某项工作。
1.3 IPC 在系统中的位置

图 1:IPC 在系统中的位置。 内核提供 IPC 机制,进程通过内核交换数据。
从图 1 可以看出,IPC 机制由内核提供。进程不能直接通信,但可以通过内核提供的“通道”交换数据。
二、进程间通信的主要方式
操作系统提供了多种 IPC 方式,每种方式适用于不同的场景。常见的有:
| 方式 | 特点 | 适用场景 |
|---|---|---|
| 管道 | 半双工,字节流 | 父子进程、兄弟进程 |
| 命名管道 | 有名字,可用于任意进程 | 任意进程之间 |
| 消息队列 | 有边界,支持优先级 | 结构化数据传输 |
| 共享内存 | 最快,需要同步 | 大量数据共享 |
| 信号量 | 用于同步,不传数据 | 协调进程行为 |
| 信号 | 异步通知 | 事件通知 |
| 套接字 | 可跨主机 | 网络通信、本机通信 |
图 2:常见的进程间通信方式。
三、管道
3.1 什么是管道?
管道(Pipe)是最古老的 IPC 方式之一,在 Unix 系统中广泛使用。它本质上是一个内核缓冲区,一个进程写入数据,另一个进程读取数据。
管道有两种:
- 匿名管道(Anonymous Pipe):没有名字,只能用于父子进程或兄弟进程之间;
- 命名管道(Named Pipe,FIFO):有名字,可以用于任意进程之间。
3.2 匿名管道
匿名管道通过 pipe() 系统调用创建:
int fd[2];
pipe(fd);
// fd[0] 用于读取,fd[1] 用于写入
匿名管道的特点:
- 半双工:数据只能单向流动。如果需要双向通信,需要创建两个管道;
- 字节流:没有消息边界,读取方需要自己解析;
- 生命周期:随进程结束而消失;
- 容量有限:内核缓冲区通常为 64KB。

图 3:匿名管道的工作原理。 一个进程写入,另一个进程读取,数据经过内核缓冲区。
经典应用:Shell 中的管道符号 |。
ls -l | grep ".txt"
这个命令创建了两个进程:ls 和 grep。ls 的输出通过管道传给 grep,grep 过滤出包含 .txt 的行。
3.3 命名管道
命名管道通过 mkfifo() 创建:
mkfifo /tmp/myfifo
命名管道的特点:
- 有名字:在文件系统中有一个路径名;
- 可用于任意进程:不限于父子进程;
- 半双工:和匿名管道一样,数据只能单向流动;
- 生命周期:除非显式删除,否则一直存在。

图 4:命名管道的工作原理。 通过文件系统中的路径名访问,可用于任意进程。
3.4 管道的优缺点
| 优点 | 缺点 |
|---|---|
| 简单易用 | 半双工,只能单向 |
| 内核提供,无需额外同步 | 容量有限 |
| 匿名管道无需命名 | 只能用于有亲缘关系的进程(匿名管道) |
| 读写自动阻塞 | 字节流,无消息边界 |
四、消息队列
4.1 什么是消息队列?
消息队列(Message Queue)是内核维护的一个链表,进程可以向队列中写入消息,也可以从队列中读取消息。
消息队列的特点:
- 有边界:每条消息有明确的长度,读取方一次读取一条完整消息;
- 支持优先级:消息可以有不同的优先级,高优先级的消息先被读取;
- 生命周期:随内核持续,除非显式删除;
- 可用于任意进程:通过消息队列 ID 访问。
4.2 消息队列的工作原理

图 5:消息队列的工作原理。 进程通过 msgsnd 发送消息,通过 msgrcv 接收消息。
在 Linux 中,消息队列的常用系统调用包括:
| 系统调用 | 作用 |
|---|---|
msgget() | 创建或获取消息队列 |
msgsnd() | 发送消息 |
msgrcv() | 接收消息 |
msgctl() | 控制消息队列(删除、获取状态) |
4.3 消息队列的优缺点
| 优点 | 缺点 |
|---|---|
| 有消息边界 | 消息大小有限制 |
| 支持优先级 | 内核复制数据,有开销 |
| 可用于任意进程 | 生命周期随内核,需要手动清理 |
| 读写自动同步 | 不适合大量数据传输 |
五、共享内存
5.1 什么是共享内存?
共享内存(Shared Memory)是最快的 IPC 方式。它的原理是:把同一块物理内存映射到多个进程的虚拟地址空间中。这样,多个进程就可以直接读写同一块内存,就像访问自己的内存一样。

图 6:共享内存的工作原理。 不同进程的虚拟页映射到同一块物理内存。
5.2 共享内存的使用
在 Linux 中,共享内存可以通过 shmget()、shmat()、shmdt()、shmctl() 等系统调用使用。
// 创建共享内存
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
// 映射到进程地址空间
void *ptr = shmat(shmid, NULL, 0);
// 读写共享内存
strcpy(ptr, "Hello, shared memory!");
// 解除映射
shmdt(ptr);
// 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
5.3 共享内存的优缺点
| 优点 | 缺点 |
|---|---|
| 速度最快 | 需要额外的同步机制 |
| 适合大量数据传输 | 实现复杂 |
| 直接读写,无内核复制 | 可能引入竞态条件 |
| 灵活 | 需要手动管理生命周期 |
关键问题:共享内存本身不提供同步机制。如果多个进程同时读写共享内存,就会出现竞态条件。因此,共享内存通常需要配合信号量或互斥锁使用。

图 7:共享内存与信号量配合使用。 信号量用于同步,避免竞态条件。
六、信号量
6.1 什么是信号量?
信号量(Semaphore)主要用于同步,而不是传递数据。它是一个整数变量,支持两种原子操作:
P操作(wait):如果信号量大于 0,则减 1;否则等待;V操作(signal):信号量加 1,唤醒等待的进程。
信号量在 IPC 中的作用是:协调多个进程对共享资源的访问。
6.2 信号量的使用
// 创建信号量
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666);
// P 操作
struct sembuf p = {0, -1, 0};
semop(semid, &p, 1);
// 临界区
// V 操作
struct sembuf v = {0, 1, 0};
semop(semid, &v, 1);
6.3 信号量的优缺点
| 优点 | 缺点 |
|---|---|
| 有效同步 | 不传递数据 |
| 支持多个进程 | 使用复杂 |
| 原子操作 | 可能导致死锁 |
七、信号
7.1 什么是信号?
信号(Signal)是一种异步通知机制。一个进程可以向另一个进程发送信号,接收进程注册信号处理函数来响应。
信号的特点:
- 异步:发送方不知道接收方何时处理;
- 轻量:只传递一个信号编号,不传递大量数据;
- 有限:信号类型有限(Linux 有 64 种)。
7.2 常见信号
| 信号 | 编号 | 含义 |
|---|---|---|
| SIGINT | 2 | 中断(Ctrl+C) |
| SIGKILL | 9 | 强制终止 |
| SIGTERM | 15 | 终止请求 |
| SIGCHLD | 17 | 子进程状态改变 |
| SIGUSR1 | 10 | 用户自定义信号 1 |
7.3 信号的使用
// 发送信号
kill(pid, SIGTERM);
// 注册信号处理函数
signal(SIGTERM, handler);
图 8:信号的工作机制。 发送方发送信号,接收方执行信号处理函数。
八、套接字
8.1 什么是套接字?
套接字(Socket)是最通用的 IPC 方式,最初为网络通信设计,但也可以用于本机进程间通信。
套接字的特点:
- 可跨主机:支持网络通信;
- 全双工:可以同时双向传输;
- 灵活:支持多种协议(TCP、UDP、Unix Domain Socket);
- 通用:几乎所有操作系统都支持。
8.2 套接字的类型
| 类型 | 说明 | 适用场景 |
|---|---|---|
| Unix Domain Socket | 本机进程间通信 | 高性能本机通信 |
| TCP Socket | 面向连接,可靠 | 网络通信 |
| UDP Socket | 无连接,不可靠 | 实时通信 |
8.3 套接字的使用
// 创建套接字
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
// 绑定地址
bind(sockfd, ...);
// 监听
listen(sockfd, 5);
// 接受连接
int clientfd = accept(sockfd, ...);
// 发送/接收数据
send(clientfd, buffer, len, 0);
recv(clientfd, buffer, len, 0);

图 9:套接字的工作机制。 支持双向通信,可用于本机或跨主机。
8.4 套接字的优缺点
| 优点 | 缺点 |
|---|---|
| 通用性强 | 开销相对较大 |
| 支持跨主机 | 实现复杂 |
| 全双工 | 需要管理连接 |
| 协议丰富 | 不适合极小量数据 |
九、各种 IPC 方式的对比
| 方式 | 数据传输 | 同步 | 跨主机 | 速度 | 适用场景 |
|---|---|---|---|---|---|
| 匿名管道 | 字节流 | 自动 | 否 | 快 | 父子进程 |
| 命名管道 | 字节流 | 自动 | 否 | 快 | 任意进程 |
| 消息队列 | 有边界 | 自动 | 否 | 中 | 结构化数据 |
| 共享内存 | 任意 | 需配合 | 否 | 最快 | 大量数据 |
| 信号量 | 不传数据 | 是 | 否 | 快 | 同步 |
| 信号 | 信号编号 | 异步 | 否 | 快 | 事件通知 |
| 套接字 | 字节流 | 自动 | 是 | 中 | 网络、通用 |
图 10:IPC 方式的选择指南。
十、实际应用场景
10.1 Shell 管道
Shell 中的管道是最常见的 IPC 应用:
ps aux | grep nginx | awk '{print $2}'
这个命令创建了三个进程,通过管道串联,数据从 ps 流向 grep,再流向 awk。
10.2 数据库与客户端
数据库服务器和客户端通常通过套接字通信。客户端发送 SQL 查询,服务器返回结果。
10.3 浏览器多进程架构
Chrome 浏览器采用多进程架构:浏览器进程、渲染进程、GPU 进程、插件进程。它们之间通过 IPC 交换数据。例如,渲染进程把页面内容传给浏览器进程显示。
10.4 微服务架构
微服务之间通过网络套接字通信(HTTP、gRPC 等)。虽然运行在不同主机上,但本质上也是 IPC 的扩展。
图 11:IPC 的实际应用场景。
十一、常见误区澄清
| 误区 | 真相 |
|---|---|
| “管道可以双向通信” | 匿名管道是半双工的,双向需要两个管道 |
| “共享内存不需要同步” | 共享内存本身不提供同步,必须配合信号量 |
| “消息队列比共享内存快” | 共享内存最快,消息队列需要内核复制 |
| “信号可以传递大量数据” | 信号只传递编号,不传递数据 |
| “套接字只能用于网络” | 套接字也可用于本机进程间通信 |
| “所有 IPC 方式都跨主机” | 只有套接字支持跨主机 |
| “线程间通信用 IPC” | 线程共享内存,不需要 IPC |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)