K08 —— 进程间共享内存:shm 和 mmap(MAP_SHARED)
金句:最快的进程间通信,是根本不需要通信——直接共享同一块内存。
进程间共享内存的三种方式
Linux 支持三种进程间共享内存的机制:
1. System V Shared Memory(shmget/shmat)
- 历史悠久,POSIX 之前的 API
- 通过内核对象(shmid_kernel)管理
2. POSIX Shared Memory(shm_open/shm_unlink)
- 基于 tmpfs 的文件映射
- 通过文件描述符操作
3. mmap(MAP_SHARED)
- 匿名共享(父子进程)
- 文件映射共享(文件作为后端)
三种方式的共同点:
- 都是把物理页映射到多个进程的虚拟地址空间
- 写入一块,所有进程立即看到(无复制)性能对比:
System V shm POSIX shm(/dev/shm) mmap(MAP_SHARED)
创建开销 高(内核对象) 中(tmpfs 文件) 低(匿名)或中(文件)
访问延迟 同上(都走页表) 同上 同上
生命周期 显式删除 最后一个引用关闭 最后一个映射关闭
调试便捷性 ipcs -m ls /dev/shm /proc/PID/maps
POSIX 标准 否 是 是
跨节点(NUMA) 支持 支持 支持System V Shared Memory 的内核路径
shm 的内核数据结构路径:
用户态:shmget(key, size, IPC_CREAT|0666)
↓
内核:shmid_kernel 结构体(struct shmid_kernel)
↓
关联一个匿名 tmpfs 文件(没有真实文件名)
↓
struct file → struct address_space
↓
物理页(来自 buddy system,纳入 page cache)
用户态:shmat(shmid, NULL, 0)
↓
内核:为当前进程创建一个 VMA(虚拟内存区域)
↓
VMA.vm_ops → shm_vm_ops(文件操作)
↓
页表建立映射:虚拟地址 → struct page → 物理页
关键点:
- shm 的物理页是 page cache 的一部分
- 可以被 tmpfs 的回收机制回收(如果内存紧张)
- shmctl(IPC_RMID) 只是标记删除,真正的页释放在最后一个进程分离后
查看共享内存(ipcs):
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x1234 65536 user 666 1048576 2 dest
nattch = 附加本段的进程数
status = dest 表示"标记删除,等待最后一个进程分离"System V Shared Memory
shmget / shmat 的使用:
key_t key = ftok("/some/path", 1);
int shmid = shmget(key, 1024*1024, IPC_CREAT | 0666);
// 附加到进程地址空间
void *addr = shmat(shmid, NULL, 0);
// 使用共享内存
memcpy(addr, "hello", 6);
// 分离(不再使用,但共享区域还在)
shmdt(addr);
// 删除共享区域(标记删除,最后一个进程分离后真正释放)
shmctl(shmid, IPC_RMID, NULL);
内部实现:
- shmget 创建/获取内核对象(shmid_kernel)
- shmat 通过 shm_npages 个 VMA 映射到进程空间
- 物理页来自 buddy system,通过 page cache / tmpfs 文件管理shm 的生命周期:
- shmget 创建,shmctl(IPC_RMID) 删除(标记删除)
- 即使所有进程都 shmdt,共享区域仍然存在
- 除非显式删除,且所有进程都已分离
系统限制(/proc/sys/kernel/shm*):
- shmmax:最大段大小(通常 8GB)
- shmmni:最大段数量(通常 4096)
- shmall:最大总内存(pages)
ipcs -m 查看共享内存:
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x1234 65536 user 666 1048576 2 destPOSIX Shared Memory(shm_open)
shm_open 的使用:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
// 创建共享内存对象(基于 /dev/shm tmpfs)
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 1024*1024); // 设置大小
// 映射到进程地址空间
void *addr = mmap(NULL, 1024*1024, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
// 使用
memcpy(addr, "hello", 6);
// 清理
munmap(addr, 1024*1024);
close(fd);
shm_unlink("/my_shm"); // 删除
特点:
- 路径名在 /dev/shm(tmpfs)
- 可以用 ls /dev/shm 查看(调试友好)
- 更简单,符合 POSIX 标准/dev/shm(tmpfs):
- tmpfs 是一种内存文件系统
- 存储在 Page Cache 中(file-backed)
- 读取写入实际是内存操作(无磁盘 I/O)
- 物理页来自 Page Cache,可以被回收(内存紧张时)
- 缺点:大小受 tmpfs 配额限制,默认是物理内存的一半
- 如果 /dev/shm 写满,shm_open 会失败(返回 ENOMEM)
- 调整大小:mount -o size=10G -o remount tmpfs /dev/shm
优点:
- 不用 key,路径名更直观
- 基于文件,容易调试(可以用 ls 查看)
- 符合 POSIX 标准(新代码建议用这个)mmap(MAP_SHARED) 的两种用法
匿名共享(父子进程):
void *addr = mmap(NULL, 4096,
PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS,
-1, 0);
fork();
memcpy(addr, "hello", 6); // 父子都看到(COW 关闭)
wait();
munmap(addr, 4096);
原理:
- MAP_ANONYMOUS:没有文件后端
- MAP_SHARED:fork 后共享(COW 关闭)
- 物理页在所有进程间共享
---
文件共享:
int fd = open("/tmp/file", O_RDWR);
void *addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
fork();
memcpy(addr, "hello", 6); // 写回磁盘文件
wait();
munmap(addr, 4096);
close(fd);
原理:
- 文件内容作为共享内存的后端
- 写入会立即反映到文件中(MAP_SHARED)
- 磁盘文件是持久化的
- 需要 msync(MS_SYNC) 才保证数据落盘(可选)fork 后三种方式的行为差异
方式 fork 后行为 COW ?
System V shm 自动共享(所有进程见同一物理页) 否(天然共享)
POSIX shm 打开的 fd fork 后继承,映射后共享 否(天然共享)
mmap(MAP_SHARED, fd) fd 继承,映射后共享 否(天然共享)
mmap(MAP_ANONYMOUS) MAP_SHARED:父子共享 否
mmap(MAP_ANONYMOUS) MAP_PRIVATE:COW(各自独立副本) 是(默认 COW)
关键区别:
- 有文件/段后端的三种(shm + mmap shared):fork 后天然共享
- MAP_ANONYMOUS + MAP_PRIVATE(默认):fork 后 COW,各自独立
这意味着:
- System V shm:fork 后子进程直接看到父进程写入的内容(无 COW)
- mmap 匿名 MAP_PRIVATE:fork 后子进程有独立副本,除非触发 COW 写入msync:何时需要强制刷盘
MAP_SHARED 写入文件时的行为:
- 写入共享内存 → Page Cache 脏页 → 由内核 writeback 机制刷盘
- 不是每次写入都立刻落盘(会有延迟)
msync 的作用:
#include <sys/mman.h>
msync(addr, length, MS_SYNC); // 同步等待落盘
msync(addr, length, MS_ASYNC); // 异步刷盘,立即返回
MS_INVALIDATE:同时通知内核重新从磁盘读取(很少用)
典型场景:
- 数据库日志:写入后必须立刻落盘 → 用 msync(MS_SYNC)
- 共享内存缓存:不需要立即落盘 → 用默认的延迟写回即可
注意:
- 对 MAP_ANONYMOUS 共享内存调用 msync 是无效的(无文件后端)共享内存的同步问题
共享内存本身不提供同步机制!
多个进程同时读写同一块内存会导致数据竞争。
解决方案:
1. 原子操作(atomic)
- POSIX atomics(<stdatomic.h>)
- 只适合简单计数器
2. 互斥锁(mutex)
- 进程间可以用 pthread_mutex(需要 PTHREAD_PROCESS_SHARED 属性)
- 或者 System V semaphore
3. 信号量(semaphore)
- System V semaphore(semget/semop)
- POSIX semaphore(sem_open)
4. 文件锁(F_SETLKW)
- flock():简单文件锁
- fcntl():更强
示例(System V semaphore):
key_t semkey = ftok("/tmp", 1);
int semid = semget(semkey, 1, IPC_CREAT | 0666);
semctl(semid, 0, SETVAL, 1); // 初始化为 1
// P 操作(原子减 1,如果值为 0 则阻塞)
struct sembuf p = {0, -1, 0};
semop(semid, &p, 1);
// V 操作(原子加 1)
struct sembuf v = {0, 1, 0};
semop(semid, &v, 1);
// 配合共享内存使用:
semop(semid, &p, 1); // 进入临界区
// 操作共享内存
semop(semid, &v, 1); // 离开临界区/dev/shm 大小限制——生产环境容易踩的坑
/dev/shm 的默认大小:
- 通常是物理内存的 50%(可通过 df 查看)
- $ df -h /dev/shm
tmpfs 7.7G 0 7.7G 0% /dev/shm
- 7.7G = 物理内存的一半
如果共享内存写满(超过 /dev/shm 配额):
- shm_open 失败,返回 errno=ENOMEM
- 程序可能崩溃或进入异常状态
调整 /dev/shm 大小(需要 root):
# 临时调整
mount -o size=10G -o remount /dev/shm
# 永久调整(/etc/fstab)
tmpfs /dev/shm tmpfs size=10G 0 0
生产建议:
- 预留足够 /dev/shm 空间(或明确知道上限)
- 监控 /dev/shm 使用率
- 如果需要大共享内存,考虑 System V shm(shmmax 可以很大)总结
- shmget/shmat:System V 共享内存,key-based,内核对象,有 ipcs 可见
- shm_open/shm_unlink:POSIX 共享内存,路径名,基于 /dev/shm tmpfs
- mmap(MAP_SHARED|MAP_ANON):匿名共享,父子进程,关闭 COW
- mmap(MAP_SHARED, fd):文件映射共享,文件作为后端,写会持久化
- fork 行为:有文件后端的共享内存(shm + mmap shared)fork 后天然共享;匿名 MAP_PRIVATE 才是 COW
- msync:MAP_SHARED 写文件后默认延迟写回,需要强制落盘时用 msync
- 同步:共享内存不提供同步,需要 mutex/semaphore/原子操作等
- /dev/shm 配额:默认物理内存 50%,用满会 ENOMEM,需要提前规划
下篇预告(K09):什么是内存泄漏?为什么 malloc 不 free 会导致内存无法回收?泄漏的检测方法和防止措施。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论