共享内存初探:什么是共享内存?为什么需要它?低延迟场景下的核心优势
大家好,我是你们这门课的老朋友。今天咱们来聊聊共享内存。
说实话,我第一次接触共享内存,是在做一个高频交易系统的原型。当时数据从A进程传到B进程,走的是Socket,延迟动不动就几十微秒。老板拍桌子说:“再慢就卷铺盖走人!” 我那时候就在想,有没有一种办法,让两个进程像两个线程一样,直接读写同一块内存?
嗯,答案就是共享内存。
什么是共享内存?
说白了,共享内存就是一块物理内存,被多个进程映射到了各自的虚拟地址空间里。
你想想看,正常情况下,每个进程都有自己的“小院子”,谁也看不见谁。共享内存呢,就是在这些院子之间开了一扇门。A进程往里面写数据,B进程立马就能读到。不需要复制,不需要经过内核。
核心概念:共享内存是IPC(进程间通信)中速度最快的一种方式。它绕过了内核的中间层,让数据在用户态直接交换。
我习惯用一张图来理解这个过程:
为什么需要它?
你可能会问:“我直接用管道、消息队列、Socket不也行吗?”
行,当然行。但你要看场景。
我举个例子。假设你要在两个进程之间传递一个100KB的日志结构。用Socket的话,数据得从用户态复制到内核态,再复制到另一个进程的用户态。这一来一回,两次拷贝。再加上上下文切换,延迟轻松上到几十微秒。
而共享内存呢?
- 零拷贝:数据只存在于物理内存中,两个进程直接读写。没有内核介入。
- 无上下文切换:不需要系统调用(除了初始化的那一次)。
- 极低延迟:纳秒级别的读写延迟,而不是微秒。
我的经验:在低延迟场景下,共享内存的延迟通常比Socket低2-3个数量级。我做过一个对比测试,同样是1KB数据交换,共享内存耗时约200纳秒,而Unix Domain Socket耗时约2微秒。差了10倍。
低延迟场景下的核心优势
咱们来细数一下,共享内存到底牛在哪。
| 特性 | 共享内存 | 传统IPC(Socket/管道) |
|---|---|---|
| 数据拷贝次数 | 0次(直接读写) | 2次(用户态→内核态→用户态) |
| 上下文切换 | 0次(初始化后) | 至少2次(每次读写) |
| 延迟 | 纳秒级 | 微秒级 |
| 吞吐量 | 极高(受限于内存带宽) | 中等(受限于内核处理能力) |
| 同步机制 | 需要自行实现(信号量/自旋锁) | 内核自动处理 |
看到没?共享内存最大的优势就是“快”。
但这里有个坑。我曾经在一个项目中,直接用共享内存做生产者-消费者模型,结果发现数据读出来是乱的。为什么?因为两个进程同时写同一块内存,没有做同步。
注意:共享内存本身不提供任何同步机制。你需要自己用信号量、互斥锁或原子操作来保证数据一致性。否则,轻则数据错乱,重则程序崩溃。
什么时候该用共享内存?
我个人习惯,在以下场景优先考虑共享内存:
- 高频交易:行情数据、订单簿更新,延迟就是金钱。
- 实时音视频处理:帧数据量大,延迟要求高。
- 游戏引擎:主逻辑和渲染进程之间交换状态。
- 嵌入式系统:资源受限,需要高效通信。
反过来,如果你的应用对延迟不敏感,或者数据量很小,用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创建的文件描述符要记得关闭。mmap的MAP_SHARED标志不能写错,否则两个进程看不到同一块内存。- 共享内存的大小最好是页对齐(通常是4096字节的倍数)。
避坑指南:我曾经在 ftruncate 之后忘记检查返回值,结果共享内存大小设置失败,导致后续写入越界。嗯,从那以后我每次都会加一句 if (ftruncate(...) == -1) { perror("ftruncate"); exit(1); }。
小结
共享内存,说白了就是让两个进程“住进同一个房间”。它快,因为它不经过内核。它难,因为你要自己管好“房间里的秩序”。
在低延迟场景下,共享内存几乎是唯一的选择。但记住,它是一把双刃剑——用好了,延迟降到纳秒级;用不好,bug追到你怀疑人生。
下一节,咱们会深入共享内存的底层实现,看看它到底是怎么在内核中工作的。到时候我会带大家手写一个完整的共享内存通信库。
公众号:蓝海资料掘金营,微信deep3321