共享内存初探:什么是共享内存?为什么需要它?低延迟场景下的核心优势

大家好,我是你们这门课的老朋友。今天咱们来聊聊共享内存。

说实话,我第一次接触共享内存,是在做一个高频交易系统的原型。当时数据从A进程传到B进程,走的是Socket,延迟动不动就几十微秒。老板拍桌子说:“再慢就卷铺盖走人!” 我那时候就在想,有没有一种办法,让两个进程像两个线程一样,直接读写同一块内存?

嗯,答案就是共享内存。

什么是共享内存?

说白了,共享内存就是一块物理内存,被多个进程映射到了各自的虚拟地址空间里。

你想想看,正常情况下,每个进程都有自己的“小院子”,谁也看不见谁。共享内存呢,就是在这些院子之间开了一扇门。A进程往里面写数据,B进程立马就能读到。不需要复制,不需要经过内核。

核心概念:共享内存是IPC(进程间通信)中速度最快的一种方式。它绕过了内核的中间层,让数据在用户态直接交换。

我习惯用一张图来理解这个过程:

进程A 虚拟地址空间 (私有数据) 进程B 虚拟地址空间 (私有数据) 共享内存区域 (物理内存,被两个进程映射) mmap/shmget mmap/shmget 图:共享内存映射原理

为什么需要它?

你可能会问:“我直接用管道、消息队列、Socket不也行吗?”

行,当然行。但你要看场景。

我举个例子。假设你要在两个进程之间传递一个100KB的日志结构。用Socket的话,数据得从用户态复制到内核态,再复制到另一个进程的用户态。这一来一回,两次拷贝。再加上上下文切换,延迟轻松上到几十微秒。

而共享内存呢?

  • 零拷贝:数据只存在于物理内存中,两个进程直接读写。没有内核介入。
  • 无上下文切换:不需要系统调用(除了初始化的那一次)。
  • 极低延迟:纳秒级别的读写延迟,而不是微秒。

我的经验:在低延迟场景下,共享内存的延迟通常比Socket低2-3个数量级。我做过一个对比测试,同样是1KB数据交换,共享内存耗时约200纳秒,而Unix Domain Socket耗时约2微秒。差了10倍。

低延迟场景下的核心优势

咱们来细数一下,共享内存到底牛在哪。

特性 共享内存 传统IPC(Socket/管道)
数据拷贝次数 0次(直接读写) 2次(用户态→内核态→用户态)
上下文切换 0次(初始化后) 至少2次(每次读写)
延迟 纳秒级 微秒级
吞吐量 极高(受限于内存带宽) 中等(受限于内核处理能力)
同步机制 需要自行实现(信号量/自旋锁) 内核自动处理

看到没?共享内存最大的优势就是“快”。

但这里有个坑。我曾经在一个项目中,直接用共享内存做生产者-消费者模型,结果发现数据读出来是乱的。为什么?因为两个进程同时写同一块内存,没有做同步。

注意:共享内存本身不提供任何同步机制。你需要自己用信号量、互斥锁或原子操作来保证数据一致性。否则,轻则数据错乱,重则程序崩溃。

什么时候该用共享内存?

我个人习惯,在以下场景优先考虑共享内存:

  1. 高频交易:行情数据、订单簿更新,延迟就是金钱。
  2. 实时音视频处理:帧数据量大,延迟要求高。
  3. 游戏引擎:主逻辑和渲染进程之间交换状态。
  4. 嵌入式系统:资源受限,需要高效通信。

反过来,如果你的应用对延迟不敏感,或者数据量很小,用Socket或管道反而更省事。毕竟共享内存要自己处理同步,代码复杂度上去了。

一个简单的例子

咱们来看一段最基础的共享内存代码。这是Linux上用POSIX共享内存的写法:

// 创建共享内存对象
int shm_fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(shm_fd, 4096);  // 设置大小

// 映射到进程地址空间
void *ptr = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);

// 写入数据
sprintf((char *)ptr, "Hello from process A!");

// 读取数据(另一个进程中)
printf("Received: %s\n", (char *)ptr);

// 清理
munmap(ptr, 4096);
close(shm_fd);
shm_unlink("/my_shm");

这段代码看着简单,但实际项目中要注意的点不少。比如:

  • shm_open 创建的文件描述符要记得关闭。
  • mmapMAP_SHARED 标志不能写错,否则两个进程看不到同一块内存。
  • 共享内存的大小最好是页对齐(通常是4096字节的倍数)。

避坑指南:我曾经在 ftruncate 之后忘记检查返回值,结果共享内存大小设置失败,导致后续写入越界。嗯,从那以后我每次都会加一句 if (ftruncate(...) == -1) { perror("ftruncate"); exit(1); }

小结

共享内存,说白了就是让两个进程“住进同一个房间”。它快,因为它不经过内核。它难,因为你要自己管好“房间里的秩序”。

在低延迟场景下,共享内存几乎是唯一的选择。但记住,它是一把双刃剑——用好了,延迟降到纳秒级;用不好,bug追到你怀疑人生。

下一节,咱们会深入共享内存的底层实现,看看它到底是怎么在内核中工作的。到时候我会带大家手写一个完整的共享内存通信库。


公众号:蓝海资料掘金营,微信deep3321