3. POSIX共享内存:shm_open与mmap结合,创建与销毁共享内存段
共享内存,说白了就是让两个或多个进程直接读写同一块物理内存。没有拷贝,没有序列化,数据写进去就能读出来。我刚开始接触这个的时候,觉得它简直像魔法一样——两个完全独立的进程,怎么就能看到同一块数据呢?
嗯,其实原理没那么玄乎。操作系统提供了一套机制,让不同进程的虚拟地址映射到同一个物理页面。POSIX共享内存就是其中一种标准实现,核心就两个函数:shm_open 和 mmap。
3.1 核心概念:共享内存的“身份证”
POSIX共享内存用“名字”来标识一段共享内存段。这个名字以斜杠开头,比如 /my_shared_mem。你可以把它想象成共享内存的“身份证号”,进程之间通过这个名字来找到同一块内存。
整个流程分三步走:
- 创建/打开:用
shm_open创建或打开一个共享内存对象 - 设置大小:用
ftruncate设定共享内存的大小 - 映射:用
mmap把共享内存映射到进程的地址空间
用完记得销毁,不然内存会泄漏。我在项目中就吃过这个亏,后面细说。
关键点:shm_open 返回的是文件描述符,不是内存地址。真正的内存映射靠 mmap 完成。
3.2 创建共享内存段
先看创建端的代码。我习惯把创建逻辑封装成一个函数,这样调用方不用关心底层细节。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define SHM_NAME "/my_shared_mem"
#define SHM_SIZE 4096
int create_shared_memory() {
// 创建共享内存对象
int shm_fd = shm_open(SHM_NAME,
O_CREAT | O_RDWR,
0666);
if (shm_fd == -1) {
perror("shm_open failed");
return -1;
}
// 设置共享内存大小
if (ftruncate(shm_fd, SHM_SIZE) == -1) {
perror("ftruncate failed");
close(shm_fd);
shm_unlink(SHM_NAME);
return -1;
}
// 映射到进程地址空间
void *ptr = mmap(NULL, SHM_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED,
shm_fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap failed");
close(shm_fd);
shm_unlink(SHM_NAME);
return -1;
}
// 文件描述符可以关闭了,映射还在
close(shm_fd);
printf("共享内存创建成功,地址:%p\n", ptr);
return 0;
}
这里有几个细节要注意:
O_CREAT | O_RDWR:如果不存在就创建,以读写方式打开0666:权限位,和文件权限一样MAP_SHARED:所有进程对内存的修改互相可见ftruncate必须在mmap之前调用
我的习惯:共享内存大小通常设成页面大小的整数倍。Linux 默认页面大小是 4096 字节,所以我经常用 4096、8192 这样的值。不是必须的,但能避免一些对齐问题。
3.3 连接已有的共享内存
另一个进程要访问同一块共享内存,不需要重新创建,直接打开就行。
int connect_shared_memory() {
int shm_fd = shm_open(SHM_NAME, O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open failed");
return -1;
}
void *ptr = mmap(NULL, SHM_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED,
shm_fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap failed");
close(shm_fd);
return -1;
}
close(shm_fd);
printf("连接到共享内存,地址:%p\n", ptr);
return 0;
}
看到区别了吗?连接端没有 O_CREAT 标志,也没有 ftruncate 调用。因为共享内存已经存在了,大小也定好了。
3.4 销毁共享内存段
销毁分两步:先解除映射,再删除共享内存对象。
int destroy_shared_memory(void *ptr) {
// 解除映射
if (munmap(ptr, SHM_SIZE) == -1) {
perror("munmap failed");
return -1;
}
// 删除共享内存对象
if (shm_unlink(SHM_NAME) == -1) {
perror("shm_unlink failed");
return -1;
}
printf("共享内存已销毁\n");
return 0;
}
注意:shm_unlink 只是标记删除,真正的内存释放要等到所有进程都 munmap 之后。我曾经在调试时先调了 shm_unlink,结果另一个进程还在用,直接崩溃了。嗯,血的教训。
3.5 完整示例:生产者-消费者
来个实际点的例子。生产者写数据,消费者读数据。
// producer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
typedef struct {
int ready;
char data[256];
} shared_data;
int main() {
int shm_fd = shm_open("/shm_example",
O_CREAT | O_RDWR, 0666);
ftruncate(shm_fd, sizeof(shared_data));
shared_data *shm = mmap(NULL, sizeof(shared_data),
PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
close(shm_fd);
// 写入数据
shm->ready = 0;
strcpy(shm->data, "Hello from producer!");
shm->ready = 1;
printf("生产者:数据已写入\n");
sleep(5); // 等消费者读完
munmap(shm, sizeof(shared_data));
shm_unlink("/shm_example");
return 0;
}
// consumer.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
typedef struct {
int ready;
char data[256];
} shared_data;
int main() {
int shm_fd = shm_open("/shm_example", O_RDWR, 0666);
if (shm_fd == -1) {
perror("共享内存不存在");
return 1;
}
shared_data *shm = mmap(NULL, sizeof(shared_data),
PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
close(shm_fd);
// 等待数据就绪
while (!shm->ready) {
usleep(100);
}
printf("消费者:收到数据 - %s\n", shm->data);
munmap(shm, sizeof(shared_data));
return 0;
}
3.6 核心流程图
下面这张图展示了整个生命周期,从创建到销毁的完整流程。
3.7 常见问题与避坑指南
| 问题 | 原因 | 解决方法 |
|---|---|---|
| shm_open 返回 -1 | 名字格式不对,或权限不足 | 确保名字以斜杠开头,检查权限位 |
| mmap 返回 MAP_FAILED | 文件描述符无效,或大小设置不对 | 先调 ftruncate,再调 mmap |
| 共享内存内容不对 | 忘记用 MAP_SHARED 标志 | 检查 mmap 的 flags 参数 |
| 程序退出后共享内存还在 | 忘记调 shm_unlink | 在合适时机调用 shm_unlink |
我曾经踩过的坑:有一次在生产环境上,共享内存段没销毁,导致系统 /dev/shm 目录下积累了上百个废弃的共享内存对象。排查了半天才发现是异常退出时没调 shm_unlink。后来我养成了一个习惯:在程序启动时先调 shm_unlink 清理旧的共享内存,再创建新的。这样即使上次异常退出,也不会留下垃圾。
3.8 性能考量
共享内存为什么快?说白了就是省掉了数据拷贝。传统 IPC 方式(管道、消息队列)需要在内核空间和用户空间之间来回拷贝数据,而共享内存直接读写物理内存,零拷贝。
但要注意:
- 创建开销:
shm_open+mmap第一次调用时开销较大,因为要分配物理页面 - 映射开销:
mmap涉及页表修改,但只做一次 - 读写性能:一旦映射完成,读写就是普通内存操作,纳秒级延迟
我个人建议:如果进程间需要频繁交换大量数据(比如每秒几千次、每次几 KB 以上),共享内存是首选。如果只是偶尔发个消息,用管道或消息队列更省事。
小技巧:可以用 ls -l /dev/shm 查看系统上所有的 POSIX 共享内存对象。调试时特别有用,能确认共享内存有没有被正确清理。
嗯,关于 POSIX 共享内存的创建与销毁,核心内容就这些了。记住三个关键函数:shm_open 创建/打开、ftruncate 设大小、mmap 做映射。销毁时 munmap 和 shm_unlink 配合使用。代码写多了你会发现,这套模式其实很固定,就像搭积木一样。