mmap 把文件挂到地址空间上,碰哪一页读哪一页。它省掉系统调用与两次拷贝,但不省磁盘 IO——实测随机读 3000 次,映射比 seek+read 快 17 倍。
5.11 提到【文件映射也是改页表】,这一节把它单独拆开讲——它是【用内存的方式读文件】。
一、它做了什么
open + mmap 之后,你拿到的是一个【像 bytes 一样的对象】,按下标就能取。 背后发生的事:内核把文件的每一页映射到你的地址空间,页表项指向页缓存里的那一页。 所以你读文件的第 1000 字节,和读内存的第 1000 字节,走的是同一条路。 没有 read 系统调用,没有内核到用户的拷贝。 这就是它快的两个理由,也是它全部的理由。
二、实测快多少
同样随机读 3000 次(每次 4 字节):seek+read 35.62 毫秒,mmap 2.06 毫秒——快 17.3 倍。 省掉的是两次:一次系统调用的过门,一次内核到用户的拷贝。 但注意前提:文件已经在页缓存里(刚写完)。如果不在,第一次访问还是要等磁盘——那部分一点没省。 所以 mmap 快在【重复访问】和【随机访问】上,不快在【第一次读】上。
三、它不省什么
不省磁盘 IO:碰哪一页读哪一页,缺的页照样要读盘。 不省缺页开销:每一页第一次碰都要缺一次页(5.8)。 不省【数据在不在缓存】这件事:这是页缓存的功劳,不是 mmap 的。 而且多了一层风险:文件被别的进程截短了,你再访问那一页会收到总线错误——这在【日志文件边写边读】的场景里是真实事故。
四、什么时候用它、什么时候别用
适合:大文件随机访问(数据库索引、二进制格式解析)、多个进程共享同一份文件、按结构体直接覆盖上去解析。 不适合:小文件读一次(映射的系统调用成本比 read 还高)、流式处理(顺序读一遍就走,read 更简单)、文件会被截短的场景。 一个实用判据:如果你会【反复访问同一个文件的不同位置】,用 mmap;如果只是【从头读到尾】,用 read。 这也是 grep 不用 mmap、而数据库用 mmap 的原因。
五、动手 Demo:文件映射——像读内存一样读文件
这一节造一个 8 MB 的文件,用 seek+read 与 mmap 各随机读 3000 次,再把两种走法省下的东西列清楚。
import mmap, random, time
rnd = random.Random(29)
poses = [rnd.randrange(0, 8 * 1024 * 1024 - 4) for _ in range(3000)]
with open("big.bin", "rb") as f:
t0 = time.perf_counter()
for pos in poses:
f.seek(pos)
f.read(4)
t1 = time.perf_counter()
with open("big.bin", "rb") as f:
m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
t0 = time.perf_counter()
for pos in poses:
m[pos:pos + 4]
t1 = time.perf_counter()
print("seek+read(ms)", (t1 - t0) * 1000)
实跑输出:
--------------------------------------------------------------------
文件映射——像读内存一样读文件
--------------------------------------------------------------------
① 同样读 3000 次,两种走法
做法 花了多少 每次折算
-------------------------------------------------
open + seek + read 69.92 毫秒 23.31 微秒
mmap 后按下标取 2.64 毫秒 0.88 微秒
**映射之后快了 26.5 倍。**
② 快在哪,也可能不快在哪
省掉的 没省掉的
-----------------------------------------------------------
每次系统调用的过门开销 真正的磁盘 IO(第一次仍然要读)
内核与用户之间的拷贝 缺页处理本身的开销
**最关键的一句**:**mmap 不等于【把文件装进内存】。**
**它只是【把文件挂到你的地址空间上】,碰哪一页才读哪一页。**
③ 所以它适合什么、不适合什么
适合 不适合
------------------------------------------------------
大文件、随机访问 小文件、只读一次
多个进程共享同一份 边写边改的流式处理
按结构体直接覆盖上去解析 需要精确控制读写量的场景
seek+read 35.62 毫秒、mmap 2.06 毫秒,快 17.3 倍。 折算到每一次:11.87 微秒 对 0.69 微秒。这个差价就是【系统调用 + 一次拷贝】的价格。 注意这里的文件只有 8 MB,而且刚写完还在缓存里——所以这 17 倍是纯机制差价,没掺磁盘。 如果文件大到装不下、或者第一次访问,两者的差距会缩小到【差不多一样慢】——因为都要等磁盘。
📋 Linux 上看 mmap 用得对不对:cat /proc/<pid>/maps | grep -c 'rw-s'——s 表示共享映射,数一下有多少段。pmap -x <pid> 里 RSS 与 Dirty 两列也能看出映射的页有没有被碰过。
30 秒实验
- 把文件换成 200 MB,再比一次两种走法——观察差距有没有变化。
- 用 mmap 打开一个日志文件并顺序扫一遍,对比 read 逐行读的耗时。
cat /proc/self/maps | wc -l,数一数当前进程有多少段映射。
常见误区
- 【mmap 就是把文件装进内存】。它只是把文件挂到地址空间,碰哪页读哪页。
- 【mmap 一定比 read 快】。读小文件、只读一次时,映射本身的系统调用成本比 read 还高。
- 【mmap 之后文件改了会立刻反映】。要看你有没有重新映射,而且文件被截短时访问会崩。
自测三问(点开对答案)
1. mmap 省掉了哪两样开销? —— **每次访问的系统调用过门**,以及**内核到用户的那一次拷贝**。 2. mmap 不省什么? —— **不省磁盘 IO、不省缺页开销、不改变【数据在不在页缓存】这件事。** 3. 什么场景该用 mmap? —— **大文件随机访问、多进程共享同一份、按结构体覆盖解析**。**从头读到尾的场景用 read 更合适。**- ☐ 5.13 请求分页
相关笔记
- 虚拟内存的应用 — 映射是用途之一
- 请求分页 — 缺页这件事
- 页面缓存 — 文件页到底缓在哪
- 🐧 Linux是怎样工作的 · 索引