这是“操作系统系列教程”的第二十一篇。前面几篇我们讲了进程、线程、调度、同步、死锁,知道了进程是操作系统分配资源的基本单位。但有一个关键问题一直没有回答:进程之间如何交换数据?一个进程产生的数据,怎么传给另一个进程?答案就是——进程间通信(Inter-Process Communication,IPC)。

进程之间的“墙”

先来想一个问题。在操作系统中,每个进程都有自己独立的虚拟地址空间。进程 A 看不到进程 B 的内存,进程 B 也看不到进程 A 的内存。这堵“墙”是操作系统为了保护进程而刻意设置的:

  • 防止一个进程意外或恶意修改另一个进程的数据;
  • 防止一个进程崩溃影响其他进程;
  • 让每个进程都以为自己独占内存。

但这堵“墙”也带来了一个麻烦:进程之间无法直接交换数据。

现实中,进程之间经常需要协作。比如:

  • 浏览器进程需要把数据传给渲染进程,才能显示页面;
  • 数据库进程需要把查询结果传给应用进程;
  • Shell 需要把 ls 命令的输出传给 grep 命令进行过滤。

如果进程之间不能通信,这些功能都无法实现。于是,操作系统提供了进程间通信机制,在隔离的“墙”上开了一扇扇“门”。

这篇文章,我们就来把“进程间通信”这件事讲清楚。

说明:本文涉及管道、消息队列、共享内存、信号量等概念。初学者如果遇到暂时看不懂的术语,可以先跳过细节,重点理解“进程之间需要交换数据,操作系统提供了多种通信方式”这一核心思想。

一、为什么需要进程间通信?

1.1 进程隔离的代价

进程隔离是操作系统的核心设计之一。它保证了:

  • 安全性:一个进程不能读取另一个进程的密码、密钥等敏感数据;
  • 稳定性:一个进程崩溃不会导致其他进程崩溃;
  • 独立性:每个进程都以为自己独占内存。

但隔离也意味着:进程之间不能直接读写对方的数据。如果两个进程需要协作,就必须通过操作系统提供的特殊机制来传递数据。

1.2 进程间通信的定义

给出一个正式的定义:

进程间通信(IPC)是操作系统提供的、允许不同进程之间交换数据和协调行为的机制。

这个定义里有三个关键点:

  1. 不同进程:IPC 用于进程之间,而不是同一进程内的线程(线程共享内存,通信更简单);
  2. 交换数据:IPC 的核心目的是传递数据;
  3. 协调行为:IPC 也可以用于同步,比如一个进程等待另一个进程完成某项工作。

1.3 IPC 在系统中的位置

在这里插入图片描述

图 1:IPC 在系统中的位置。 内核提供 IPC 机制,进程通过内核交换数据。

从图 1 可以看出,IPC 机制由内核提供。进程不能直接通信,但可以通过内核提供的“通道”交换数据。

二、进程间通信的主要方式

操作系统提供了多种 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 常见信号

信号编号含义
SIGINT2中断(Ctrl+C)
SIGKILL9强制终止
SIGTERM15终止请求
SIGCHLD17子进程状态改变
SIGUSR110用户自定义信号 1

7.3 信号的使用

// 发送信号
kill(pid, SIGTERM);

// 注册信号处理函数
signal(SIGTERM, handler);

kill

通知

执行

进程A

信号

进程B

信号处理函数

图 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 方式的对比

方式数据传输同步跨主机速度适用场景
匿名管道字节流自动否快父子进程
命名管道字节流自动否快任意进程
消息队列有边界自动否中结构化数据
共享内存任意需配合否最快大量数据
信号量不传数据是否快同步
信号信号编号异步否快事件通知
套接字字节流自动是中网络、通用

是

否

是

否

是

否

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 的扩展。

IPC 应用

Shell 管道

数据库客户端

浏览器多进程

微服务

图 11:IPC 的实际应用场景。

十一、常见误区澄清

误区真相
“管道可以双向通信”匿名管道是半双工的,双向需要两个管道
“共享内存不需要同步”共享内存本身不提供同步,必须配合信号量
“消息队列比共享内存快”共享内存最快,消息队列需要内核复制
“信号可以传递大量数据”信号只传递编号,不传递数据
“套接字只能用于网络”套接字也可用于本机进程间通信
“所有 IPC 方式都跨主机”只有套接字支持跨主机
“线程间通信用 IPC”线程共享内存,不需要 IPC
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