mmap不能直接用于多进程同步,因其仅提供共享内存视图,不保证写操作对其他进程的可见顺序,也不防止编译器/cpu重排或缓存不一致;必须配合原子操作、内存序及显式同步机制(如信号量)才能确保正确性。

为什么 mmap 不能直接用于多进程同步
很多人一上来就用 mmap 映射同一块文件或匿名内存,以为写完数据另一端立刻能读到,结果发现数据错乱、读到旧值、甚至段错误。根本原因在于:mmap 提供的是共享内存视图,不是同步原语——它不保证写操作对其他进程可见的顺序,也不防止编译器/CPU 重排或缓存不一致。
必须搭配显式同步机制,否则行为未定义。常见错误包括:
- 仅靠
msync刷回磁盘,但没处理 CPU 缓存行可见性(尤其在非 cache-coherent 的 NUMA 系统上) - 用普通变量做 flag(如
ready_flag = 1),没有std::atomic或内存序约束 - 误以为
MAP_SHARED自动同步所有 CPU 核心的 L1/L2 缓存(实际依赖硬件一致性协议,且不保证指令重排)
如何用 mmap + atomic 实现无锁通信环形缓冲区
最实用的模式是:mmap 分配一块固定大小的共享内存,内部用 std::atomic<size_t></size_t> 管理读写指针,配合内存序控制可见性。不需要 mutex,避免内核态切换开销。
关键点:
- 映射时必须用
MAP_SHARED(MAP_PRIVATE写时复制,改了也看不到) - 读写指针必须声明为
std::atomic<size_t></size_t>,且使用memory_order_acquire/memory_order_release配对 - 结构体布局需手动对齐,避免 false sharing(例如把读指针、写指针、数据区用
alignas(64)分开) - 初始化必须由一个进程完成(比如父进程先写好初始值,再 fork;或用命名信号量协调首次访问)
示例片段(简化版):
struct ShmRing {
alignas(64) std::atomic<size_t> write_pos{0};
alignas(64) std::atomic<size_t> read_pos{0};
char data[4096];
};
// 映射后,生产者:
auto wp = ring->write_pos.load(std::memory_order_acquire);
// ... 检查空间、拷贝数据 ...
ring->write_pos.store(new_wp, std::memory_order_release);
</size_t></size_t>
跨进程首次同步必须用 POSIX 信号量或文件锁
mmap 内存本身无法解决“谁先初始化”“谁等谁”的问题。两个独立进程(非父子)启动后,可能同时尝试读/写未初始化的原子变量,触发未定义行为。
可靠方案只有两种:
- 用命名信号量
sem_open("/myshm_sem", O_CREAT, 0644, 1),一个进程sem_wait后初始化共享结构,再sem_post;另一端等同信号量再开始访问 - 用
open("/tmp/myshm_init", O_CREAT | O_EXCL)尝试创建标志文件,成功者负责初始化,失败者轮询等待文件内容变为 "ready"
绝对不要用 sleep + 轮询原子变量初值——既浪费 CPU,又无法保证初始化完成的精确时机。
Linux 下 mmap 共享内存的清理陷阱
很多人以为 munmap 后内存自动释放,其实不然。匿名 mmap(MAP_ANONYMOUS | MAP_SHARED)在最后一个进程调用 munmap 后,内核仍保留该区域直到所有引用消失;而基于文件的 mmap,若映射的是 shm_open 创建的 fd,则必须显式调用 shm_unlink,否则 /dev/shm 下残留对象会一直占用内存,重启都不清。
更隐蔽的问题:
- fork 后子进程继承 mmap 地址,但若父进程先
munmap,子进程再访问会触发 SIGSEGV - 用
MAP_POPULATE预加载页表虽快,但会阻塞映射调用,且在大内存场景下易 OOM -
msync(MS_SYNC)在 SSD 上延迟高,一般只需MS_ASYNC或干脆不用(除非要求落盘持久化)
真正健壮的做法:用 shm_open + shm_unlink 管理生命周期,并在每个进程退出前确保已 munmap,同时用 atexit 注册清理函数防崩溃泄漏。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











