Linux 内存映射(mmap):将文件直接映射到内存的高效 IO 技术
内存映射(Memory Mapping)是 Linux 内核提供的一种高效文件访问机制,通过将磁盘文件的部分或全部内容映射到进程的地址空间,使进程可以像访问内存一样读写文件,无需通过传统的 read()/write() 系统调用。这种技术在高性能 IO 场景(如数据库、大文件处理)中被广泛应用。
内存映射的基本原理
核心思想
内存映射通过 mmap() 系统调用建立进程地址空间的一块区域与磁盘文件的关联:
- 内核会在进程的虚拟地址空间中分配一段连续的地址范围(称为 “映射区”);
- 这段地址不直接对应物理内存,而是与目标文件的磁盘区块建立映射关系;
- 当进程访问映射区时,内核会通过页表将访问转换为对文件的读写操作(按需加载数据到物理内存,即 “页缓存” 机制)。
与传统 IO 的区别
传统文件访问(read()/write())需要经过 “用户态缓冲区 ↔ 内核页缓存 ↔ 磁盘” 的两次数据拷贝,而内存映射简化了流程:
| 操作方式 | 数据路径 | 优势场景 |
|---|---|---|
| 传统 IO | 用户缓冲区 ↔ 内核页缓存 ↔ 磁盘 | 小文件、随机读写、需精确控制 IO |
| 内存映射(mmap) | 进程地址空间(映射区) ↔ 内核页缓存 ↔ 磁盘 | 大文件、顺序读写、频繁访问 |
核心优势:减少用户态与内核态之间的数据拷贝,提升大文件访问效率。
mmap () 系统调用详解
函数原型
1 |
|
参数说明:
addr:指定映射区的起始地址(通常设为NULL,由内核自动分配);length:映射的文件长度(字节数,必须 ≥ 0);prot:映射区的保护权限(内存访问权限):PROT_READ:可读;PROT_WRITE:可写;
PROT_EXEC:可执行;PROT_NONE:无权限。
flags:映射类型和行为(关键参数):MAP_SHARED:映射区的修改会同步到文件(进程间共享修改);MAP_PRIVATE:映射区的修改是私有的(Copy-on-Write,不影响原文件);MAP_ANONYMOUS:匿名映射(无关联文件,用于进程间共享内存)。
fd:待映射的文件描述符(需先通过open()打开);offset:文件映射的起始偏移量(必须是页大小的整数倍,通常为 0)。
返回值:
- 成功:返回映射区的起始地址(void *);
- 失败:返回
MAP_FAILED((void *)-1),并设置errno。
配套函数:munmap ()
用于解除内存映射,释放映射区:
1 | int munmap(void *addr, size_t length); |
addr:mmap()返回的映射区起始地址;length:映射区的长度(需与mmap()一致);- 返回值:0 表示成功,-1 表示失败。
内存映射的关键特性
两种映射模式(flags 参数)
(1)MAP_SHARED(共享映射)
- 进程对映射区的修改会同步到磁盘文件,且其他映射该文件的进程可见;
- 适用于需要持久化修改或多进程协作的场景(如共享日志文件)。
(2)MAP_PRIVATE(私有映射)
- 进程对映射区的修改不会同步到原文件,也不会被其他进程看到;
- 内核采用 “写时复制(Copy-on-Write)” 机制:仅当进程修改映射区时,才复制物理页,避免影响其他进程;
- 适用于临时读写文件(如读取配置文件并临时修改)。
匿名映射(MAP_ANONYMOUS)
无需关联文件(
fd设为 -1,offset忽略),映射区的内容在进程退出后消失;常用于进程间共享内存(结合
fork()或线程),或需要大块内存临时存储数据的场景;示例:
1
2// 分配 4KB 匿名共享内存(可被子进程继承)
void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
页缓存与延迟写入
内存映射依赖内核的页缓存(Page Cache) 机制:
首次访问映射区时,内核会将文件内容从磁盘加载到物理内存(页缓存);
进程修改映射区时,内核先更新页缓存,再通过 “延迟写回” 机制异步将脏页(修改过的页)写入磁盘(可通过
msync()强制同步);msync()函数:手动同步映射区与文件,确保修改持久化:1
2int msync(void *addr, size_t length, int flags);
// flags:MS_SYNC(同步等待写完成)、MS_ASYNC(异步写)、MS_INVALIDATE(丢弃缓存,重新从文件加载)
使用示例:通过 mmap 读写文件
读取文件内容
1 |
|
写入文件内容(共享映射)
1 |
|
注意事项与限制
- 文件大小与映射长度:
length不能超过文件大小(除非文件可扩展,且以O_RDWR打开);- 映射超过文件大小的部分写入时,可能导致文件扩展(取决于文件系统支持)。
- 对齐要求:
offset必须是系统页大小(通常 4KB)的整数倍,否则mmap()失败(EINVAL)。
- 文件描述符关闭:
- 映射建立后,可关闭
fd(映射仍有效),但建议保持打开直至映射解除(便于错误排查)。
- 映射建立后,可关闭
- 信号安全:
mmap()和munmap()不是异步信号安全的,避免在信号处理函数中调用。
- 性能陷阱:
- 小文件使用
mmap()可能因页缓存 overhead 导致性能不如传统 IO; - 频繁修改小区域时,
MAP_PRIVATE的写时复制可能带来额外开销。
- 小文件使用
应用场景
内存映射适用于以下场景:
- 大文件处理:如数据库表文件、日志文件,避免频繁
read()/write()拷贝; - 进程间共享内存:通过
MAP_SHARED或匿名映射实现高效通信(比管道、消息队列快); - 动态加载代码:如操作系统加载可执行文件到内存执行(映射二进制文件并设置
PROT_EXEC); - 零拷贝 IO:结合网络编程(如
sendfile()),实现文件数据从页缓存直接发送到网络,避免用户态拷贝