惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
有赞技术团队
有赞技术团队
S
Securelist
Attack and Defense Labs
Attack and Defense Labs
T
Threat Research - Cisco Blogs
L
LINUX DO - 最新话题
The GitHub Blog
The GitHub Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
S
Security Affairs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
美团技术团队
量子位
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
GbyAI
GbyAI
O
OpenAI News
IT之家
IT之家
F
Full Disclosure
W
WeLiveSecurity
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Palo Alto Networks Blog
P
Privacy International News Feed
Webroot Blog
Webroot Blog
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
月光博客
月光博客
博客园 - 【当耐特】
N
News | PayPal Newsroom
Cloudbric
Cloudbric
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
The Exploit Database - CXSecurity.com
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
博客园 - 三生石上(FineUI控件)
K
Kaspersky official blog
云风的 BLOG
云风的 BLOG
宝玉的分享
宝玉的分享
博客园 - 叶小钗
TaoSecurity Blog
TaoSecurity Blog
Martin Fowler
Martin Fowler
Simon Willison's Weblog
Simon Willison's Weblog
A
Arctic Wolf
Apple Machine Learning Research
Apple Machine Learning Research
D
Docker
aimingoo的专栏
aimingoo的专栏
Microsoft Security Blog
Microsoft Security Blog
PCI Perspectives
PCI Perspectives
Microsoft Azure Blog
Microsoft Azure Blog
雷峰网
雷峰网

极客兔兔

