跳转至

共享内存

是什么 / 解决什么问题

共享内存是最快的 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");
# 共享内存对象存在于 /dev/shm
ls -la /dev/shm/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)

性能考量

为什么共享内存最快

Pipe/Socket:   进程A write → 内核缓冲区 → 进程B read  (2次拷贝)
共享内存:       进程A 写入 → 进程B 直接读取               (0次拷贝)

大页共享内存

// 使用大页减少 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

// 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 mmap
  • man 2 memfd_create — 匿名内存文件
  • man 2 shmget — System V API