进程之间如何“对话”?一篇讲清六种进程间通信方式

进程之间是隔开的------每个进程一张自己的页表,A 里写的 int x = 1,B 里根本看不到这个 x。这种隔离正是进程存在的意义,但也意味着两个进程想把数据递给对方,光靠自己读写内存是做不到的,必须绕一圈,借内核帮忙。

借道内核的方式有很多种,经典的六种是:管道、命名管道、消息队列、共享内存、信号量、Socket。它们解决的是同一件事,但各走各的路,代价和适用场景差很多。


管道 pipe

管道是最简单的一种:内核里开一块缓冲区,一头写、一头读。

c 复制代码
int fd[2];
pipe(fd);        // fd[0] 是读端,fd[1] 是写端

你肯定用过它,只是没注意:

shell 复制代码
ls | grep txt

这条命令里,ls 的标准输出和 grep 的标准输入之间那个 |,就是 shell 帮两个进程建的一个管道。

它的特点很明确:

  • 单向。数据只能从写端流到读端,想双向得开两个管道。
  • 没有名字 。它只是两个文件描述符,别的进程拿不到,所以只能给有亲缘关系 的进程用(靠 fork 把 fd 继承过去)。
  • 字节流,没有边界 。写进去 hello 和 world,读端可能一次读成 helloworld,粘在一起,怎么切分要自己约定。

写端写满缓冲区(默认 64KB)会阻塞,读端读空了也会阻塞。所有写端都关掉后,读端会读到 EOF;所有读端都关掉后,写端再写会收到 SIGPIPE。这几条规则,就是 shell 里管道流式处理的底层。


命名管道 FIFO

管道限制在亲缘进程,太窄。命名管道(FIFO)就是给它加一个文件名:

shell 复制代码
mkfifo /tmp/myfifo

之后任何进程都能 open("/tmp/myfifo", ...),跟打开普通文件一样。它和匿名管道在读写语义上完全一样------还是单向、还是字节流、还是写满就阻塞------唯一的区别就是有了名字,不认识的两个进程也能对接 。mkfifo 出来的文件在磁盘上不占数据块,它只是个路标,真正的缓冲还是在内存里。


消息队列

管道是"一连串字节",消息队列是"一连串消息"。内核里维护一条链表,每个节点是一条有明确边界的消息。

c 复制代码
msgget(...);                 // 拿到队列 id
msgsnd(qid, &msg, len, 0);   // 发一条
msgrcv(qid, &msg, len, type, 0);  // 收一条

比管道多出来的能力:

  • 有边界。一条是一条,不会像管道那样粘成一坨,收的人不用自己拼。
  • 可以按类型取 。msgrcv 的 type 参数能指定只收某一类消息,不一定是先进先出。
  • 有生命周期。队列挂在内核上,发送进程退出了队列还在,直到显式删除。

代价是每发一条要拷贝两次(用户态→内核,内核→另一进程的用户态),量一大就慢。


共享内存

前面三种都是"内核帮你搬数据",共享内存干脆不搬:把同一块物理内存映射进两个进程的地址空间。

c 复制代码
shmget(...);              // 申请一块共享内存段
shmat(shmid, NULL, 0);    // 映射到自己的地址空间,拿到指针

映射完成后,两个进程拿到的指针指向同一块物理页,一个写、另一个立刻能读到。没有拷贝,没有内核中转,是六种里最快的。

代价也来自这个"快":内核只负责把它们接上,之后就撒手了,不提供任何同步。两个进程同时写同一块内存,读到什么全看运气。所以共享内存几乎从不单独用,一定要配一个同步手段------典型搭配就是下一节的信号量。

换个角度看,共享内存提供的其实是"通信的场所",而不是"通信的机制"。交换数据靠它,协调谁先说谁后说,得靠信号量。


信号量

信号量不传数据,它管的是同步和互斥:谁可以进临界区、最多几个进程同时进。