Go sync.Cond | Go 语言高性能编程 Go 死码消除与调试(debug)模式 | Go 语言高性能编程 Go sync.Once | Go 语言高性能编程 Go 逃逸分析 | Go 语言高性能编程 2020 年终总结 | 极客兔兔 Go struct 内存对齐 | Go 语言高性能编程 Go 空结构体 struct{} 的使用 | Go 语言高性能编程 控制协程(goroutine)的并发数量 | Go 语言高性能编程 | 极客兔兔 如何退出协程 goroutine (其他场景) | Go 语言高性能编程 如何退出协程 goroutine (超时场景) | Go 语言高性能编程 Go 语言陷阱 - 数组和切片 | Go 语言高性能编程 减小 Go 代码编译后的二进制体积 | Go 语言高性能编程 Go Reflect 提高反射性能 | Go 语言高性能编程 读写锁和互斥锁的性能比较 | Go 语言高性能编程 | 极客兔兔 for 和 range 的性能比较 | Go 语言高性能编程 切片(slice)性能及陷阱 | Go 语言高性能编程 | 极客兔兔 字符串拼接性能及原理 | Go 语言高性能编程 | 极客兔兔 pprof 性能分析 | Go 语言高性能编程 benchmark 基准测试 | Go 语言高性能编程 Go 语言高性能编程 | 极客兔兔 Go 接口型函数的使用场景 | 极客兔兔 Python 简明教程 | 快速入门 | 极客兔兔 Go 语言笔试面试题(代码输出) | 极客面试 | 极客兔兔 动手写RPC框架 - GeeRPC第七天 服务发现与注册中心(registry) | 极客兔兔 动手写RPC框架 - GeeRPC第六天 负载均衡(load balance) 动手写RPC框架 - GeeRPC第五天 支持HTTP协议 | 极客兔兔 动手写RPC框架 - GeeRPC第四天 超时处理(timeout) | 极客兔兔 动手写RPC框架 - GeeRPC第三天 服务注册(service register) 动手写RPC框架 - GeeRPC第二天 支持并发与异步的客户端 | 极客兔兔 动手写RPC框架 - GeeRPC第一天 服务端与消息编码 | 极客兔兔 7天用Go从零实现RPC框架GeeRPC | 极客兔兔 Go 语言笔试面试题(并发编程) | 极客面试 | 极客兔兔 Go 语言笔试面试题(基础语法) | 极客面试 | 极客兔兔 Go 语言笔试面试题汇总 | 极客面试 | 极客兔兔 Go Context 并发编程简明教程 | 快速入门 动手写ORM框架 - GeeORM第七天 数据库迁移(Migrate) | 极客兔兔 动手写ORM框架 - GeeORM第六天 支持事务(Transaction) | 极客兔兔 动手写ORM框架 - GeeORM第五天 实现钩子(Hooks) | 极客兔兔 动手写ORM框架 - GeeORM第四天 链式操作与更新删除 | 极客兔兔 动手写ORM框架 - GeeORM第三天 记录新增和查询 | 极客兔兔 动手写ORM框架 - GeeORM第二天 对象表结构映射 | 极客兔兔 动手写ORM框架 - GeeORM第一天 database/sql 基础 SQLite 常用命令 | 速查表(Cheat Sheet) 7天用Go从零实现ORM框架GeeORM | 极客兔兔 动手写分布式缓存 - GeeCache第七天 使用 Protobuf 通信 动手写分布式缓存 - GeeCache第六天 防止缓存击穿 | 极客兔兔 动手写分布式缓存 - GeeCache第五天 分布式节点 | 极客兔兔 动手写分布式缓存 - GeeCache第四天 一致性哈希(hash) | 极客兔兔 Go Mock (gomock)简明教程 | 快速入门 动手写分布式缓存 - GeeCache第三天 HTTP 服务端 动手写分布式缓存 - GeeCache第二天 单机并发缓存 | 极客兔兔 Go Test 单元测试简明教程 | 快速入门 7天用Go从零实现分布式缓存GeeCache | 极客兔兔 Go WebAssembly (Wasm) 简明教程 | 快速入门 Go RPC & TLS 鉴权简明教程 | 快速入门 Go Protobuf 简明教程 | 快速入门 Go语言动手写Web框架 - Gee第七天 错误恢复(Panic Recover) WSL, Git, Mircosoft Terminal 等常用工具配置 Rust 简明教程 | 快速入门 | 极客兔兔 Go语言动手写Web框架 - Gee第六天 模板(HTML Template) 百宝箱 - 值得收藏的工具网站 | 极客兔兔 Go语言动手写Web框架 - Gee第五天 中间件Middleware | 极客兔兔 Go语言动手写Web框架 - Gee第四天 分组控制Group | 极客兔兔 Go语言动手写Web框架 - Gee第三天 前缀树路由Router | 极客兔兔 博客折腾记(七) - Gitalk Plus | 极客兔兔 Go语言动手写Web框架 - Gee第二天 上下文Context | 极客兔兔 Go2 新特性简明教程 | 快速入门 | 极客兔兔 博客折腾记(六) - 不要为了流量忘记了初心 | 极客兔兔 Go语言动手写Web框架 - Gee第一天 http.Handler | 极客兔兔 7天用Go从零实现Web框架Gee教程 | 极客兔兔 Go Gin 简明教程 | 快速入门 Go 语言简明教程 | 快速入门 | 极客兔兔 机器学习笔试面试题 11-20 | 极客面试 | 极客兔兔 机器学习笔试面试题 1-10 | 极客面试 | 极客兔兔 机器学习笔试面试题汇总 | 极客面试 | 极客兔兔 TensorFlow 2 中文文档 - RNN LSTM 文本分类 TensorFlow 2 中文文档 - TFHub 迁移学习 TensorFlow 2 中文文档 - 卷积神经网络分类 CIFAR-10 TensorFlow 2 中文文档 - 保存与加载模型 TensorFlow 2 中文文档 - 过拟合与欠拟合 TensorFlow 2 中文文档 - 回归预测燃油效率 TensorFlow 2 中文文档 - 特征工程结构化数据分类 TensorFlow 2 中文文档 - IMDB 文本分类 TensorFlow 2 中文文档 - MNIST 图像分类 TensorFlow 2 / 2.0 中文文档 TensorFlow 2.0 (九) - 强化学习 70行代码实战 Policy Gradient 博客折腾记(五) - 友链这件事,没那么简单 | 极客兔兔 博客折腾记(四) - 原创资格是争取来的 | 极客兔兔 TensorFlow 2.0 (八) - 强化学习 DQN 玩转 gym Mountain Car TensorFlow 2.0 (七) - 强化学习 Q-Learning 玩转 OpenAI gym 博客折腾记(三) - 主题设计、彩蛋与阅读量翻倍 | 极客兔兔 TensorFlow 2.0 (六) - 监督学习玩转 OpenAI gym game 博客折腾记(二) - 对搜索引擎的理解 | 极客兔兔 博客折腾记(一) - 极致性能的尝试 | 极客兔兔 Pandas 数据处理(三) - Cheat Sheet 中文版 TensorFlow 2.0 (五) - mnist手写数字识别(CNN卷积神经网络) TensorFlow入门(四) - mnist手写数字识别(制作h5py训练集) | 极客兔兔 TensorFlow入门(三) - mnist手写数字识别(可视化训练) | 极客兔兔 Pandas 数据处理(二) - 筛选数据 | 极客兔兔 Pandas 数据处理(一) - DataFrame 与 Series
Go Mmap 文件内存映射简明教程 | 快速入门
2020-04-20 · via 极客兔兔

golang mmap

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的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"golang.org/x/exp/mmap"
)

