内存映射文件
内存映射文件(memory-mapped files)是操作系统提供的一种特殊功能:把一个磁盘文件”映射”到进程的虚拟地址空间里,之后像访问内存数组一样访问它,不再需要 read / write。
它是请求分页机制的一个直接副产品——既然页可以从外存按需调入,那么”外存”是对换区还是普通文件,机制上并无区别。
机制
传统文件访问 vs 内存映射
传统方式每读一次文件,数据要走两步:
- 系统调用
read()陷入内核,把数据从磁盘读到内核缓冲区 - 再从内核缓冲区拷贝到用户提供的缓冲区
每次读写都要经历一次系统调用和一次内核到用户的数据拷贝。
内存映射方式则是:
- 进程调用
mmap(),请求把某个文件映射到自己的虚拟地址空间 - 操作系统只是建立映射关系(在页表里做记号),并不真的读取任何数据
- 进程直接用指针访问那段地址
- 访问到某页时若它还不在内存,触发缺页中断,操作系统按普通的缺页流程把该页从文件中调入
关键在于第 2 步和第 4 步:建立映射时不搬数据,真正访问哪一页才调哪一页。 这就是”按需”的含义。
三个好处
① 省掉了一次数据拷贝。 数据从磁盘调入页框后,进程直接访问该页框,不需要再拷贝到用户缓冲区。这就是常说的”零拷贝”的一种形态。
② 省掉了大量系统调用。 建立映射只需一次 mmap,之后的所有访问都是普通的内存访问指令,不再陷入内核(除非缺页)。传统方式则是每次读写都要一次系统调用。
③ 天然支持共享。 多个进程映射同一个文件时,操作系统可以让它们的页表指向同一批物理页框。于是一个进程的修改,另一个进程立刻就能看到——这就成了一种进程通信手段,而且是共享存储类中效率最高的一种。
写回时机
进程对映射区的修改不会立即写回磁盘。写回发生在这些时刻:
- 该页被置换出去时(前提是修改位 M = 1)
- 进程解除映射(
munmap)或退出时 - 进程主动要求同步(如
msync)
也就是说,修改是先落在内存页框上的,磁盘上的文件在一段时间内是”过时”的。这既是它高效的原因,也是它的风险——进程异常终止可能丢失尚未写回的修改。
与页缓存的关系
现代操作系统把内存中缓存的文件数据统一称为页缓存(page cache)。内存映射文件的页,本质上就是页缓存中的页被映射进了某个进程的地址空间。
由此可以把内存中的页分成两类,这个分类在3.2.2 讲调入来源时已经出现过:
| 文件映射页(file-backed) | 匿名页(anonymous) | |
|---|---|---|
| 内容来自 | 某个文件 | 无对应文件(堆、栈) |
| 典型 | 可执行代码、mmap 的文件、页缓存 | malloc 的空间、进程栈 |
| 回收时 | 干净页直接丢弃,脏页写回原文件 | 只能换出到对换区(swap) |
| 是否占用 swap | 否 | 是 |
这个分类解释了一个常见的困惑:为什么系统显示”已用内存”很高、“已缓存”特别大,但系统运行毫无压力?
因为文件映射页(页缓存)是随时可回收的——干净页直接丢掉即可,连写回都不用。它们占着内存只是”顺便缓存一下”,一旦有真实需求,立刻可以让出来。
这正是 Linux 那句话的含义:空闲的内存是被浪费的内存——与其空着,不如拿来缓存文件,反正随时能还。
边界
内存映射文件 vs 对换区
两者都涉及”页面在内存与外存之间往返”,但外存那一端的角色完全不同:
| 内存映射文件 | 对换区(swap) | |
|---|---|---|
| 外存内容 | 用户的真实文件,本身有意义 | 临时存放被换出的页,无独立意义 |
| 页的类型 | 文件映射页 | 匿名页 |
| 干净页被回收 | 直接丢弃(文件里有原本) | 匿名页没有”原本”,必须先写出 |
| 目的 | 访问文件 + 共享 | 扩充内存容量 |
核心差别:文件映射页有”娘家”可回,匿名页没有。 干净的文件页可以随时丢弃,因为文件里那份就是最新的;而匿名页一旦丢弃,数据就永远消失了,所以必须先写到 swap。
内存映射文件是共享存储的一种
在2.1.5 进程通信里讲过共享存储。内存映射同一个文件,正是实现共享存储最常用的方式。
而且它比”匿名共享内存”多一个好处:数据有文件作后盾,进程全部退出后内容仍然保留在磁盘上。
但要注意 2.1.5 的那条边界仍然成立:操作系统只负责把同一块内存映射给双方,不负责同步。 多进程同时写同一映射区,仍然需要自己用信号量或锁来互斥。
映射建立时不读数据
这是最容易误解的一点:mmap 一个 1GB 的文件,不会占用 1GB 内存,也不会立刻产生任何磁盘 I/O。
它只是在页表中建立了”这段虚拟地址对应文件的这些位置”的记号。真正的数据搬运全部由后续的缺页中断按页驱动。
因此映射一个远大于物理内存的文件是完全合法的——只要不同时访问全部内容即可。
对照速查
| 传统 read/write | 内存映射 | |
|---|---|---|
| 系统调用次数 | 每次读写都要 | 只在建立映射时一次 |
| 数据拷贝 | 磁盘→内核缓冲→用户缓冲(多一次拷贝) | 磁盘→页框,进程直接访问 |
| 何时读数据 | 调用 read 时 | 访问到该页、缺页时 |
| 共享 | 需额外机制 | 天然支持(映射同一文件) |
| 页的类型 | 来源 | 回收方式 | 占 swap |
|---|---|---|---|
| 文件映射页 | 文件 | 干净页直接丢弃,脏页写回文件 | ❌ |
| 匿名页 | 无(堆、栈) | 必须换出到 swap | ✅ |
考点
- 内存映射文件的实质:把文件映射进虚拟地址空间,用缺页机制按需调入
- 建立映射时不搬数据,也不占用相应大小的内存
- 三个好处:省一次拷贝、省系统调用、天然支持共享
- 修改不立即写回,在置换、解除映射或主动同步时才写
- 文件映射页 vs 匿名页:前者有”原本”可回,干净页直接丢弃,不占 swap
链接
- 🏠 返回总览:操作系统第 3 章:内存管理总览
- ⬅️ 上一节:3.2.6 页框回收
- ➡️ 下一节:3.2.8 虚拟存储器性能影响因素
- 🔗 作为进程通信手段见 2.1.5 进程的通信
- 📖 名词库:第 3 章名词库