共享内存与内存映射
复习
- 文件怎样落到磁盘:数据块、元数据和索引节点的基本关系
- 文件分配与空闲空间:数据块怎样找到、占用和回收
- 缓冲与页面缓存:内存怎样减少缓慢的设备访问
TL;DR
- 内存映射(mmap):把一个文件映射进进程的地址空间
- 之后读写这块内存,就等于读写文件,不必再调用 read/write
- 多个进程映射同一个文件,就实现了共享内存
- 它把“文件访问”和“共享页面”统一到了同一套机制上
正文
走到这里,我们已经有了两个世界:一边是文件(字节流),一边是内存(页)。这一章,把两者打通。
把文件“贴”到内存里
平时的文件读写是 read / write:程序把数据在内存和文件之间搬来搬去。可如果程序只是想在文件的某一段上反复读写,每次都调系统调用就很啰嗦。
内存映射(memory mapping,通常叫 mmap)换了个思路:直接把文件的某一段,映射到进程地址空间里的一块区域。之后:
- 程序像访问普通内存一样读写这块区域
- 背后,读写会落到对应的文件页上
文件的一段 ──映射──> 进程地址空间的某片区域
程序直接读写内存 == 读写文件
省去了显式的 read/write,代码更简洁,数据也不必在内核与用户之间来回搬。
顺手实现共享内存
更妙的是:如果多个进程都把同一个文件映射进来,那么它们看到的,就是同一批物理页。
于是,一个进程往这块内存里写,另一个进程马上就能读到——这就是共享内存。前面讲进程间通信时,我们说过共享内存最快,但需要自己同步;mmap 正是实现它的一种常见方式。
进程 A ─┐
├── 映射同一文件 ──> 同一批物理页
进程 B ─┘
和页面缓存的配合
别忘了上一章的页面缓存。mmap 读文件时,数据其实也是通过页面缓存来的:
- 访问映射区域,缺页时把文件页读进页面缓存
- 映射区域指过去,多个进程共享同一份缓存页
- 写入时页变“脏”,由系统择机写回文件
于是,文件访问、共享内存、页面缓存,三件事被统一到同一套“页”的机制里。这种“用一个模型同时解决多个问题”的简洁,正是好设计的标志。当然,它也继承了延迟写的风险:映射区改了、还没写回文件时断电,改动同样可能丢失。
思考题
mmap 让“读写文件”变成了“读写内存”,看起来方便很多。为什么它不能完全取代 read/write?在读写远小于文件大小、或者顺序流式读写时,哪种更合适?
小结
知识点
- mmap 把文件映射进地址空间
- 读写映射区等于读写文件
- 多进程映射同一文件即共享内存
- 与页面缓存配合,统一了文件访问与共享页面
参考资料
- Wikipedia(zh):内存映射文件:mmap
- Wikipedia(zh):共享内存:shared memory
思考题答案(仅供参考)
因为 mmap 要建立映射、处理缺页,本身有开销;对于“只读一小段就完事”的访问,反而比一次 read 更重。而在大文件上做顺序流式读写时,read/write 配合预读往往更顺手,也不必占着地址空间。mmap 的优势场景,是“反复随机访问文件的一部分”,或者“多个进程要共享同一份数据”。一句话:工具各有适用的场合,mmap 不是万能替代。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