3. 共享内存基础
在 Cyber RT 的零拷贝通信链路中,共享内存是绕开内核缓冲区、让同一块物理内存被多个进程直接映射的底层基石。理解 System V、POSIX 与 mmap 三类机制的差异,是判断“何时能真正零拷贝、何时只是减少一次拷贝”的关键。本节从原理、API、优缺点与适用场景四个维度展开,并给出选型对照。
一、核心原理:共享内存到底共享了什么
共享内存的本质是:内核在物理内存中分配一段页框,然后把这同一段物理页分别映射到多个进程各自的虚拟地址空间。进程 A 写入的数据,进程 B 通过自己的虚拟地址即可读到,中间不经过 read/write 系统调用的数据搬运。因此它常被称为“最快的 IPC”,因为数据只被写一次、读一次,没有内核到用户态的复制。
但“快”是有代价的:内核不提供同步语义。多个进程同时读写同一块内存会产生竞态,必须由用户态配合信号量、互斥锁或原子变量来保证一致性。这也是 Cyber RT 在共享内存之上还要叠加无锁环形队列、序列号校验等机制的原因。
二、三类机制的原理与 API 对照
System V 共享内存是最早的机制,通过 shmget 创建或获取一个以 key 标识的共享段,shmat 将其挂载到进程地址空间,shmdt 解除挂载,shmctl 控制删除。它的标识是全局 key,生命周期独立于进程,即使所有进程退出,段仍然存在,需要显式删除,否则会残留占用内存。
key_t key = ftok("/tmp/cyber_shm", 'A');
int shmid = shmget(key, 4096, IPC_CREAT | 0666);
void *addr = shmat(shmid, NULL, 0);
/* 读写 addr ... */
shmdt(addr);
shmctl(shmid, IPC_RMID, NULL); /* 显式删除,否则残留 */
POSIX 共享内存以文件路径式名字标识,如 /cyber_topic,通过 shm_open 打开、ftruncate 设定大小、mmap 映射,最后 shm_unlink 删除。它更符合“文件即对象”的直觉,权限、名字管理更清晰,且天然与 mmap 配合,是现代 Linux 程序更常用的选择。
int fd = shm_open("/cyber_topic", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
/* 读写 addr ... */
munmap(addr, 4096);
shm_unlink("/cyber_topic");
mmap 匿名映射不依赖任何名字,通过 MAP_ANONYMOUS | MAP_SHARED 直接获得一段可共享的内存。它常用于父子进程之间(fork 后继承映射),或作为上层共享内存库的底层实现。它没有全局名字,因此不适合无亲缘关系的进程间发现与共享。
void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
/* fork 后父子进程共享该段 */
三、选型对照表
| 维度 | System V shm | POSIX shm | mmap 匿名映射 |
|---|---|---|---|
| 标识方式 | key(ftok 生成) | 路径名 /name | 无名字 |
| 核心 API | shmget/shmat/shmdt/shmctl | shm_open/ftruncate/mmap/shm_unlink | mmap/munmap |
| 生命周期 | 独立于进程,需显式删除 | 独立于进程,需 shm_unlink | 随映射进程全部退出而释放 |
| 跨无亲缘进程 | 支持 | 支持 | 不支持 |
| 与文件 IO 一致性 | 弱 | 强,可 fsync | 无文件语义 |
| 典型场景 | 遗留系统、老中间件 | 现代 IPC、Cyber RT 类框架 | 父子进程、临时共享缓冲 |
四、案例场景:Cyber RT 中的共享内存通道
Cyber RT 的共享内存传输通常按“发现—建链—映射—读写—回收”推进。发送方创建或打开一个以 topic 命名的 POSIX 共享内存对象,写入消息头与负载;接收方通过同样的名字打开并 mmap,直接读取负载指针,从而避免把数据从内核复制到用户态。下图给出该链路的信号与数据流关系。
在这个链路里,真正决定性能的是“接收方是否直接拿到指针”。如果接收方仍然把共享内存内容复制到自己的堆缓冲区,那么零拷贝就退化为“少一次内核拷贝”,收益会明显下降。因此 Cyber RT 在消息结构上通常采用定长头 + 变长负载的布局,让接收方可以零解析地定位负载。
五、误区与复盘
- 误区一:共享内存一定比 socket 快。小消息、低频通信时,建链与同步开销可能超过拷贝收益;共享内存更适合大块、高频、低延迟的数据流。
- 误区二:映射成功就等于数据可见。没有同步时,接收方可能读到半写状态。必须配合序列号、双缓冲或内存屏障。
- 误区三:忘记删除共享对象。System V 段和 POSIX 名字都会在进程退出后残留,长期运行会耗尽
/dev/shm空间。上线前应检查ipcs -m与ls /dev/shm。 - 误区四:把 mmap 当万能。匿名映射无法跨无亲缘进程,跨主机更不可能,分布式场景仍需网络传输。
综上,System V 适合维护遗留系统,POSIX 共享内存是现代框架的主流选择,mmap 匿名映射则适合父子进程或作为底层实现。Cyber RT 的零拷贝能力,正是建立在 POSIX 共享内存 + mmap + 用户态同步这三件套之上。