mmap (内存映射文件) 思维导图
中心主题: mmap (Memory-Mapped Files)
核心概念
- 定义
- 将一个文件或文件的一部分直接映射到进程的虚拟地址空间中。
- 操作文件就像操作内存一样,通过指针读写,无需传统的
read()/write()系统调用。
- 本质
- 一种 I/O 优化技术,也是一种 进程间通信 方式。
- 是操作系统 虚拟内存管理 机制的一个应用。
- 核心思想
- 文件 = 内存:将文件视为进程地址空间中的一块特殊内存。
- 按需加载:操作系统不会立即将整个文件读入物理内存,而是在进程首次访问某部分数据时,才产生 缺页中断,然后将对应的页面从磁盘加载到内存中。
工作原理
- 关键系统调用
mmap(): 创建映射。munmap(): 解除映射。msync(): 将内存中的数据同步回磁盘文件。
- 核心流程
- 调用
mmap():- 进程请求将文件映射到自己的虚拟地址空间。
- 操作系统在进程的页表中创建新的 页表项,这些项指向文件在磁盘上的数据块,而不是物理内存页面。
- 物理内存中并没有加载文件的实际数据。
- 访问内存:
- 进程通过指针访问映射区域的内存地址。
- CPU 发起内存访问,但对应的虚拟地址在页表中标记为 “无效”(表示数据不在物理内存中)。
- 触发缺页中断。
- 缺页中断处理:
- 操作系统的 虚拟内存管理单元 捕获此中断。
- 检查到该虚拟地址对应的是文件映射,
- 在物理内存中找到一个空闲页面(或淘汰一个旧页面)。
- 通过文件系统的 I/O 操作,将文件中对应的数据块 读取 到这个物理页面中。
- 更新页表,将虚拟地址映射到这个新的物理页面。
- 中断处理完毕,CPU 重新执行刚才导致中断的指令。
- 这次,内存访问成功,数据已在物理内存中。
- 写操作:
- 写时复制: 默认情况下,对映射区域的 写操作 会触发 写时复制 机制,操作系统会分配一个全新的物理页面,将原始数据复制过来,然后在这个新页面上进行修改,原始文件数据在此时不会被改变。
- 同步回磁盘: 当调用
msync()或映射被解除时,被修改过的脏页面会被写回到磁盘文件中,更新文件内容。
- 解除映射
munmap():- 进程通知操作系统不再需要这块映射。
- 操作系统:
- 将所有 脏页 写回磁盘(如果设置了
MS_SYNC或MS_ASYNC)。 - 释放进程的页表项。
- 释放相关的物理内存页面(如果它们没有被其他地方引用)。
- 将所有 脏页 写回磁盘(如果设置了
- 调用
优点
- 高效 I/O
- 减少数据拷贝:传统
read/write需要用户空间缓冲区和内核空间缓冲区之间的数据拷贝。mmap直接映射文件,避免了这些拷贝。 - 系统调用开销小:对文件的读写变成了内存访问,大大减少了陷入内核态的次数。
- 减少数据拷贝:传统
- 内存共享
- 高效的 IPC:多个进程可以映射同一个文件的同一部分到各自的地址空间,操作系统会确保它们看到的是同一份物理内存页面,实现了高效的内存共享。
- 进程间通信:一个进程写入映射区域,另一个进程读取,即可实现通信,速度极快。
- 处理大文件
- 内存效率高:可以轻松处理远大于物理内存大小的文件,因为只有实际被访问的部分才会被加载到内存中。
- 随机访问:可以像数组一样对文件进行高效的随机访问,无需从头开始读取。
- 简化代码
代码逻辑更简洁,文件操作和内存操作合二为一。

缺点
- 内存占用
- 即使映射了整个大文件,也会占用进程的 虚拟地址空间,虚拟地址空间是有限的(例如在 32 位系统上只有 4GB),可能会影响加载其他大型库或创建其他内存映射。
- 同步复杂性
- 内存和文件不同步:修改了映射内存,文件内容并不会立即改变,必须显式调用
msync()来同步,否则数据可能会丢失。 - 多进程同步问题:多个进程同时读写同一个映射区域时,需要额外的同步机制(如信号量、互斥锁)来避免数据竞争和冲突。
- 内存和文件不同步:修改了映射内存,文件内容并不会立即改变,必须显式调用
- 异常处理
访问映射区域时可能会因为各种原因(如段错误、权限问题)导致程序崩溃,比传统的文件操作更难调试。
- 不适合小文件
- 对于非常小的文件,
mmap的创建和管理开销可能比直接read/write更大。
- 对于非常小的文件,
主要使用场景
- 处理大型文件
- 数据库系统(如 SQLite, PostgreSQL)。
- 日志分析、大文本文件处理。
- 实现高效的 IPC
- 共享内存区域,用于生产者-消费者模型、多进程协作。
- Web 服务器(如 Nginx)使用
mmap来高效地发送静态文件。
- 动态链接库加载
- 操作系统将
.so或.dll文件映射到进程的地址空间,实现代码和数据的共享。
- 操作系统将
- 设备 I/O
将设备(如显卡显存、硬件寄存器)的内存区域映射到用户空间,直接操作硬件。
代码示例 (C/C++)
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
const char *file_path = "test.txt";
const char *write_data = "Hello, mmap!";
int fd;
struct stat sb;
char *mapped_addr;
// 1. 打开文件
fd = open(file_path, O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 2. 获取文件大小
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
exit(EXIT_FAILURE);
}
off_t file_size = sb.st_size;
// 3. 调整文件大小 (如果文件为空,需要扩展)
if (file_size == 0) {
if (ftruncate(fd, strlen(write_data)) == -1) {
perror("ftruncate");
close(fd);
exit(EXIT_FAILURE);
}
file_size = strlen(write_data);
}
// 4. 创建内存映射
// PROT_READ: 可读
// PROT_WRITE: 可写
// MAP_SHARED: 映射的修改会写回文件,并与其他共享该映射的进程可见
mapped_addr = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped_addr == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
printf("文件已映射到地址 %p\n", mapped_addr);
// 5. 通过指针操作文件(像操作内存一样)
printf("原始内容: %s\n", mapped_addr);
// 写入数据
strcpy(mapped_addr, write_data);
printf("已写入: %s\n", mapped_addr);
// 6. 同步内存到文件 (可选)
// MS_SYNC: 等待写入完成
// MS_ASYNC: 异步写入
if (msync(mapped_addr, file_size, MS_SYNC) == -1) {
perror("msync");
}
// 7. 解除映射
if (munmap(mapped_addr, file_size) == -1) {
perror("munmap");
}
// 8. 关闭文件描述符
close(fd);
return 0;
}
关键参数总结
| 参数 | 说明 |
|---|---|
mmap() |
|
void *addr |
建议的起始地址,通常设为 NULL 让内核自动选择。 |
size_t length |
要映射的文件区域长度(字节)。 |
int prot |
保护标志:PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE。 |
int flags |
映射类型:MAP_SHARED (修改共享) 或 MAP_PRIVATE (写时复制)。 |
int fd |
文件描述符。 |
off_t offset |
文件中的偏移量,必须是页面大小的整数倍。 |
munmap() |
|
void *addr |
mmap() 返回的地址。 |
size_t length |
要解除映射的区域大小。 |
msync() |
|
void *addr |
要同步的内存地址。 |
size_t length |
要同步的内存区域大小。 |
int flags |
MS_SYNC (同步), MS_ASYNC (异步), MS_INVALIDATE (使其他缓存失效)。 |
这个思维导图希望能帮助你全面、系统地理解 mmap 的各个方面,从它的核心思想到底层实现,再到实际应用和注意事项,都进行了详细的梳理。
