🐧 第五章 内存管理
📄 一句话概括

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 秒实验

  1. 把文件换成 200 MB,再比一次两种走法——观察差距有没有变化。
  2. 用 mmap 打开一个日志文件并顺序扫一遍,对比 read 逐行读的耗时。
  3. cat /proc/self/maps | wc -l,数一数当前进程有多少段映射。

常见误区

  1. 【mmap 就是把文件装进内存】。它只是把文件挂到地址空间,碰哪页读哪页。
  2. 【mmap 一定比 read 快】。读小文件、只读一次时,映射本身的系统调用成本比 read 还高。
  3. 【mmap 之后文件改了会立刻反映】。要看你有没有重新映射,而且文件被截短时访问会崩。
自测三问(点开对答案) 1. mmap 省掉了哪两样开销? —— **每次访问的系统调用过门**,以及**内核到用户的那一次拷贝**。 2. mmap 不省什么? —— **不省磁盘 IO、不省缺页开销、不改变【数据在不在页缓存】这件事。** 3. 什么场景该用 mmap? —— **大文件随机访问、多进程共享同一份、按结构体覆盖解析**。**从头读到尾的场景用 read 更合适。**

相关笔记