Go语言中不munmap旧映射直接将新文件mmap映射到旧指针是否可行?
结论
你提出的方案不可行,且一定会产生内存泄漏,同时还存在数据一致性的隐藏问题,核心原因如下:
1. 为什么会有内存泄漏?
Go 语言的 GC 只会管理 Go 运行时分配的堆内存,syscall.Mmap 是直接向内核申请的虚拟地址空间映射,完全脱离 Go GC 的管控,只要你不主动调用 syscall.Munmap 释放,这部分地址空间会被进程一直占用,直到进程退出。
你现在的逻辑里注释了旧映射的 Munmap 调用,之后又把变量 b 直接赋值为新映射的切片,旧映射的地址会彻底丢失,你再也没有机会释放它,每更新一次就会泄漏一次对应大小的虚拟内存,长时间运行会耗尽进程的虚拟地址空间,最终触发 mmap 失败。
2. 为什么方案不可行?
你这么做并不能解决并发访问的问题,还会带来数据不一致:
- 如果其他 goroutine 已经提前拷贝了旧的
b切片、或者拿到了切片的底层指针,就算你修改了主 goroutine 里的b变量的值,这些 goroutine 访问的还是旧的映射区域,读到的永远是旧数据,根本感知不到更新。 - 你删除了旧文件之后,旧的映射虽然还能正常访问(内核会保留 inode 直到所有引用释放),但这部分冗余的映射完全无用,只会占用资源。
- 你的示例代码存在无意义的重复操作:你已经提前
mmap得到了nb,完全可以直接把nb赋值给b,不需要先Munmap(nb)再重新mmap一次,纯属多余的系统调用开销。
正确的实现方案
想要安全替换并发访问的 mmap 映射,有两种常用的无段错误方案:
方案1:读写锁管控(最稳妥)
- 所有访问 mmap 切片的 goroutine,访问前先获取读锁,访问完成后释放读锁
- 更新映射时,先获取写锁,拿到锁之后再执行旧映射的
Munmap,再把b赋值为新的映射,最后释放写锁 - 这个方案可以保证不会有任何 goroutine 访问到已经被释放的映射,逻辑简单不容易出问题。
示例代码片段:
import "sync" var ( mmapBuf []byte rwMu sync.RWMutex ) // 所有goroutine访问buf都走这个方法 func GetMmapBuf() []byte { rwMu.RLock() defer rwMu.RUnlock() return mmapBuf } // 更新映射的逻辑 func UpdateMmap(newBuf []byte) { rwMu.Lock() defer rwMu.Unlock() // 释放旧映射,此时没有任何goroutine持有旧buf _ = syscall.Munmap(mmapBuf) mmapBuf = newBuf }
方案2:原子操作+延迟释放(高性能场景)
- 用
atomic.Value存储 mmap 切片,所有 goroutine 用Load方法获取当前最新的切片 - 更新时用
Store方法把新切片存入atomic.Value - 把旧的映射扔到延迟队列里,等待足够长的时间(确保所有持有旧切片的 goroutine 都已经使用完成),再调用
Munmap释放旧映射 - 这个方案性能更高,没有锁的开销,但需要保证等待时间足够覆盖所有 goroutine 持有旧切片的最长时间。
注意事项
- 不要把 mmap 切片的底层指针泄露到其他地方,避免出现不受管控的内存访问
MAP_SHARED模式的映射修改后,必要时调用syscall.Msync主动刷盘,避免进程崩溃丢失数据- 无论什么场景,不用的 mmap 映射必须主动调用
Munmap释放,不要依赖 Go GC 处理内核级的内存资源。
内容的提问来源于stack exchange,提问作者TomSawyer
相关产品推荐
相关产品推荐