func main() {
at, _ := mmap.Open("./tmp.txt")
buff := make([]byte, 2)
_, _ = at.ReadAt(buff, 4)
_ = at.Close()
fmt.Println(string(buff))
}
1
2
3
$ echo "abcdefg" > tmp.txt
$ go run .
ef

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"os"
)

func main() {
f, _ := os.OpenFile("tmp.txt", os.O_CREATE|os.O_RDWR, 0644)
_, _ = f.WriteAt([]byte("abcdefg"), 0)

buff := make([]byte, 2)
_, _ = f.ReadAt(buff, 4)
_ = f.Close()
fmt.Println(string(buff))
}

3 mmap(linux)

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

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

每个参数的含义分别是:

1
2
3
4
5
6
7
8
9
10
11
- fd:待映射的文件描述符。
- offset:映射到内存区域的起始位置,0 表示由内核指定内存地址。
- length:要映射的内存区域的大小。
- prot:内存保护标志位,可以通过或运算符`|`组合
- PROT_EXEC
- PROT_READ
- PROT_WRITE
- PROT_NONE
- flags:映射对象的类型,常用的是以下两类
- MAP_SHARED
- MAP_PRIVATE

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const defaultMaxFileSize = 1 << 30        
const defaultMemMapSize = 128 * (1 << 20)

type Demo struct {
file *os.File
data *[defaultMaxFileSize]byte
dataRef []byte
}

func _assert(condition bool, msg string, v ...interface{}) {
if !condition {
panic(fmt.Sprintf(msg, v...))
}
}
  • 内存有换页机制,映射的物理内存可以远小于文件。
  • Demo结构体由3个字段构成,file 即文件描述符,data 是映射内存的起始地址,dataRef 用于后续取消映射。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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]))
}

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
}
  • 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, geektutu!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
_ = os.Remove("tmp.txt")
f, _ := os.OpenFile("tmp.txt", os.O_CREATE|os.O_RDWR, 0644)
demo := &Demo{file: f}
demo.grow(1)
demo.mmap()
defer demo.munmap()

msg := "hello geektutu!"

demo.grow(int64(len(msg) * 2))
for i, v := range msg {
demo.data[2*i] = byte(v)
demo.data[2*i+1] = byte(' ')
}
}
  • 在调用 mmap 之前,调用了 grow(1),因为在 mmap 中使用 &b[0] 获取到映射内存的起始地址,所以文件大小至少为 1 byte。
  • 接下来,便是通过直接操作 demo.data,修改文件内容了。

运行:

1
2
3
$ go run .
$ cat tmp.txt
h e l l o g e e k t u t u !

4 mmap(Windows)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (demo *Demo) mmap() {
h, err := syscall.CreateFileMapping(syscall.Handle(demo.file.Fd()), nil, syscall.PAGE_READWRITE, 0, defaultMemMapSize, nil)
_assert(h != 0, "failed to map", err)

addr, err := syscall.MapViewOfFile(h, syscall.FILE_MAP_WRITE, 0, 0, uintptr(defaultMemMapSize))
_assert(addr != 0, "MapViewOfFile failed", err)

err = syscall.CloseHandle(syscall.Handle(h));
_assert(err == nil, "CloseHandle failed")


demo.data = (*[defaultMaxFileSize]byte)(unsafe.Pointer(addr))
}

func (demo *Demo) munmap() {
addr := (uintptr)(unsafe.Pointer(&demo.data[0]))
_assert(syscall.UnmapViewOfFile(addr) == nil, "failed to munmap")
}
  • 需要 CreateFileMappingMapViewOfFile 两步才能完成内存映射。MapViewOfFile 返回映射成功的内存地址,因此可以直接将该地址转换成 byte 数组。
  • Windows 对文件的大小没有要求,直接操作内存data,文件大小会自动发生改变。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
_ = os.Remove("tmp.txt")
f, _ := os.OpenFile("tmp.txt", os.O_CREATE|os.O_RDWR, 0644)
demo := &Demo{file: f}
demo.mmap()
defer demo.munmap()

msg := "hello geektutu!"
for i, v := range msg {
demo.data[2*i] = byte(v)
demo.data[2*i+1] = byte(' ')
}
}
1
2
3
$ go run .
$ cat .\tmp.txt
h e l l o g e e k t u t u !

附 参考



上一篇 « 动手写ORM框架 - GeeORM第七天 数据库迁移(Migrate) 下一篇 » Go Context 并发编程简明教程

© 2026 - 极客兔兔 - 沪ICP备18001798号-1

👁   📚