mmap
可以进程的虚拟内存中创建内存映射,常用来将磁盘文件映射到内存,读写文件转变成读写对应的内存
使用mmap
读写文件,通常比普通的read
和write
系统调用要更快:
- 直接操作内存,避免了系统调用的执行,从而避免用户态/内核态之间的切换开销
- 直接读写内存,不需要经过内核缓冲区,减少数据拷贝
用户访映射的内存时,如果磁盘数据还没有加载到内存中,会触发缺页异常,然后操作系统会以page
为单位将磁盘文件加载到内存中;如果内存数据被修改了,操作系统会将脏页回写到对应的文件中(脏页回写并不是实时的)。缺页异常的处理开销会比较大,尤其是当物理内存不足时,还会涉及到内存页的淘汰,如果被淘汰的内存页是脏页的话,还需要将其同步到磁盘中。在某些情况下,使用mmap
映射内存,可能会频繁的触发缺页异常,从而导致性能下降,甚至不如直接使用read/write
系统调用,比如物理内存很小,而映射的文件很大时。
而且使用mmap
并不会更改映射文件的大小,当需要更改文件大小时,需要重新映射。
mmap
除了支持将文件映射到内存,还支持匿名映射,匿名映射不需要底层文件。匿名映射的内存也可以在多个进程间共享,因此可以用来实现进程间的内存共享通信。go
运行时的内存管理,便是使用匿名映射向操作系统申请了一块大内存,然后基于tcmalloc
进行内存管理。
在go中使用mmap
mmap
相关的系统调用有:
- mmap:创建内存映射
- munmap:取消内存映射
- msync:同步内存到磁盘文件;内存的修改并不是实时写回磁盘的,当对实时性要求很高,比如数据库的写操作,需要在写内存后手动刷新到磁盘中
接下来通过一个实例看一下如果在go中使用这几个接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| func checkErr(err error) { if err != nil { log.Fatal(err) } }
func main() { const INT_SIZE = unsafe.Sizeof(int(0)) fd, err := os.OpenFile("a.txt", os.O_RDWR|os.O_CREATE, os.ModePerm) checkErr(err)
info, _ := fd.Stat() if info.Size() != int64(INT_SIZE) { fd.Truncate(int64(INT_SIZE)) }
buf, err := syscall.Mmap(int(fd.Fd()), 0, int(INT_SIZE), syscall.PROT_WRITE|syscall.PROT_READ, syscall.MAP_SHARED) fd.Close() checkErr(err) p := (*int)(unsafe.Pointer(&buf[0])) log.Printf("the value saved on file is %d", *p) *p = rand.New(rand.NewSource(time.Now().UnixNano())).Intn(1000) _, _, errno := syscall.Syscall(syscall.SYS_MSYNC, uintptr(unsafe.Pointer(p)), uintptr(INT_SIZE), syscall.MS_SYNC) if errno != 0 { log.Fatal(syscall.Errno(errno)) } _, _, errno = syscall.Syscall(syscall.SYS_MUNMAP, uintptr(unsafe.Pointer(p)), uintptr(INT_SIZE), 0) if errno != 0 { log.Fatal(syscall.Errno(errno)) }
}
|
可以看到,我们可以直接将mmap
创建的内存,强制转换成具体的类型值,然后像普通的变量一样操作,连序列化/反序列化都省了。