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,直接读取负载指针,从而避免把数据从内核复制到用户态。下图给出该链路的信号与数据流关系。

发送进程 Publisher POSIX 共享内存 /cyber_topic 消息头 + 负载 mmap 映射区 接收进程 Subscriber 写入 直接读取 零拷贝关键点 发送方只写一次共享内存,不经过内核 socket 缓冲区 接收方 mmap 后按指针读取,无 read/write 数据搬运 同步依赖信号量/原子变量,生命周期由 shm_unlink 管理

在这个链路里,真正决定性能的是“接收方是否直接拿到指针”。如果接收方仍然把共享内存内容复制到自己的堆缓冲区,那么零拷贝就退化为“少一次内核拷贝”,收益会明显下降。因此 Cyber RT 在消息结构上通常采用定长头 + 变长负载的布局,让接收方可以零解析地定位负载。

五、误区与复盘

  • 误区一:共享内存一定比 socket 快。小消息、低频通信时,建链与同步开销可能超过拷贝收益;共享内存更适合大块、高频、低延迟的数据流。
  • 误区二:映射成功就等于数据可见。没有同步时,接收方可能读到半写状态。必须配合序列号、双缓冲或内存屏障。
  • 误区三:忘记删除共享对象。System V 段和 POSIX 名字都会在进程退出后残留,长期运行会耗尽 /dev/shm 空间。上线前应检查 ipcs -m 与 ls /dev/shm。
  • 误区四:把 mmap 当万能。匿名映射无法跨无亲缘进程,跨主机更不可能,分布式场景仍需网络传输。
排查共享内存问题时,优先确认三件事:对象是否残留(ipcs -m / ls /dev/shm)、映射大小是否一致(ftruncate 与 mmap length)、同步原语是否与数据布局匹配。多数“读到脏数据”都源于后两者。

综上,System V 适合维护遗留系统,POSIX 共享内存是现代框架的主流选择,mmap 匿名映射则适合父子进程或作为底层实现。Cyber RT 的零拷贝能力,正是建立在 POSIX 共享内存 + mmap + 用户态同步这三件套之上。