共享内存基础:进程间最快的通信方式

大家好,我是你们的嵌入式系统讲师。今天我们来聊聊共享内存——这个在Linux进程间通信(IPC)工具箱里,速度最快、效率最高的家伙。

说实话,我刚开始做嵌入式开发那会儿,对共享内存的理解也就停留在「几个进程能同时访问一块内存」这个层面。直到有一次做视频采集系统,两个进程之间要传大量图像数据,用管道?太慢。用消息队列?封装开销太大。最后换了共享内存,性能直接翻倍。嗯,从那以后我就知道,这玩意儿是真香。

共享内存的概念与原理

共享内存,说白了就是让两个或多个进程,共同访问同一块物理内存区域。

你想想看,正常情况下每个进程都有自己的虚拟地址空间,A进程的地址0x1000和B进程的0x1000,映射到物理内存上完全是两个不同的地方。但共享内存不一样——它让不同进程的虚拟地址,映射到同一块物理内存上。

这样一来,A进程往这块内存写数据,B进程马上就能读到。不需要拷贝,不需要经过内核中转,直接读写。这就是它快的原因。

核心要点:共享内存是IPC中唯一不经过内核中转的数据交换方式。管道、消息队列、Socket都要在内核空间和用户空间之间来回拷贝数据,共享内存不用。

共享内存与物理内存的关系

这里有个容易混淆的点,我当年也踩过坑。

共享内存本身并不占用物理内存——它只是建立了一个映射关系。真正占用物理内存的,是进程往共享内存里写数据的那一刻。

打个比方:共享内存就像一张空桌子。你说「这张桌子归我们几个用了」,这叫创建共享内存。但桌子本身是空的,不占地方。只有当你往桌上放东西,才真正占用了空间。

在Linux内核里,共享内存是通过shmid_kernel结构体来管理的。每个共享内存段都有一个唯一的标识符——shmid。内核维护着一张全局的共享内存表,记录着哪些段被创建了、多大、谁在用。

我的经验:ipcs -m命令可以查看当前系统里所有的共享内存段。调试的时候特别好用。我曾经靠这个命令抓到一个内存泄漏的bug——某个进程创建了共享内存但忘了删除,系统里堆了几百个废弃的共享内存段。

共享内存的创建:shmget

创建共享内存,用的是shmget函数。它的原型长这样:

#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

三个参数:

  • key:相当于共享内存的名字。不同进程通过这个key找到同一块共享内存。可以用IPC_PRIVATE(只能用于父子进程),也可以用ftok()生成一个唯一的key。
  • size:共享内存的大小,单位是字节。注意,内核会按页对齐(通常是4KB),所以你指定的大小会被向上取整到页的整数倍。
  • shmflg:权限标志,类似文件权限。比如IPC_CREAT | 0666表示如果不存在就创建,权限是读写。

返回值是shmid,后面所有的操作都靠这个id来定位共享内存段。

注意:shmget返回的是shmid,不是指针!很多新手会搞混。shmid只是一个整数标识符,你得用shmat把它挂接到进程的地址空间,才能拿到真正的指针。

共享内存的挂接与脱接:shmat / shmdt

创建好共享内存之后,进程要使用它,必须先「挂接」到自己的地址空间里。

void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

shmat的参数:

  • shmid:shmget返回的标识符。
  • shmaddr:建议的挂接地址。传NULL让内核自己选,我建议你永远传NULL——省心。
  • shmflg:通常传0。如果传SHM_RDONLY,就只能读不能写。

返回值是一个指针,指向共享内存的起始地址。拿到这个指针,你就可以像操作普通内存一样读写它了。

shmdt就简单了——把共享内存从当前进程的地址空间里卸下来。注意,这只是断开映射,不会删除共享内存本身。

我个人的习惯:用完共享内存后,立刻调用shmdt。别等到进程退出时让系统自动清理。在长时间运行的服务里,不及时脱接会导致虚拟地址空间碎片化,时间长了可能出问题。

共享内存的控制:shmctl

shmctl是共享内存的「控制面板」。它的功能很丰富:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

常用的cmd有这几个:

命令 作用
IPC_STAT 获取共享内存的状态信息,存到buf里
IPC_SET 修改共享内存的权限、所有者等属性
IPC_RMID 标记共享内存为删除状态

这里有个细节要注意:IPC_RMID并不是立即删除共享内存。它只是打上一个「待删除」的标记。只有当所有挂接的进程都调用shmdt脱接之后,内核才会真正释放这块内存。

我曾经踩过的坑:有一次我调用了shmctl(..., IPC_RMID, NULL),然后以为共享内存已经被删了。结果另一个进程还在读写它,数据全乱了。后来才明白,IPC_RMID只是标记删除,不是立即释放。正确的做法是:先通知所有进程脱接,再调用IPC_RMID。

共享内存的读写操作

读写共享内存,其实就是读写普通内存。拿到shmat返回的指针后,用memcpysprintf、甚至直接指针赋值都行。

// 写数据
char *data = (char *)shmat(shmid, NULL, 0);
strcpy(data, "Hello from process A");

// 读数据
char buffer[256];
strcpy(buffer, data);
printf("Read: %s\n", buffer);

就这么简单。没有系统调用,没有数据拷贝,直接读写物理内存。

但简单归简单,有个大问题——同步。

共享内存的同步问题

这是共享内存最头疼的地方,也是面试最爱问的。

你想啊,两个进程同时往同一块内存写数据,会发生什么?数据竞争,结果不可预期。A进程写了一半,B进程读到了半成品数据,那就乱套了。

共享内存本身不提供任何同步机制。它就是个「裸」内存,你得自己想办法保证数据的一致性。

常用的同步手段有:

  • 信号量:最经典的做法。写之前P操作,写完之后V操作。保证同一时刻只有一个进程在写。
  • 互斥锁:如果共享内存放在多线程环境里,可以用pthread的互斥锁。但要注意,互斥锁默认只在同一进程的线程间有效,跨进程要用PTHREAD_PROCESS_SHARED属性。
  • 文件锁:简单粗暴,但性能差一些。

我的建议:对于简单的生产者-消费者场景,用信号量就够了。对于复杂的数据结构(比如共享队列、共享缓冲区),建议用读写锁——读操作可以并发,写操作独占。我在做多路视频采集时就是这么干的,读进程有3个,写进程只有1个,读写锁比互斥锁性能好很多。

知识体系总览

下面这张图,把共享内存的核心知识点串起来了:

共享内存知识体系 共享内存 shmget 创建 shmat / shmdt shmctl 控制 读写操作 同步问题 信号量 互斥锁 文件锁 映射到同一块物理内存 核心:不同进程的虚拟地址 → 同一物理地址,无需内核中转

从图上可以看得很清楚:共享内存的核心就是「映射到同一块物理内存」。创建、挂接、控制、读写、同步,这五个环节缺一不可。其中同步问题是最容易出错的,也是实际项目中需要花最多心思去设计的。

好了,共享内存的基础知识就讲到这里。记住一句话:共享内存是IPC的速度之王,但同步问题是你绕不过去的坎。下一节我们会深入实战,用代码把今天讲的东西全部跑一遍。


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