mmap在Go中不能直接用syscall.Mmap,因其不处理跨平台差异、页对齐、错误码转换、GC感知及生命周期管理,易致SIGBUS、panic或内存泄漏;必须选用github.com/edsrzf/mmap-go等成熟封装库。

为什么 mmap 在 Go 里不能直接用 syscall.Mmap 就完事
Go 标准库不提供跨平台的内存映射封装,syscall.Mmap 是底层系统调用,直接用它容易踩坑:Windows 上要用 syscall.VirtualAlloc + syscall.CreateFileMapping,Linux/macOS 虽支持 mmap,但权限标志(如 MAP_SHARED vs MAP_PRIVATE)、偏移对齐(必须是页大小倍数)、错误码处理逻辑都不统一。更麻烦的是,Go 的 GC 不知道你映射了外部内存,如果映射区被意外释放或文件被 truncate,panic: runtime error: invalid memory address 会直接炸掉。
用 golang.org/x/exp/mmap 还是自己封装 mmap 系统调用
golang.org/x/exp/mmap 是实验性包,API 稳定性无保障,且只支持 Unix-like 系统,Windows 完全没覆盖。生产环境别碰。更稳妥的做法是用成熟第三方封装:github.com/edsrzf/mmap-go —— 它做了三件事:自动处理平台差异、注册 finalizer 防止资源泄漏、暴露 Flush() 和 Unmap() 显式控制生命周期。关键点:
-
mmap.FileMap持有文件句柄和映射地址,必须显式调用Unmap(),否则 Windows 下句柄泄漏极快 -
Flush()只对MAP_SHARED有效,MAP_PRIVATE修改不会写回文件 - 映射长度不能超过文件当前大小(除非先
f.Truncate()),否则ReadAt会返回io.EOF
如何安全地并发读写同一块 mmap 区域
内存映射本身不提供同步语义。多个 goroutine 同时读没问题,但写必须加锁——不是因为 mmap 线程不安全,而是文件内容一致性需要业务逻辑保证。常见错误:
- 用
sync.Mutex锁整个映射区:吞吐暴跌,尤其大文件 - 用
sync.RWMutex允许多读,但写操作仍阻塞所有读:读多写少场景下依然卡 - 完全不加锁,靠原子操作改 struct 字段:字段未对齐或跨 cache line 时,
atomic.StoreUint64可能写坏相邻字段
推荐方案:按页切分(os.Getpagesize() 通常为 4096),每页配一个 sync.RWMutex,读写前计算偏移所属页号再锁定。这样写操作只锁 4KB,不影响其他区域。
立即学习“go语言免费学习笔记(深入)”;
如何避免 mmap 导致 OOM 或 swap 暴涨
映射大文件不等于立即分配物理内存,但一旦访问任意位置,就会触发 page fault 并实际加载。如果同时映射 10GB 文件并随机访问,Linux 很可能把其他进程挤出内存。关键控制点:
- 用
mmap.MapFlags加syscall.MAP_NORESERVE(Linux)或MEM_COMMIT(Windows)可延迟分配,但无法彻底规避 - 对超大文件,改用分段映射:每次只
mmap128MB,用完Unmap()再映射下一段 - 监控
/proc/[pid]/maps(Linux)或GetProcessMemoryInfo(Windows)确认 RSS 是否异常增长 - 绝对不要在 HTTP handler 里长期持有 mmap 句柄——goroutine 泄漏 = mmap 泄漏 = 内存泄漏
真正难的不是怎么映射,是怎么让 mmap 行为可预测:页对齐、显式刷盘、按需映射、及时释放,缺一不可。


















