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,这样多个进程才能共享 - fd:
shm_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);
}
共享内存的同步
共享内存本身不提供同步机制。多个进程同时读写同一块内存,数据竞争是必然的。怎么办?
我常用的方案有两种:
- POSIX信号量:轻量级,适合简单场景
- 互斥锁(放在共享内存中):需要设置
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共享内存的内容就讲到这里。记住:创建、映射、同步、读写、销毁,五步走。每一步都有坑,但踩过一次就记住了。嗯,希望你在项目中用起来顺手。