Go mmap 文件内存映射
mmap是个很好用的内存映射工具,它可以将文件映射到内存中,可以方便地操作文件。使用mmap的优点是:
- 内存映射可以使得读写文件的性能更高,因为操作的是内存而不是磁盘。
- 可以方便地操作文件,不需要再通过文件操作的方式打开读写文件。
- 方便多线程操作文件。
优点
mmap 另一个非常重要的特性是:减少内存的拷贝次数。在 linux 系统中,文件的读写操作通常通过 read 和 write 这两个系统调用来实现,这个过程会产生频繁的内存拷贝。比如 read 函数就涉及了 2 次内存拷贝:
- 操作系统读取磁盘文件到页缓存
- 从页缓存将数据拷贝到 read 传递的 buf 中
mmap 只需要一次拷贝。即操作系统读取磁盘文件到页缓存,进程内部直接通过指针方式修改映射的内存。因此 mmap 特别适合读写频繁的场景,既减少了内存拷贝次数,提高效率,又简化了操作。KV数据库 bbolt 就使用了这个方法持久化数据。
例子
package main
import (
"fmt"
"log"
"os"
"syscall"
"unsafe"
)
const defaultMaxFileSize = 1 << 30 // 假设文件最大为 1G
const defaultMemMapSize = 128 * (1 << 20) // 假设映射的内存大小为 128M
func main() {
mmpFile := NewMmpFile("test.txt")
defer mmpFile.munmap()
defer mmpFile.file.Close()
msg := "hello csdn colinrs!"
mmpFile.grow(int64(len(msg) * 2))
for i, v := range msg {
mmpFile.data[i] = byte(v)
}
}
type MmpFile struct {
file *os.File
data *[defaultMaxFileSize]byte
dataRef []byte
}
func NewMmpFile(fileName string) *MmpFile {
file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatalf("Open file error: %v", err)
}
mmpFile := &MmpFile{file: file}
mmpFile.mmap()
return mmpFile
}
func _assert(condition bool, msg string, v ...interface{}) {
if !condition {
panic(fmt.Sprintf(msg, v...))
}
}
func (mmpFile *MmpFile) mmap() {
b, err := syscall.Mmap(int(mmpFile.file.Fd()), 0,
defaultMemMapSize, syscall.PROT_WRITE|syscall.PROT_READ, syscall.MAP_SHARED)
_assert(err == nil, "failed to mmap", err)
mmpFile.dataRef = b
mmpFile.data = (*[defaultMaxFileSize]byte)(unsafe.Pointer(&b[0]))
}
func (mmpFile *MmpFile) grow(size int64) {
if info, _ := mmpFile.file.Stat(); info.Size() >= size {
return
}
_assert(mmpFile.file.Truncate(size) == nil, "failed to truncate")
}
func (mmpFile *MmpFile) munmap() {
_assert(syscall.Munmap(mmpFile.dataRef) == nil, "failed to munmap")
mmpFile.data = nil
mmpFile.dataRef = nil
}
- 遇到 mmp 文件报错为 syscall.Errno=permission denied),
- 可以参考:syscall.Errno=permission denied)
- 将读取文件修改为 os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0666)
参考
- Go Mmap 文件内存映射简明教程
- 阅读笔记:零拷贝及一些引申内容