14、POSIX共享内存:从理论到实战

大家好,我是你们的老朋友。今天我们来聊聊POSIX共享内存。说实话,我在嵌入式领域摸爬滚打这么多年,共享内存这玩意儿几乎天天见。它就像进程间通信的"高速公路",数据直接共享,不用绕路。嗯,咱们直接进入正题。

POSIX共享内存的概念

POSIX共享内存,说白了就是让两个或多个进程共享一块物理内存。每个进程通过自己的虚拟地址空间映射到同一块物理内存上。你想想看,这比管道、消息队列那些方式快多了——数据不用拷贝来拷贝去。

我记得刚入行那会儿,有个项目需要实时采集传感器数据,然后交给另一个进程做分析。用管道传数据,延迟总是超标。后来换成共享内存,问题一下子就解决了。这就是共享内存的魅力——零拷贝,高性能。

核心要点:POSIX共享内存基于文件描述符操作,使用shm_open()创建或打开一个共享内存对象,然后通过mmap()映射到进程地址空间。

POSIX共享内存的创建:shm_open

shm_open()是POSIX共享内存的入口函数。它的原型长这样:

#include <sys/mman.h>
#include <sys/stat.h>        /* For mode constants */
#include <fcntl.h>           /* For O_* constants */

int shm_open(const char *name, int oflag, mode_t mode);

参数解释一下:

  • name:共享内存对象的名字,必须以斜杠开头,比如/my_shm
  • oflag:标志位,常用O_CREAT | O_RDWR,表示创建并读写
  • mode:权限位,比如0666

返回的是文件描述符。如果失败,返回-1。

个人经验:我建议你创建共享内存时,名字尽量有规律,比如/project_module_shm。我曾经在一个项目里看到有人用/tmp开头的名字,结果跟临时文件冲突了,排查了半天。

共享内存的映射:mmap

有了文件描述符,下一步就是映射。用mmap()把共享内存对象映射到进程的虚拟地址空间。

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);

关键参数:

  • addr:建议映射地址,传NULL让内核选
  • length:映射大小,单位字节
  • prot:保护标志,PROT_READ | PROT_WRITE
  • flags:必须包含MAP_SHARED,这样多个进程才能共享
  • fdshm_open()返回的文件描述符

映射成功后,返回一个指针,直接操作这个指针就是操作共享内存。

注意:映射之前,一定要用ftruncate()设置共享内存对象的大小。我见过有人忘了这步,结果mmap返回成功,但一读写就段错误。嗯,血的教训。

// 设置共享内存大小为4096字节
ftruncate(shm_fd, 4096);

// 映射到进程地址空间
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (ptr == MAP_FAILED) {
    perror("mmap");
    exit(1);
}

共享内存的同步

共享内存本身不提供同步机制。多个进程同时读写同一块内存,数据竞争是必然的。怎么办?

我常用的方案有两种:

  1. POSIX信号量:轻量级,适合简单场景
  2. 互斥锁(放在共享内存中):需要设置PTHREAD_PROCESS_SHARED属性

举个例子,用POSIX命名信号量:

#include <semaphore.h>

sem_t *sem = sem_open("/my_sem", O_CREAT, 0666, 1);

// 写数据前加锁
sem_wait(sem);
// 写入共享内存...
// 解锁
sem_post(sem);

避坑指南:我曾经在一个多进程项目中,忘了给信号量加O_CREAT标志,结果第二个进程打开时直接失败。还有一次,信号量名字跟共享内存名字搞混了,调试了一下午。名字规范真的很重要。

共享内存的销毁:shm_unlink

用完共享内存,记得清理。调用shm_unlink()删除共享内存对象的名字,然后所有映射了该对象的进程,在munmap()后,资源才会真正释放。

// 取消映射
munmap(ptr, 4096);

// 关闭文件描述符
close(shm_fd);

// 删除共享内存对象
shm_unlink("/my_shm");

重要:如果进程异常退出,没有调用shm_unlink(),共享内存对象会一直存在。重启后可能造成资源泄漏。我建议在程序启动时先shm_unlink()一次,确保清理干净。

POSIX共享内存与System V共享内存的对比

很多老项目还在用System V共享内存。两者有什么区别?我整理了一张表:

对比项 POSIX共享内存 System V共享内存
接口风格 基于文件描述符,类似文件操作 基于标识符(key),类似IPC操作
创建/打开 shm_open() shmget()
映射 mmap() shmat()
销毁 shm_unlink() shmctl(IPC_RMID)
可移植性 POSIX标准,现代系统都支持 传统Unix系统支持
同步机制 可配合POSIX信号量 可配合System V信号量
生命周期 引用计数,最后一个进程释放后销毁 显式删除,否则一直存在

我个人更推荐POSIX共享内存。原因很简单:接口更清晰,跟文件操作统一,而且配合mmap()用起来很顺手。不过,如果你在维护老系统,System V的代码也得能看懂。

POSIX共享内存的应用

实际项目中,POSIX共享内存最常见的场景:

  • 大数据量传输:比如视频帧、音频数据,用管道传太慢
  • 实时控制:一个进程采集数据,另一个进程做控制决策
  • 共享配置:多个进程读取同一份配置,更新后立即可见

我记得有个项目是做工业相机图像处理的。相机采集一帧图像大概10MB,如果用管道传,每秒只能传几帧。后来改成共享内存,直接映射到处理进程的地址空间,每秒能处理30帧。效果立竿见影。

小技巧:共享内存的大小最好按页对齐(通常是4096字节的倍数)。这样内存管理更高效,也避免了一些边界问题。

知识体系总览

下面这张图总结了POSIX共享内存的核心流程:

POSIX共享内存核心流程 1. 创建共享内存 shm_open("/name", O_CREAT|O_RDWR, 0666) 2. 设置大小 ftruncate(fd, size) 3. 映射到地址空间 mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0) 同步机制(可选但推荐) POSIX信号量 / 进程共享互斥锁 4. 读写共享内存 直接操作指针:memcpy / 结构体赋值 5. 销毁共享内存 munmap() → close() → shm_unlink("/name") 进程A 写数据 进程B 读数据

这张图把整个流程串起来了。从创建到销毁,每一步都很清晰。你写代码的时候,按这个流程走,基本不会出错。


好了,POSIX共享内存的内容就讲到这里。记住:创建、映射、同步、读写、销毁,五步走。每一步都有坑,但踩过一次就记住了。嗯,希望你在项目中用起来顺手。

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