Go Mmap 文件内存映射简明教程

image.png

1 mmap 简介

In computing, mmap is a POSIX-compliant Unix system call that maps files or devices into memory. It is a method of memory-mapped file I/O. – mmap - wikipedia.org

简单理解,mmap 是一种将文件/设备映射到内存的方法,实现文件的磁盘地址和进程虚拟地址空间中的一段虚拟地址的一一映射关系。也就是说,可以在某个进程中通过操作这一段映射的内存,实现对文件的读写等操作。修改了这一段内存的内容,文件对应位置的内容也会同步修改,而读取这一段内存的内容,相当于读取文件对应位置的内容。

mmap 另一个非常重要的特性是:减少内存的拷贝次数。在 linux 系统中,文件的读写操作通常通过 read 和 write 这两个系统调用来实现,这个过程会产生频繁的内存拷贝。比如 read 函数就涉及了 2 次内存拷贝:

    1. 操作系统读取磁盘文件到页缓存;
    1. 从页缓存将数据拷贝到 read 传递的 buf 中(例如进程中创建的byte数组)。

mmap 只需要一次拷贝。即操作系统读取磁盘文件到页缓存,进程内部直接通过指针方式修改映射的内存。因此 mmap 特别适合读写频繁的场景,既减少了内存拷贝次数,提高效率,又简化了操作。KV数据库 bbolt 就使用了这个方法持久化数据。

2 标准库 mmap

Go 语言标准库 ==golang.org/x/exp/mmap== 仅实现了 read 操作,后续能否支持 write 操作未知。使用场景非常有限。看一个简单的例子:

从第4个byte开始,读取 tmp.txt 2个byte的内容。

1package main 2 3import ( 4 "fmt" 5 "golang.org/x/exp/mmap" 6) 7 8func main() { 9 at, _ := mmap.Open("./tmp.txt") 10 buff := make([]byte, 2) 11 _, _ = at.ReadAt(buff, 4) 12 _ = at.Close() 13 fmt.Println(string(buff)) 14}
1$ echo "abcdefg" > tmp.txt 2$ go run . 3ef

如果使用 os.File 操作,代码几乎是一样的,os.File`` 还支持写操作 WriteAt

1package main 2 3import ( 4 "fmt" 5 "os" 6) 7 8func main() { 9 f, _ := os.OpenFile("tmp.txt", os.O_CREATE|os.O_RDWR, 0644) 10 _, _ = f.WriteAt([]byte("abcdefg"), 0) 11 12 buff := make([]byte, 2) 13 _, _ = f.ReadAt(buff, 4) 14 _ = f.Close() 15 fmt.Println(string(buff)) 16} 17

3 mmap(linux)

如果要支持 write 操作,那么就需要直接调用 mmap 的系统调用来实现了。Linux 和 Windows 都支持 mmap,但接口有所不同。对于 linux 系统,mmap 方法定义如下:

func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error)

每个参数的含义分别是:

1- fd:待映射的文件描述符。 2- offset:映射到内存区域的起始位置,0 表示由内核指定内存地址。 3- length:要映射的内存区域的大小。 4- prot:内存保护标志位,可以通过或运算符`|`组合 5 - PROT_EXEC // 页内容可以被执行 6 - PROT_READ // 页内容可以被读取 7 - PROT_WRITE // 页可以被写入 8 - PROT_NONE // 页不可访问 9- flags:映射对象的类型,常用的是以下两类 10 - MAP_SHARED // 共享映射,写入数据会复制回文件, 与映射该文件的其他进程共享。 11 - MAP_PRIVATE // 建立一个写入时拷贝的私有映射,写入数据不影响原文件。

首先定义2个常量和数据类型Demo:

1const defaultMaxFileSize = 1 << 30 // 假设文件最大为 1G 2const defaultMemMapSize = 128 * (1 << 20) // 假设映射的内存大小为 128M 3 4type Demo struct { 5 file *os.File 6 data *[defaultMaxFileSize]byte 7 dataRef []byte 8} 9 10func _assert(condition bool, msg string, v ...interface{}) { 11 if !condition { 12 panic(fmt.Sprintf(msg, v...)) 13 } 14}
  • 内存有换页机制,映射的物理内存可以远小于文件。
  • Demo结构体由3个字段构成,file 即文件描述符,data 是映射内存的起始地址,dataRef 用于后续取消映射。

定义 mmap, grow, ummap 三个方法:

