一、什么是 mmap?
mmap(memory map)是操作系统(主要是类 Unix 系统)提供的一种内存映射文件机制。它允许进程将文件或其他对象直接映射到自己的虚拟地址空间,从而可以像访问内存一样访问文件内容,而不需要显式的 read/write 系统调用。
二、mmap 的基本用法
在 Linux 下,mmap的原型为:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);addr:指定映射的起始地址,通常为 NULL,由内核决定。length:映射的长度。prot:访问权限(如 PROT_READ、PROT_WRITE)。flags:映射类型(如 MAP_SHARED、MAP_PRIVATE)。fd:文件描述符。offset:文件偏移量。
三、mmap 的底层原理
1. 虚拟内存与页表
- 现代操作系统为每个进程分配虚拟地址空间,实际物理内存由内核管理。
- 虚拟地址到物理地址的映射由页表维护,通常以 4KB 为一页。
2. mmap 的实现流程
(1)建立映射
当进程调用mmap时,内核会:
- 在进程的虚拟地址空间中分配一段连续的虚拟地址区域(VMA,Virtual Memory Area)。
- 记录这个 VMA 与文件的映射关系(文件 inode、偏移量等)。
(2)页面缺页异常(Page Fault)
- 进程首次访问映射区域时,发现该虚拟地址没有物理页(页表项无效),触发缺页异常。
- 内核捕获异常,根据 VMA 信息,找到对应的文件页,从文件系统读取数据(或分配空页),将其加载到物理内存,并建立页表映射。
- 后续访问直接命中物理内存,无需系统调用。
(3)同步与回写
- 对于 MAP_SHARED,内存页的修改会被标记为“脏页”,内核会在合适时机(如 msync 或 munmap)将修改内容回写到磁盘文件。
- MAP_PRIVATE 的写入采用写时复制(Copy-On-Write, COW),不会影响原文件。
3. 内核数据结构
- VMA(vm_area_struct):描述一段虚拟内存区域及其属性。
- 文件页缓存(page cache):文件内容读到内存后,存放在页缓存中,供 mmap 及普通 IO 共享。
- 页表:维护虚拟页与物理页的映射关系。
四、mmap 的优势
- 高效 IO:减少用户空间与内核空间的多次拷贝,避免 read/write 的缓冲区复制。
- 共享内存:多个进程可通过 mmap 实现高效的进程间通信。
- 按需加载:只访问到的页面才会被实际加载,节省内存。
- 零拷贝:某些场景下,数据可直接在物理内存中操作,无需显式拷贝。
五、应用场景
- 大文件处理(如数据库、视频流)
- 进程间通信(共享内存段)
- 内存映射设备(如显卡、外设寄存器)
- 文件缓存
六、底层细节与优化
1. 缺页异常处理
- mmap 区域的访问依赖于“延迟加载”,只有访问到某一页时才真正读入内存,减少 IO。
- 如果文件被多进程 mmap,内核只会维护一份物理页缓存,节省内存。
2. COW(写时复制)
- MAP_PRIVATE 时,写操作会触发 COW,内核会分配新的物理页,原页保持只读。
3. 页回收与同步
- 内核的页回收机制会定期将脏页回写到磁盘,保证数据一致性。
- 用户可通过 msync 主动同步。
4. 文件大小与映射长度
- 映射长度超过文件大小时,超出部分访问会触发 SIGBUS 信号。
七、与 read/write 的对比
read/write需要多次用户空间和内核空间的数据复制,效率较低。mmap只需一次内核空间到用户空间的映射,访问数据如同访问普通内存,效率高。
八、常见问题
- 映射区的释放:需要 munmap。
- 文件删除与映射:即使文件被删除,映射区仍然可用,直到所有进程解除映射。
- 多进程同步:需注意并发写入的同步问题。
九、流程图示(简化)
进程调用 mmap | v 内核建立 VMA 映射关系 | v 进程访问映射区 | v 缺页异常 -> 内核加载文件页到物理内存 -> 建立页表映射 | v 进程直接访问物理内存十、类比说明 mmap
1. 类比:图书馆借书
假设你在图书馆看书,有两种方式:
- 传统方式(read/write):你每次想看书的一部分内容,都要去前台借出来,带回自己的座位看,看完再还回去。每次搬运都很麻烦。
- mmap 方式:你直接在书架旁边,把书摊开放在桌子上,随时翻看哪一页都行。你不需要每次都跑到前台借书,只需要翻到你想看的那一页,图书馆管理员会在你翻到那一页时把那一页递给你。
mmap 就像第二种方式:按需借阅,只有你真正需要时,才把内容搬到你面前。这就是“缺页异常”机制。
2. 内核处理细节(更深入)
(1)VMA 与页表
- 进程调用 mmap 后,内核会在进程的虚拟地址空间里建立一个 VMA(虚拟内存区域),并把这个区域标记为和某个文件关联。
- 页表还没有建立实际的物理页映射,只有当你访问到某个地址时,才触发缺页异常。
(2)Page Fault 处理流程
- 当进程访问 mmap 区域的某个地址时,CPU 发现该虚拟地址没有对应的物理页(页表项无效),于是抛出 page fault(缺页异常)。
- 内核捕获异常,查找该地址属于哪个 VMA,确认是和某个文件关联。
- 内核查找文件系统,把对应的数据页读到物理内存(可能从磁盘读取,也可能已经在页缓存里)。
- 更新进程的页表,把虚拟地址映射到刚刚读入的物理页。
- 进程继续运行,仿佛这块内存一直都在。
(3)写入时的 COW(写时复制)
- 如果是 MAP_PRIVATE,写入时会触发写时复制:原来的物理页保持只读,内核会分配一个新的物理页,把数据复制过去,进程的页表指向新的页。
(4)共享机制
- 多个进程 mmap 同一个文件时,内核会维护一份物理页缓存,所有映射都可以访问同样的数据,实现高效共享。
3. mmap 的高级应用
(1)共享内存
- 进程可以 mmap 一个匿名区域(不关联文件,MAP_ANONYMOUS),用于进程间通信。多个进程 mmap 同一个区域,就能实现共享内存。
(2)大文件处理
- 处理超大文件时,mmap 只需映射部分内容,随用随取,不必一次性读入全部数据,节省内存。
(3)零拷贝
- 某些网络服务(如 nginx)用 mmap + sendfile 实现零拷贝,提高 IO 性能。
4. mmap 的局限与注意事项
- 同步问题:多个进程写入同一个 mmap 区域时,需要加锁,否则可能导致数据不一致。
- 资源管理:mmap 的区域释放要用 munmap,否则会导致内存泄漏。
- 信号处理:访问越界或文件被截断后再访问,会触发 SIGSEGV 或 SIGBUS 信号。
- 性能调优:频繁小块写入可能不如直接 write,mmap 更适合大块数据或随机访问场景。
5. 内核源码层面(更底层)
- Linux 内核中,mmap 相关的核心代码在 mm/mmap.c、mm/filemap.c、fs/*.c 等文件。
- VMA 结构体为
struct vm_area_struct,记录了映射区的起止地址、权限、关联文件等信息。 - 缺页处理通过
do_page_fault(),最终调用filemap_fault(),完成文件页的加载和页表更新。
十一、内核源码流程(Linux)
1. mmap 系统调用入口
用户空间调用mmap(),最终会进入内核的sys_mmap()(或do_mmap()):
void *sys_mmap(...) { // 参数校验 // ... // 进入 do_mmap return do_mmap(...) }2. VMA 结构体
每个进程的虚拟地址空间被切分成若干 VMA(struct vm_area_struct),每个 VMA 记录:
- 起止地址
- 权限(读/写/执行)
- 文件映射信息(
struct file *) - 偏移量、flags 等
这些 VMA 被挂在进程的mm_struct的红黑树(或链表)里,便于查找。
3. 缺页异常处理
- 当进程访问未分配物理页的虚拟地址时,CPU 触发 page fault,进入内核的
do_page_fault()。 - 内核查找该地址属于哪个 VMA,确认是文件映射。
- 调用 VMA 的
vm_ops->fault(),通常是filemap_fault(),完成文件内容的加载,建立页表映射。
4. 文件页缓存
- 文件内容被读入页缓存(page cache),所有 mmap/read/write 都共享这份缓存,极大提升了多进程/多线程访问同一文件的效率。
- 脏页(被修改但未写回磁盘的页)由后台进程(如 pdflush 或 writeback)定期同步回磁盘,也可用
msync()手动触发。
5. COW(写时复制)
- MAP_PRIVATE 时,写入会触发 COW,分配新物理页,保证原文件内容不被修改。
- MAP_SHARED 时,直接修改页缓存,后续会同步到磁盘。
十二、性能瓶颈与优化
1. 优势
- 大文件随机访问性能高,避免了多次用户态/内核态拷贝。
- 多进程共享同一份页缓存,节省内存。
2. 瓶颈
- 小文件高频写入,mmap 可能比 write 更慢(频繁缺页、脏页回写)。
- 内存压力大时,页缓存容易被回收,导致频繁 page fault。
- mmap 区域越界、文件被截断,易导致 SIGBUS/SIGSEGV。
3. 优化建议
- 对于大文件、随机读写场景优先使用 mmap。
- 写入后及时调用
msync(),保证数据持久化。 - 合理设计 mmap 区域大小,避免一次性映射超大文件导致内存压力。
- 多进程/多线程并发时,加锁保护 mmap 区域。
十三、工程实践案例
1. 数据库
如 SQLite、MongoDB、Redis 的 AOF 文件,常用 mmap 做持久化,提升写入性能。
2. 图像/视频处理
处理超大图片、视频文件时,mmap 只需映射部分区域,按需加载,节省内存。
3. 高性能服务器
Nginx、Apache 等高性能服务器,利用 mmap + sendfile 实现零拷贝,提升网络 IO 性能。
4. 共享内存
进程间通信(IPC),通过 mmap 映射匿名内存区域(MAP_ANONYMOUS),多个进程共享数据,效率远高于管道、消息队列。
十四、代码示例(C语言)
int fd = open("data.bin", O_RDWR); size_t filesize = ...; void *ptr = mmap(NULL, filesize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); // 直接像数组一样访问文件内容 char value = ((char *)ptr)[12345]; // 修改内容 ((char *)ptr)[12345] = 'A'; // 持久化到磁盘 msync(ptr, filesize, MS_SYNC); // 释放映射 munmap(ptr, filesize); close(fd);十五、常见问题解答
文件被删除后 mmap 区域还能用吗?
可以,直到所有映射被解除,数据还在物理内存页里。mmap 区域越界会怎样?
访问未映射区域会触发 SIGSEGV 或 SIGBUS,导致进程崩溃。多进程写入 mmap 如何同步?
需加锁(如 pthread_mutex),否则可能数据不一致。mmap 区域必须用 munmap 释放吗?
是的,否则会内存泄漏。
十六、Java mmap 实现原理
在 Java 中,MappedByteBuffer是对操作系统底层 mmap 的封装。它允许你将文件的一部分或全部直接映射到内存,读写操作直接反映到文件内容,底层由 JVM 通过 JNI 调用操作系统的 mmap 接口实现。
- FileChannel.map()方法用于创建映射。
- 返回的
MappedByteBuffer实例可以像数组一样 get/set 数据。
底层原理和 C 的 mmap 类似,Java 只是做了对象封装和安全性控制。
十七、Java mmap 代码示例
1. 基本读写示例
import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; public class MmapDemo { public static void main(String[] args) throws Exception { // 打开文件 RandomAccessFile raf = new RandomAccessFile("test.dat", "rw"); FileChannel channel = raf.getChannel(); // 映射文件的前 1MB 到内存 int size = 1024 * 1024; MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, size); // 写入数据 buffer.put(0, (byte)123); buffer.put(1, (byte)45); // 读取数据 byte value = buffer.get(0); System.out.println("Read value: " + value); // 刷新到磁盘(可选) buffer.force(); // 关闭资源 channel.close(); raf.close(); } }2. 只读映射
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size);3. 多进程/多线程共享
多个进程可以 mmap 同一个文件实现共享内存(需自行同步),多个线程可直接共享同一个 MappedByteBuffer 对象。
十八、注意事项
映射区域大小
文件必须足够大,否则映射会失败。可以提前扩展文件大小。同步问题
buffer.force()可以强制把修改刷新到磁盘,否则由操作系统决定何时同步。资源释放
MappedByteBuffer 的释放由 JVM 管理,不能直接unmap,但可以通过反射 hack 或等待 GC。越界访问
访问未映射区域会抛出异常。性能
对于大文件、随机读写场景,mmap 性能非常高,适合日志、数据库、缓存等应用。
十九、工程应用场景
- 日志文件高性能写入
- 大文件处理(图片、视频、数据库文件)
- 进程间通信(通过 mmap 同一个文件实现共享内存)
二十、进阶:MappedByteBuffer 强制释放(unmap)
由于 JVM 没有直接提供 unmap 方法,通常只能等待垃圾回收。但在一些极端场景下,可以用反射 hack:
import sun.misc.Cleaner; import sun.nio.ch.DirectBuffer; public static void unmap(MappedByteBuffer buffer) { if (buffer == null) return; Cleaner cleaner = ((DirectBuffer) buffer).cleaner(); if (cleaner != null) cleaner.clean(); }注意:这种做法依赖于内部 API,不推荐在生产环境使用,JDK 9+ 可能不可用。