Appearance
内存映射文件
2026 大纲 三(二)5 内存映射文件(memory-mapped files)——属「三、内存管理 →(二)虚拟内存管理」。
把文件当成内存来读写
到这里,请求分页的整套机制已经齐了:页表项里有存在位,不在就缺页, 缺页就从对换区把它调进来。
现在换个角度看这套机制——它其实和"读文件"是同一件事。
平时读文件要走 read():陷入内核、把数据从磁盘读进页高速缓存、 再从页高速缓存拷贝一份到你给的用户缓冲区,然后返回。 每读一段就走一遍,还得自己管缓冲区、自己记读到哪了。
可虚存机制做的事情几乎一模一样:你访问一个地址, 它发现不在内存,就去后备存储把那一页取回来,然后你直接访问。 唯一的差别是"后备存储"是对换区而不是你的文件。
那把后备存储换成文件不就行了?——这就是内存映射文件: 把文件的某一段,登记为进程虚拟地址空间里某一段的后备存储。 登记之后,读写这段内存就等于读写这段文件,用指针,不再走任何系统调用。
三处最容易想错的,读之前先立住:
mmap() 返回时一个字节都没读——它只划出虚拟地址区间、登记对应关系、 把页表项置成"不在内存"。所以映射一个 100 GB 的文件几乎是瞬时的, mmap 的耗时与文件大小无关,而 read 正比于字节数。
它省下的不是磁盘 I/O,而是"页高速缓存 → 用户缓冲区"那一次 CPU 拷贝; "磁盘 → 页高速缓存"那次 DMA 谁也省不掉。
它也不是零成本——它把系统调用换成了缺页中断,而缺页中断更贵。
一、基本概念
内存映射文件(Memory-Mapped File):将磁盘上的文件内容映射到进程的虚拟地址空间,之后对该区域的读写就等价于对文件的读写。
建立映射的系统调用是 mmap,它的参数直接决定了上面几条语义:
| 参数 | 含义 | 它决定了什么 |
|---|---|---|
addr | 建议的映射起始虚拟地址 | 通常传空,由内核选一段空闲的虚拟地址区间 |
length | 映射长度 | 划出多大一段虚拟地址空间 |
prot | 访问权限:可读 / 可写 / 可执行 | 落进页表项的保护位 |
flags | 共享 / 私有 / 匿名 | 修改是否对别人可见、是否写回文件 |
fd | 被映射文件的描述符 | 后备存储是哪个文件;匿名映射时不指定 |
offset | 文件内偏移(须按页对齐) | 映射文件的哪一段 |
一个由"页是唯一计量单位"推出的边界
映射区的长度若不是页的整数倍,最后一页的尾部余量同样是可读写的(那一页整页都被分配了),但超出文件长度的那部分修改不会写回文件——文件不会因此变长。要让文件变长,得先用别的手段把文件扩大再映射。
二、修改什么时候写回文件
这是内存映射文件最含糊、也最必须讲清的一处。"OS 负责写回"是对的,但什么时候写回有四个不同的触发点:
| 触发点 | 谁发起 | 时机确定吗 |
|---|---|---|
| 该页被置换出去时 | 页面置换算法 | 不确定——取决于内存压力和置换算法选了谁 |
| OS 的周期性回写 | 内核的回写机制(按脏页停留时间或脏页比例阈值) | 不确定——只保证"最终会写" |
进程显式请求同步(msync) | 程序自己 | 确定:调用返回即表示指定范围已落盘 |
解除映射(munmap)或进程退出 | 内核 | 确定,但要等到那个时候 |
想要确定性(比如写完必须保证掉电不丢),只能靠显式的 msync。这与 write() 的语义其实是对称的——write() 返回只表示数据进了页高速缓存,同样不保证落盘,要落盘得靠 fsync。
三、与页高速缓存的关系:这决定了"少一次拷贝"成不成立
常规 read()/write() 走的路是:磁盘 →(DMA)→ 页高速缓存(内核缓冲区)→(CPU 拷贝)→ 用户缓冲区。 内存映射走的路是:磁盘 →(DMA)→ 页高速缓存→ 把这一页的页框号直接填进用户进程的页表项。
关键在于两条路的中间那一站是同一个东西。 反过来推一遍就明白这个前提有多要紧:假如映射页和文件缓存是两份独立的副本,那么用户改了映射页就必须再同步回文件缓存,一次拷贝也省不掉;而且一个进程用 write() 写、另一个进程用 mmap 读同一个文件,两边看到的内容还会不一致。
把"少一次拷贝"算成具体数字:4 MB 文件、4 KB 页,read 与 mmap 逐项对照(想知道省下来的到底是哪几项、多进程时差多少时展开)
题目(数据自造):某系统页大小 4 KB,要把一个 4 MB 的文件完整读一遍。方案 A 用 read(),每次读 4 KB;方案 B 用 mmap() 建立共享映射后顺序访问一遍。
第一步:算页数。 映射与缺页的计量单位都是页,后面每一项开销都要按页数来数。
第二步:逐项对照
| 项目 | 方案 A:read() 1024 次 | 方案 B:mmap() + 顺序访问 |
|---|---|---|
| 系统调用次数 | 1024 | 1(只有那次 mmap) |
| 用户态/内核态切换 | 2048 次 | 2 次 |
| 缺页中断次数 | 0 | 1024(每页一次) |
| DMA 传输量(磁盘→内存) | 4 MB | 4 MB |
| CPU 参与的内存拷贝 | 4 MB(页高速缓存 → 用户缓冲区) | 0 |
| 物理内存占用 | 页高速缓存 4 MB + 用户缓冲区 4 MB = 8 MB | 页高速缓存 4 MB(用户页表直接指向它)= 4 MB |
把系统调用和缺页并列是要害:mmap 不是没有代价,它把 1024 次系统调用换成了 1024 次缺页中断,而缺页中断比系统调用更贵(还含一次磁盘 I/O)。所以 mmap 真正省下的是那 4 MB 的 CPU 拷贝和4 MB 的物理内存。若用预调页一次调入多页,缺页次数还能进一步压低。
第三步:两个进程同读一个文件
方案 A:各自 read() | 方案 B:各自共享映射 | |
|---|---|---|
| 页高速缓存 | 4 MB(两进程共用) | 4 MB |
| 用户缓冲区 | 4 MB × 2 = 8 MB | 0(两进程页表指向同一批页框) |
| 合计 | 12 MB | 4 MB |
进程越多差距越大——
四、它与共享内存、与请求分页的关系
多个进程把同一个文件用共享映射映入各自的地址空间,页表项就指向同一批物理页框——这就是一块共享内存。它与匿名共享内存并不是两种机制,而是同一种机制的两种后备存储:
| 内存映射文件(共享映射) | 匿名共享内存 | |
|---|---|---|
| 后备存储 | 被映射的文件 | 对换区,不落盘 |
| 怎么找到对方 | 靠文件路径——无亲缘关系的任意进程都能打开同一个文件 | 靠内核对象的标识,或父子进程继承 |
| 数据持久性 | 进程全退出后内容还在文件里 | 进程退出、内核释放后内容消失 |
| 通信本质 | 完全相同:让多个进程的页表项指向同一批物理页框 | 同左 |
再往下看一层,映射页与普通虚存页的差别同样只有"后备存储是谁"这一项:
| 环节 | 普通虚存页 | 映射页 | 一样吗 |
|---|---|---|---|
| 首次访问 | 缺页中断,从对换区/文件区调入 | 缺页中断,从被映射文件调入 | 机制相同,来源不同 |
| 页表项字段 | P / A / M / 外存地址 | P / A / M / 文件内位置 | 相同 |
| 内存不足时 | 按置换算法换出 | 按置换算法换出 | 相同 |
| 脏页换出 | 写回对换区 | 写回被映射的文件 | 机制相同,去向不同 |
| 干净页换出 | 直接丢弃 | 直接丢弃 | 相同 |
匿名映射说明了什么
把 flags 设成匿名、不指定文件,就得到一段不关联任何文件的映射区——它跟普通的堆、栈没有本质区别。这说明 mmap 的本质不是"访问文件的新方式",而是"操作虚拟地址空间的通用手段":在地址空间里划一段区间,并指定它的后备存储。指定成文件,就是内存映射文件;不指定,就是普通的匿名内存。"映射文件"只是这个通用机制最有名的一个用法。
关于共享存储与消息传递、管道的选型判据(以及一道量化对比的算例),见进程间通信。
考点速记
- 核心机制一句话:把文件的某一段,登记为进程虚拟地址空间里某一段的"后备存储"。普通虚存页的后备存储是对换区,映射页换成了这个文件——除此之外两者走完全相同的请求分页流程。⚠️ 是映射到进程的虚拟地址空间,不是映射到系统的物理地址空间。
mmap()返回时一个字节都没读:它只划出虚拟地址区间、登记对应关系、把页表项置成"不在内存",真正读入发生在首次访问触发缺页中断时。判据:mmap 的耗时与文件大小无关,read 的耗时正比于字节数。- 之后直接用指针读写,不再经过任何系统调用。
flags决定语义:共享映射——修改对其他映射者可见、会写回文件;私有映射——写时复制,修改互不可见、永远不写回文件;匿名映射——不关联文件,后备存储是对换区、区内初始全为 0。- ⚠️私有映射"永远不写回文件"最容易搞反:一旦某进程写,内核为它单独复制一份页,从此与文件、与其他进程都断开,那份私有页的后备存储变成对换区。可执行文件的代码段通常就用私有映射。
offset必须页对齐——页表项只能整页地指向一个页框,页是映射机制唯一的计量单位。prot落进页表项的保护位,越权访问触发保护故障。- 写回时机四个触发点,只有一个确定:① 该页被置换时(不确定)② OS 周期性回写(只保证"最终会写")③ 进程显式
msync(唯一确定)④ 解除映射或进程退出时。 - "少一次拷贝"的前提是 mmap 与 read/write 共用同一份页高速缓存。少掉的是"页高速缓存 → 用户缓冲区"那次 CPU 拷贝;"磁盘 → 页高速缓存"那次 DMA 谁也省不掉。
- mmap 不是零成本:它把系统调用换成了缺页中断(缺页中断更贵,还含一次磁盘 I/O)。真正省下的是那笔 CPU 拷贝和物理内存。
- 共享映射即共享内存,因此可实现进程间通信:多进程映射同一文件时,它们的页表项指向同一批物理页框——一方写入,另一方立刻看得见。⚠️ 但各进程的虚拟地址可以完全不同,相同的是映射到的物理地址。
- 与匿名共享内存的差别只有两处:后备存储(文件 vs 对换区)与命名方式(文件路径 vs 内核对象标识或父子继承)。判据一句话:看这批共享页框换出去时写到哪儿。
这一节在真题里被考过的形式:
两道题都是近年新出的,且都在考同一组分辨点——映射到哪、共享的是什么、地址相不相同。
- 判断内存映射文件的四条叙述(2025-30)。答可实现进程间通信 + 将文件映射到进程的虚拟地址空间两条。⚠️ 错的两条各有一个字的差别:"映射到系统的物理地址空间"错——映射的对象是进程的虚拟地址空间;"实现了页面到磁盘块的映射"错——那是文件系统本来就在做的事,内存映射建立的是虚拟页到文件内容的对应。
- 两个进程共享一个映射文件,判断页表项与虚拟地址的关系(2026-30)。答 W1 和 W2 映射的物理地址相同。⚠️ 另三条正好把三处分辨点各说反一次:"页表项内容完全不同"错——两者的页框号是相同的(指向同一批物理页框),只是保护位等可能不同;"P1 的修改不影响 P2"错——共享映射下一方写入另一方立刻可见,这正是它能用来通信的原因;"虚拟地址相同"错——各进程的虚拟地址由各自的地址空间独立分配,通常并不相同。
复习优先级:必须拿满,且两道题的判据高度重合。 把速记第一条和第十条记死即可—— 映射到虚拟地址空间、共享的是物理页框而非虚拟地址。 第五条(私有映射不写回)目前没考过但极易被设成错项,也要记。 "少一次拷贝"那笔账属于理解层面,不会直接设问。
易错:说内存映射文件把文件映射到物理地址空间。是映射到进程的虚拟地址空间。
易错:认为共享同一文件的两个进程,虚拟地址也相同。虚拟地址各自独立分配、通常不同,相同的是映射到的物理地址。
易错:认为共享映射下一方的修改不影响另一方。页表项指向同一批物理页框,写入立刻可见——这正是它能做 IPC 的原因。
易错:认为私有映射的修改最终也会写回文件。永远不写回——一写就复制一份私有页,后备存储改成对换区。
易错:认为
mmap()会把文件内容读进内存。它一个字节都不读,首次访问才缺页调入。
易错:认为 mmap 省掉了磁盘 I/O。省的是"页高速缓存 → 用户缓冲区"那次 CPU 拷贝,DMA 那次谁也省不掉。
易错:认为 mmap 一定比 read 快。它把系统调用换成了更贵的缺页中断,省下的是拷贝和物理内存。
相关知识
页框分配与回收|请求页式管理|虚拟存储性能与改进|进程间通信