共享内存¶
是什么 / 解决什么问题¶
共享内存是最快的 IPC 机制——多个进程直接读写同一块物理内存,无需数据拷贝。适合大数据量的进程间通信。
使用模式¶
POSIX 共享内存(推荐)¶
#include <sys/mman.h>
#include <fcntl.h>
// 创建/打开共享内存对象
int fd = shm_open("/myshm", O_RDWR | O_CREAT, 0644);
ftruncate(fd, 4096); // 设置大小
// 映射到进程地址空间
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd); // 映射后 fd 可关闭
// 使用
strcpy(ptr, "hello from process A");
// 解除映射
munmap(ptr, 4096);
// 删除共享内存对象(引用计数为 0 时真正释放)
shm_unlink("/myshm");
System V 共享内存(老 API)¶
#include <sys/ipc.h>
#include <sys/shm.h>
key_t key = ftok("/tmp/keyfile", 'A');
// 创建
int shmid = shmget(key, 4096, IPC_CREAT | 0644);
// 附加到进程
void *ptr = shmat(shmid, NULL, 0);
// 使用
memcpy(ptr, data, data_len);
// 分离
shmdt(ptr);
// 删除
shmctl(shmid, IPC_RMID, NULL);
memfd_create(匿名共享内存)¶
#include <sys/mman.h>
// 创建匿名内存文件(不出现在文件系统中)
int fd = memfd_create("myshm", MFD_CLOEXEC);
ftruncate(fd, size);
// 映射
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 通过 Unix socket 将 fd 传递给其他进程
// 或通过 fork 继承
共享内存 + 同步¶
// 共享内存需要自己处理同步
// 方法1: 进程间 mutex(放在共享内存中)
struct shared_data {
pthread_mutex_t mutex;
int counter;
char data[4096];
};
// 初始化 mutex 为进程间共享
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&shared->mutex, &attr);
// 使用
pthread_mutex_lock(&shared->mutex);
shared->counter++;
pthread_mutex_unlock(&shared->mutex);
// 方法2: futex(直接操作共享内存中的原子变量)
// 方法3: 文件锁 (flock/fcntl)
行为与语义¶
POSIX vs System V¶
| POSIX (shm_open) | System V (shmget) | |
|---|---|---|
| 命名 | 路径名 "/name" | key_t (ftok) |
| 存放 | /dev/shm | 内核管理 |
| 删除 | shm_unlink | shmctl(IPC_RMID) |
| fd 传递 | 支持 (mmap) | 不支持 |
| 权限 | 标准文件权限 | ipc 权限 |
| 推荐 | 是 | 遗留代码 |
内存一致性¶
共享内存中的读写顺序问题:
- 不同 CPU 核心可能看到不同顺序的写入
- 必须使用同步原语或内存屏障
// 正确:atomic + 适当内存序
atomic_store_explicit(&shared->ready, 1, memory_order_release);
// 另一进程:
while (!atomic_load_explicit(&shared->ready, memory_order_acquire))
;
// 此时可以安全读取 shared 的其他数据
mmap MAP_SHARED 的一致性¶
同一文件的 MAP_SHARED 映射保证一致性: - 一个进程的写入立即对其他映射同一区域的进程可见 - 内核保证同一物理页(通过 page cache)
性能考量¶
为什么共享内存最快¶
大页共享内存¶
// 使用大页减少 TLB miss
int fd = shm_open("/hugeshm", O_RDWR | O_CREAT, 0644);
void *ptr = mmap(NULL, 2 * 1024 * 1024, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_HUGETLB, fd, 0);
NUMA 感知¶
// 多 NUMA 节点系统中,共享内存页的物理位置影响性能
// 用 mbind 控制共享内存的 NUMA 策略
#include <numaif.h>
mbind(ptr, size, MPOL_INTERLEAVE, nodemask, maxnode, 0);
常见陷阱与 FAQ¶
1. shm_unlink 的时机¶
// shm_unlink 只删除名字(类似 unlink 文件)
// 已经 mmap 的映射不受影响
// 最后一个 munmap 时才真正释放
// 常见模式:创建后立即 unlink,防止泄漏
int fd = shm_open("/myshm", O_RDWR | O_CREAT, 0644);
ftruncate(fd, size);
void *ptr = mmap(NULL, size, ...);
shm_unlink("/myshm"); // 安全:映射仍有效
2. 进程崩溃后的清理¶
# System V 共享内存不会自动清理
ipcs -m # 查看
ipcrm -m <shmid> # 手动删除
# POSIX 共享内存:检查 /dev/shm
ls /dev/shm/
rm /dev/shm/myshm
3. 共享内存中不能存指针¶
// 错误:不同进程的 mmap 地址不同
shared->next = (struct node *)ptr + offset; // 进程B看到的地址无意义
// 正确:用偏移量代替指针
shared->next_offset = (char *)node - (char *)base;
// 恢复: struct node *n = (struct node *)((char *)base + shared->next_offset);
4. 权限与安全¶
// shm_open 创建的对象默认 rw
// 生产环境注意设置合适权限
shm_open("/myshm", O_RDWR | O_CREAT | O_EXCL, 0600); // 只有 owner 可访问
// O_EXCL: 如果已存在则失败(防止被恶意进程抢先创建)
观测与调试¶
# POSIX 共享内存
ls -la /dev/shm/
# System V 共享内存
ipcs -m
# 查看进程的共享映射
cat /proc/<pid>/maps | grep "/dev/shm"
pmap -x <pid> | grep shm
# 查看共享内存使用量
cat /proc/<pid>/status | grep -E "RssShmem|VmSize"
延伸阅读¶
man 7 shm_overview— POSIX 共享内存概述man 3 shm_open/man 2 mmapman 2 memfd_create— 匿名内存文件man 2 shmget— System V API