1func (demo *Demo) mmap() { 2 b, err := syscall.Mmap(int(demo.file.Fd()), 0, defaultMemMapSize, syscall.PROT_WRITE|syscall.PROT_READ, syscall.MAP_SHARED) 3 _assert(err == nil, "failed to mmap", err) 4 demo.dataRef = b 5 demo.data = (*[defaultMaxFileSize]byte)(unsafe.Pointer(&b[0])) 6} 7 8func (demo *Demo) grow(size int64) { 9 if info, _ := demo.file.Stat(); info.Size() >= size { 10 return 11 } 12 _assert(demo.file.Truncate(size) == nil, "failed to truncate") 13} 14 15func (demo *Demo) munmap() { 16 _assert(syscall.Munmap(demo.dataRef) == nil, "failed to munmap") 17 demo.data = nil 18 demo.dataRef = nil 19}
  • mmap 传入的内存保护标志位为 syscall.PROT_WRITE|syscall.PROT_READ,即可读可写,映射类型为 syscall.MAP_SHARED,即对内存的修改会同步到文件。

  • syscall.Mmap 返回的是一个切片对象,需要从该切片中获取到内存的起始地址,并转换为可操作的 byte 数组,byte数组的长度为 defaultMaxFileSize。

  • grow 用于修改文件的大小,Linux 不允许操作超过文件大小之外的内存地址。例如文件大小为 4K,可访问的地址是data[0~4095],如果访问 data[10000] 会报错。

  • munmap 用于取消映射。

在文件中写入 hello, world!

1func main() { 2 _ = os.Remove("tmp.txt") 3 f, _ := os.OpenFile("tmp.txt", os.O_CREATE|os.O_RDWR, 0644) 4 demo := &Demo{file: f} 5 demo.grow(1) 6 demo.mmap() 7 defer demo.munmap() 8 9 msg := "hello world!" 10 11 demo.grow(int64(len(msg) * 2)) 12 for i, v := range msg { 13 demo.data[2*i] = byte(v) 14 demo.data[2*i+1] = byte(' ') 15 } 16}
  • 在调用 mmap 之前,调用了 grow(1),因为在 mmap 中使用 &b[0] 获取到映射内存的起始地址,所以文件大小至少为 1 byte

  • 接下来,便是通过直接操作 demo.data,修改文件内容了。

运行:

1$ go run . 2$ cat tmp.txt 3h e l l o w o r l d! 44 mmap(Windows)

相对于 Linux,Windows 上 mmap 的使用要复杂一些。

1func (demo *Demo) mmap() { 2 h, err := syscall.CreateFileMapping(syscall.Handle(demo.file.Fd()), nil, syscall.PAGE_READWRITE, 0, defaultMemMapSize, nil) 3 _assert(h != 0, "failed to map", err) 4 5 addr, err := syscall.MapViewOfFile(h, syscall.FILE_MAP_WRITE, 0, 0, uintptr(defaultMemMapSize)) 6 _assert(addr != 0, "MapViewOfFile failed", err) 7 8 err = syscall.CloseHandle(syscall.Handle(h)); 9 _assert(err == nil, "CloseHandle failed") 10 11 // Convert to a byte array. 12 demo.data = (*[defaultMaxFileSize]byte)(unsafe.Pointer(addr)) 13} 14 15func (demo *Demo) munmap() { 16 addr := (uintptr)(unsafe.Pointer(&demo.data[0])) 17 _assert(syscall.UnmapViewOfFile(addr) == nil, "failed to munmap") 18} 19
  • 需要 CreateFileMappingMapViewOfFile 两步才能完成内存映射。MapViewOfFile 返回映射成功的内存地址,因此可以直接将该地址转换成 byte 数组。

  • Windows 对文件的大小没有要求,直接操作内存data,文件大小会自动发生改变。

使用时无需关注文件的大小。

1func main() { 2 _ = os.Remove("tmp.txt") 3 f, _ := os.OpenFile("tmp.txt", os.O_CREATE|os.O_RDWR, 0644) 4 demo := &Demo{file: f} 5 demo.mmap() 6 defer demo.munmap() 7 8 msg := "hello world!" 9 for i, v := range msg { 10 demo.data[2*i] = byte(v) 11 demo.data[2*i+1] = byte(' ') 12 } 13}
1$ go run . 2$ cat .\tmp.txt 3h e l l o w o r l d !
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

swap空间的增减方法

(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )