共享内存与 RPC
基于通用操作系统概念 · 核于 2026-08
速查
- 共享内存(shm):最快的 IPC——让多个进程映射同一块物理内存到各自虚拟地址空间,直接读写,零内核拷贝。
- 为何最快:管道/消息队列/socket 每次通信都要两次内核拷贝(用户→内核→用户);共享内存映射后是普通内存访问,不经内核,无拷贝、无 syscall。
- 代价:必须配信号量同步——共享内存本身不提供任何同步,多进程并发读写会竞态,必须用信号量/互斥锁保护临界区。
- mmap:另一种映射方式,可映射匿名内存(
MAP_ANONYMOUS,父子共享)或文件(多进程映射同一文件实现共享 + 持久化)。 - System V vs POSIX:两套 API——
shmget/shmat(System V)vsshm_open/mmap(POSIX,更现代,文件描述符语义)。 - RPC(远程过程调用):让跨机调用像本地函数——客户端调 stub,stub 序列化参数经网络发给服务端,服务端 stub 反序列化后执行,结果传回。
- RPC 的核心:stub(桩)+ 序列化(marshalling)+ 网络传输——隐藏网络细节,典型实现 gRPC(Protobuf)、Thrift、Dubbo。
- 同步 vs 异步 RPC:同步 RPC 等待返回(像普通函数);异步 RPC 立即返回 future/callback,不阻塞。
- 共享内存不能跨机:物理页只能本机共享;跨机只能 socket/RPC。
- 速度对比:共享内存 > 消息队列 ≈ 管道 > UNIX socket > TCP socket > RPC(含序列化)。
一、共享内存:最快的 IPC
共享内存让多个进程把同一块物理内存页映射进各自的虚拟地址空间。映射完成后,A 进程写 ptr = 42,B 进程读 ptr 直接看到 42——没有任何内核介入,就是普通的内存读写。
进程 A 虚拟地址空间 物理内存 进程 B 虚拟地址空间
┌────────────┐ ┌──────────┐ ┌────────────┐
│ 虚页 X ────┼────────→│ 共享页 P │←───────┼── 虚页 Y │
│ ptr = 0x..│ │ (4KB) │ │ ptr = 0x.. │
└────────────┘ └──────────┘ └────────────┘
A 写 ptr = 42 ↑ B 读 ptr → 42
(普通 store) 同一物理页 (普通 load)- 为何最快:管道/消息队列每次通信,发送方
write(用户→内核拷贝),接收方read(内核→用户拷贝),两次拷贝 + 两次 syscall。共享内存映射好后是直接内存访问,零拷贝、零 syscall——大块数据(视频帧、数据库缓冲、X11 图像)传输首选。 - 代价:无同步。共享内存不保证A 写完 B 才读——若 A 正在写一个 8 字节的 long,B 可能读到一半旧一半新(torn read)。必须用信号量/互斥锁协调:A 加锁→写→解锁,B 加锁→读→解锁。
- 必须配信号量:实际工程里共享内存几乎总是和信号量成对出现——内存传数据,信号量管时序。
二、两种共享内存 API
System V 共享内存(shmget/shmat)
c
int shmid = shmget(key, size, IPC_CREAT | 0666); // 申请一块
void *addr = shmat(shmid, NULL, 0); // 映射到自己地址空间
// ... 双方读写 addr ...
shmdt(addr); // 解除映射
shmctl(shmid, IPC_RMID, NULL); // 删除(释放物理页)- 用
key(通常是ftok生成)让多个进程定位同一块。 ipcs -m可查看、ipcrm -m <id>可删除。
POSIX 共享内存(shm_open/mmap)
c
int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, size); // 设定大小
void *addr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
// ... 双方读写 addr ...
munmap(addr, size); close(fd);
shm_unlink("/myshm");- POSIX 更现代,fd 语义,可与
mmap/ftruncate配合,Linux 下文件在/dev/shm。
mmap 映射文件
mmap 还能映射普通文件——多进程映射同一文件,既共享内存又持久化到磁盘(数据库如 PostgreSQL 用得多)。
三、信号量:共享内存的同步搭档
信号量(semaphore)本身不传数据,是一个非负计数器,提供两个原子操作:
- P(wait/proberen):若计数 > 0 则减 1 返回;若为 0 则阻塞等待。
- V(post/verhogen):计数加 1,唤醒一个等待者。
经典配对:用信号量保护共享内存的临界区——进程 A 写共享内存前 P(sem)、写完 V(sem);进程 B 读前 P(sem)、读完 V(sem)。这样保证"一次只有一个进程在临界区",消除竞态。
四、RPC:跨机的"本地调用"
RPC(Remote Procedure Call)是跨机通信的高层抽象——让调用远程机器上的函数,写起来像调用本地函数。它把网络的复杂性(连接、序列化、超时、重试)藏到 stub(桩) 里:
客户端 服务端
调用 add(1, 2)
↓
客户端 stub 服务端 stub
序列化参数 ──网络( socket )──→ 反序列化参数
调用真正的 add(1,2) → 3
反序列化结果 ←──网络────────── 序列化结果 3
得到 3,像本地函数一样返回- stub(桩):客户端和服务端各有一份,负责参数的**序列化(marshalling)**与反序列化,让调用方感觉不到网络存在。
- 序列化:把参数(int/struct/list)转成字节流——Protobuf、JSON、Thrift 都做这个。
- 传输:底层通常是 TCP socket 或 HTTP/2(gRPC 用 HTTP/2 多路复用)。
- 典型实现:gRPC(Google,Protobuf + HTTP/2)、Apache Thrift、Dubbo(阿里)、JSON-RPC。
- 同步 vs 异步:同步 RPC 调用后阻塞等返回(像普通函数调用);异步 RPC 立即返回 future/callback,不阻塞调用线程。
- 与 socket 的关系:RPC 是 socket 之上的应用层封装——它仍是 socket 通信,只是加了 stub + 序列化让接口更友好。
五、各 IPC 机制对比表
| 机制 | 速度 | 数据边界 | 跨机 | 同步语义 | 典型场景 |
|---|---|---|---|---|---|
| 共享内存 | 最快(零拷贝) | 无(字节) | ❌ | 需自配信号量 | 大数据/视频帧/数据库 |
| 消息队列 | 快 | ✅ 有边界 | ❌ | 内核管收发阻塞 | 任务分发/结构化消息 |
| 匿名管道 | 快 | 无(字节流) | ❌ | 阻塞,仅父子 | shell |、父子通信 |
| 命名 FIFO | 快 | 无(字节流) | ❌ | 阻塞,任意进程 | 无亲缘进程通信 |
| 信号量 | 快 | —(不传数据) | ❌ | 本身即同步原语 | 保护临界区 |
| 信号 | 极快 | —(只传编号) | 部分 | 异步中断 | 通知/控制(Ctrl+C) |
| 套接字 | 慢(协议栈) | TCP 无 / UDP 有 | ✅ | 阻塞或非阻塞 | 网络通信、本机跨进程 |
| RPC | 慢(序列化+网络) | ✅ 有边界 | ✅ | 同步/异步 | 微服务、分布式 |
选型口诀:单机要最快 → 共享内存+信号量;要消息边界 → 消息队列;shell 串命令 → 管道;跨机 → 套接字,要像函数调用 → RPC;只通知 → 信号。
下一步
掌握所有机制后,去参考看完整的代码模板、选型决策树、易错点清单与权威链接——把各机制的 API 速查与陷阱固化下来。