mmap 机制可以很大程度上简化磁盘文件操作,通过一定的封装,磁盘上的文件读写操作可以简化为内存的读写操作。
(1)直接读写内存-简洁明了
简单地读写内存非常简单,例如下面向数组的第三个元素进行赋值:
a := make([]int, 10)
a[0] = 511
内存中字段的读写操作相当自然,你甚至可能没有注意到在写内存这件事情。
(2)读写磁盘文件-步骤复杂
但是读写磁盘上的文件就相对复杂很多了,例如我们要在磁盘上读写的文件内容如下:
Hello World! This project is for learning mmap.
如果我们要读取文件开始 6 个偏移量后的 5 个字节数据,那么如下代码是一个方式:
package main
import (
"fmt"
"os"
)
func main() {
f, _ := os.Open("./article.txt")
defer f.Close()
buffer := make([]byte, 5)
f.Seek(6, 0)
_, _ = f.Read(buffer)
fmt.Println(string(buffer)) // 输出:world
}
读取磁盘上文件对比直接读取内存麻烦很多,步骤如下:
- 打开文件,并且注意文件资源的及时释放
- 创建一个 []byte 数组,用于存储从磁盘上读取的字节
- 利用 Seek 方法设置文件读取的偏移量
- 利用 Read 方法读取文件中的字节数据到 []byte 数组中
磁盘上文件读写比内存读写麻烦很多的原因就是 CPU 只能读写内存,而不能直接读写磁盘。磁盘文件上的字节数据需要首先读取到内存中,才能够被内存操作。
(3)mmap-读写磁盘文件就像读写内存
mmap 会负责将磁盘上不一定连续存储的数据按 block 读取到内核空间中,内核空间中的磁盘数据是连续的。然后,将内核空间中的数据映射到用户空间中,这个过程不涉及拷贝。
利用 mmap 机制,操作用户空间中的数据相当于操作磁盘上文件,它们之间的一致性依赖于缺页异常以及异步的 flush 机制实现。
案例代码如下:
package main
import (
"fmt"
"os"
"syscall"
"unsafe"
)
const defaultMaxFileSize = 1 << 30 // 假设文件最大为 1G
const defaultMemMapSize = 128 * (1 << 20) // 假设映射的内存大小为 128M
type Demo struct {
file *os.File // 文件
data *[defaultMaxFileSize]byte // 该数组负责指向 mmap 读取数据的 []byte 首地址
dataRef []byte // 引用了 Mmmap 调用返回的 []byte 数组
}
// 根据 condition 状态位进行断言
func _assert(condition bool, msg string, v ...interface{}) {
if !condition {
panic(fmt.Sprintf(msg, v...))
}
}
func (demo *Demo) mmap() {
b, err := syscall.Mmap(int(demo.file.Fd()), 0, defaultMemMapSize, syscall.PROT_WRITE|syscall.PROT_READ, syscall.MAP_SHARED)
_assert(err == nil, "failed to mmap", err)
demo.dataRef = b
demo.data = (*[defaultMaxFileSize]byte)(unsafe.Pointer(&b[0]))
}
// grow 的作用是尝试将文件扩容至 size 大小,如果已经大于 size,那么不做任何操作
func (demo *Demo) grow(size int64) {
if info, _ := demo.file.Stat(); info.Size() >= size {
return
}
_assert(demo.file.Truncate(size) == nil, "failed to truncate")
}
// 资源释放
func (demo *Demo) munmap() {
_assert(syscall.Munmap(demo.dataRef) == nil, "failed to munmap")
demo.data = nil
demo.dataRef = nil
}
func main() {
_ = os.Remove("tmp.txt")
// open file
f, _ := os.OpenFile("tmp.txt", os.O_CREATE|os.O_RDWR, 0644) // 644 只有拥有者有读写权限;而属组用户和其他用户只有读权限
demo := &Demo{file: f}
demo.grow(1)
// mmap
demo.mmap()
defer demo.munmap()
// write into file on the disk like write into array in memory
str := "helloworld"
demo.grow(int64(len(str)))
for i := 0; i < len(str); i++ {
demo.data[i] = str[i]
}
}
案例来源说明:Go Mmap 文件内存映射简明教程