共享内存是性能最极限的 IPC:生产者写、消费者读,同一页物理内存,没有 copy、没有 syscall(除同步原语)。但 Linux 上共享内存 API 有三代,写新代码该用哪个?答案是 memfd_create + mmap,其他都是历史兼容。
一、先看图
flowchart TB
subgraph SysV[SysV 共享内存 · 历史]
A1[shmget key]-->A2[shmid]
A2-->A3[shmat → VA]
A3-->A4[shmdt]
end
subgraph POSIX[POSIX 共享内存]
B1[shm_open /name]-->B2[ftruncate]
B2-->B3[mmap → VA]
B3-->B4[munmap + shm_unlink]
end
subgraph Modern[现代范式]
C1[memfd_create]-->C2[fcntl F_ADD_SEALS]
C2-->C3[SCM_RIGHTS 传 fd]
C3-->C4[mmap 对端]
end
classDef old fill:#63636e22,stroke:#636e7b,color:#adbac7;
classDef mid fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef new fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class A1,A2,A3,A4 old
class B1,B2,B3,B4 mid
class C1,C2,C3,C4 new
三者都落在同一物理内存(实际上 POSIX shm 和 memfd 都是 tmpfs 文件,SysV 是 shm 特殊 inode),区别在命名、生命周期、权限。
二、SysV shmget:遗产 API
int id = shmget(IPC_PRIVATE, size, IPC_CREAT | 0600);
void *p = shmat(id, NULL, 0);
// ...
shmdt(p);
shmctl(id, IPC_RMID, NULL);问题:
- key 命名空间:全局 int
冲突;
ftok()从路径生成 key 但不保证唯一 - 生命周期:不 IPC_RMID
就留在内核——服务崩了
ipcs看到永驻段 - 容量默认小:
kernel.shmmax、kernel.shmall(现代内核已放宽到几 GB) - kernel.shm_rmid_forced:Linux 特有 sysctl,最后一个 attach 退出就删;不是默认开
- API 冗长:shmget → shmat → 同步 → shmdt → shmctl
SysV shm 唯一仍然常见的场景:PostgreSQL 老版本、一些商业数据库兼容层。新代码别写。
三、POSIX shm_open
int fd = shm_open("/foo", O_CREAT | O_RDWR, 0600);
ftruncate(fd, size);
void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// ...
munmap(p, size);
close(fd);
shm_unlink("/foo");底层就是 /dev/shm(tmpfs)上的文件。POSIX
规定 /name 风格,Linux 把 /name
映射到 /dev/shm/name。
优点:
- fd 语义——epoll 不了,但 poll/select 可以,fcntl 可以
- 权限走文件系统 ACL
- 可以
ls /dev/shm看
缺点:名字全局命名空间,泄漏后需手工清理,和 SysV 差不多。
四、memfd_create:现代范式
int fd = memfd_create("buffer", MFD_CLOEXEC | MFD_ALLOW_SEALING);
ftruncate(fd, size);
void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);没有名字——只有 fd。好处:
- 无全局命名空间:泄漏了最多漏 fd,进程退出自动回收
- SCM_RIGHTS 传递:fd 传给另一进程,另一进程 mmap 就看到同内存
- 支持 seal:
F_ADD_SEALS禁止后续 write / shrink / grow / seal
seal 用在哪?看下节。
4.1 F_SEAL 的应用
Wayland / Chromium / Flutter 把共享 buffer 传给 compositor / GPU 进程时,生产者希望保证”传过去的这段内存以后不能变大小、不能再写入”,避免恶意子进程在 compositor 读的瞬间把页换了。
fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE);seal 是一次单向门。加了不能取消。接收方可以查
F_GET_SEALS 验证对方确实封了。
4.2 memfd 和 tmpfs 的关系
实现上:memfd 就是创建一个 tmpfs 匿名 inode(不链到任何目录),fd 指向它。其他进程只能通过 fd 引用,没法用路径开。
五、hugetlbfs:大页共享
共享 2MB / 1GB 大页场景:DPDK、QEMU vhost、Redis huge page、PostgreSQL。
方式 A:mount hugetlbfs
mount -t hugetlbfs none /mnt/huge -o pagesize=2M然后在上面 open + mmap(MAP_SHARED)。
方式 B:memfd 加 MFD_HUGETLB
int fd = memfd_create("hp", MFD_CLOEXEC | MFD_HUGETLB | MFD_HUGE_2MB);方式
C:mmap(..., MAP_HUGETLB | MAP_HUGE_2MB)。
注意 HugeTLB
页必须预留(/proc/sys/vm/nr_hugepages),不能像
THP 那样延迟组装。预留失败 mmap 直接返回 ENOMEM。
QEMU 用 memfd + MFD_HUGETLB + SCM_RIGHTS 把 guest RAM 传给 vhost-user 进程,是现代虚拟化主流路径。
六、共享内存里的同步
共享内存本身没有同步。并发读写必须自己加同步:
- futex:进程间共享的 mutex/condvar(带
PTHREAD_PROCESS_SHARED) - SysV
信号量(
semget/semop)——老派但可用 - 无锁环形:single-writer-single-reader lock-free ring(perf/DPDK/io_uring SQ-CQ 都是)
- 原子 + 内存屏障:C11
stdatomic.h、C++std::atomic
关键难点:进程间持锁的 crash。一个进程持
pthread mutex 的时候
crash,其他进程卡死。解决:PTHREAD_MUTEX_ROBUST
+ pthread_mutex_consistent,或者用 futex
手动写状态机。
七、/dev/shm 与容量管理
/dev/shm 是一个 tmpfs,默认容量 =
RAM/2。docker 里默认 64MB(用
--shm-size 调)。
常见坑:
- 容器里跑 PostgreSQL,
/dev/shm小,parallel query / shared buffers 崩 - Chromium 在容器里崩,默认 shm 不够用
- MPI 进程通信用
/dev/shm,需要大容量
监控:df -h /dev/shm;进程级
lsof +D /dev/shm。
八、跨机器共享内存?
严格说不存在(无 cache coherence)。但有近似方案:
- RDMA(C-54 未来讲):应用层字节共享的近似,但延迟不是 ns 级
- CXL.mem:PCIe 5+ 的 cache-coherent 拔插内存,多机共享物理池(实验阶段)
- 分布式共享对象:memcached / Redis / Hazelcast 在应用层伪装,本质还是网络
单机才讲”共享内存 = 零拷贝”,跨机一切都是消息。
九、共享内存的安全边界
从一个进程把 fd 传给另一个 UID 的进程 = 绕过路径权限的后门。SCM_RIGHTS 把 fd 送到 bytestream 过去,对端 mmap 就有 RW 能力。
防御:
- seal 减少能力(write seal 封写)
- Landlock / SELinux 限制 SCM_RIGHTS 的 sender/receiver
- 最小权限——别把 MAP_SHARED fd 随便传
十、常见 bug
bug A:服务重启 /dev/shm 里残留旧段 原因:shm_unlink 未调;服务 crash 解决:atexit 或手动清理;或迁移到 memfd
bug B:SysV shm 容量不够
原因:kernel.shmmax 默认小
解决:sysctl 调;或迁移到 memfd
bug C:进程间 mutex 死锁
原因:持锁 crash 没 robust
解决:PTHREAD_MUTEX_ROBUST
bug D:hugetlbfs MAP_FAILED
原因:nr_hugepages
不够或预留失败
诊断:cat /proc/meminfo | grep Huge;grep -r . /sys/kernel/mm/hugepages/
bug E:IPC_PRIVATE 段在 fork-exec 后对端不认识 原因:id 是 PID 相关的全局号,但接受方要知道 id 才能 shmat 解决:父把 id 通过 env/cmdline/socket 传给子
十一、小结
- 写新代码用 memfd_create + mmap + SCM_RIGHTS + seal
- POSIX shm_open 次之——有路径命名但便于调试
- SysV 只做兼容
- 大页用 MFD_HUGETLB 或 hugetlbfs
- 同步独立解决:futex 进程共享、robust mutex、lock-free
下一篇 B-16 讲消息队列——SysV 与 POSIX mq 的历史、kdbus 的夭折、以及现代为什么几乎都转向 socket/broker。
参考文献
- Kerrisk, M. The Linux Programming Interface, Ch. 45-54
- Linux source:
ipc/shm.c、mm/shmem.c、mm/memfd.c - Corbet, J. “memfd_create() and sealing.” LWN.net 2014
- Kernel Documentation:
Documentation/admin-guide/mm/hugetlbpage.rst shm_overview(7)、memfd_create(2)、hugetlbfs
工具
ipcs -m/ipcrm—— SysV shm 段列表与清理ls -la /dev/shm/—— POSIX shm 可见对象cat /proc/<pid>/maps | grep shmem—— 进程映射了哪些 shmpmap -x <pid>—— 映射摘要bpftrace -e 'kprobe:shmem_mmap { ... }'—— 事件级追踪
上一篇:管道、FIFO、socketpair 下一篇:消息队列:SysV、POSIX mq、kdbus
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】消息队列:SysV、POSIX mq、kdbus 夭折
消息队列承诺「有边界的带优先级的 IPC」,但 Linux 上两代 API(SysV msg、POSIX mq)都很冷清。内核内的 kdbus 曾想成为下一代系统总线,最终被拒。本文讲三者的设计、使用限制、以及为什么现代系统几乎都改走 Unix socket + 序列化库 + userspace broker。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。