它的语义是经典的 P/V 操作:

  • P(减):把信号量减 1,如果已经是 0 就阻塞,等别人加回来。
  • V(加):把信号量加 1,唤醒一个正在等的人。

初始化成 1,就是一把互斥锁(同一时刻只有一个进程能进);初始化成 N,就是"最多 N 个并发"的限流器。配共享内存用的时候,就是每个进程访问共享内存前先 P、访问完 V,保证不会互相踩。

它和信号(signal)名字像,干的完全是两回事:信号量是"等你把资源让出来",信号是"通知你出事了",后者只能传一个编号,信息量太小,通常不单独算一种 IPC。


Socket

上面五种都有一个共同的局限------都在同一台机器上。Socket 是唯一能跨主机的:

c 复制代码
socket(AF_INET, SOCK_STREAM, 0);   // 跨机器,走 TCP
socket(AF_UNIX, SOCK_STREAM, 0);   // 本机,走文件路径

Socket 是全双工 的,两边可以同时发,而且天然支持多对多、跨网络。本机通信时用 AF_UNIX(UNIX 域套接字),它走的是文件系统路径,不经过 TCP/IP 协议栈,比 AF_INET 快不少,这也是现在很多软件把本地服务从 TCP 换成 UNIX 域套接字的原因。

代价是它最"重":要建连接、要管状态、走一遍协议栈。但换来的是通用性------别的五种都出不了这台机器,只有它行。CS162 Note5 的 IPC 一节里从 file 当通信通道的角度也铺过一遍,可以对照着看。


六种对比

方式 方向 有名 / 可跨进程 可跨机器 数据边界 拷贝次数 典型场景
管道 单向 无名,仅亲缘进程 否 字节流 内核缓冲,2 次 shell 的 `
命名管道 单向 有文件名,任意进程 否 字节流 2 次 本机不相关进程对接
消息队列 单向 内核对象,任意进程 否 有消息边界 2 次 结构化小消息
共享内存 双向 内核对象,任意进程 否 无(裸内存) 0 次 大数据量、高性能
信号量 无数据 内核对象,任意进程 否 ------ ------ 同步 / 互斥
Socket 双向 有地址,任意进程 是 字节流(可加帧) 视实现,通常 2 次 网络通信、本地服务

把这张表横着读一遍,取舍就清楚了:共享内存零拷贝最快,代价是没同步,得自己加信号量兜着;消息队列、管道慢一些,但内核把同步和边界都替你想好了;想出这台机器,就只有 Socket 这一个选择。

还有一点要注意,这六种说的是"经典 IPC 机制",实际写代码时在更上层还有别的选择------数据库、Redis、消息中间件,本质都是在 Socket 之上又包了一层。

相关推荐
我的div丢了肿么办39 分钟前
go语言通过反射来操作结构体中的数据
后端·go
知守观44 分钟前
同事的 SNAPSHOT 更新了,我三天后才拉到:Maven 更新检查被 _remote.repositories 静默跳过
java·后端·maven
斑鸠喳喳1 小时前
交换器 Exchanger
后端
霸道流氓气质1 小时前
MinIO 入门与实战:S3 兼容对象存储、Spring Boot 集成与 AI 应用文件管理
人工智能·spring boot·后端
我真是泰库辣1 小时前
我写了 300 行 Python,让代码替我在《三角洲行动》里吹口琴
后端
CaptainT1 小时前
先把例子跑起来:MySQL 环境与本文的示例数据
数据库·后端·mysql
程序猿乐锅1 小时前
【从0-1学习Python | 第四篇】Python 字符串和元组
java·开发语言·笔记·后端·python·学习
码事漫谈1 小时前
一条 ALTER TABLE 的跨库旅程:KFS 如何自动翻译 DDL
后端
Rain的Java大神之路1 小时前
10 万人抢 2000 个座位,电影票选座系统怎么防超卖?一文讲透大厂高频场景题
java·后端·面试