Linux 内存映射(mmap):将文件直接映射到内存的高效 IO 技术
内存映射(Memory Mapping)是 Linux 内核提供的一种高效文件访问机制,通过将磁盘文件的部分或全部内容映射到进程的地址空间,使进程可以像访问内存一样读写文件,无需通过传统的 read()/write() 系统调用。这种技术在高性能 IO 场景(如数据库、大文件处理)中被广泛应用。
内存映射的基本原理
核心思想
内存映射通过 mmap() 系统调用建立进程地址空间的一块区域与磁盘文件的关联:
- 内核会在进程的虚拟地址空间中分配一段连续的地址范围(称为 “映射区”);
- 这段地址不直接对应物理内存,而是与目标文件的磁盘区块建立映射关系;
- 当进程访问映射区时,内核会通过页表将访问转换为对文件的读写操作(按需加载数据到物理内存,即 “页缓存” 机制)。
与传统 IO 的区别
传统文件访问(read()/write())需要经过 “用户态缓冲区 ↔ 内核页缓存 ↔ 磁盘” 的两次数据拷贝,而内存映射简化了流程:
| 操作方式 |
数据路径 |
优势场景 |
| 传统 IO |
用户缓冲区 ↔ 内核页缓存 ↔ 磁盘 |
小文件、随机读写、需精确控制 IO |
| 内存映射(mmap) |
进程地址空间(映射区) ↔ 内核页缓存 ↔ 磁盘 |
大文件、顺序读写、频繁访问 |
核心优势:减少用户态与内核态之间的数据拷贝,提升大文件访问效率。
mmap () 系统调用详解
函数原型
1 2 3
| #include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
|
参数说明:
addr:指定映射区的起始地址(通常设为 NULL,由内核自动分配);
length:映射的文件长度(字节数,必须 ≥ 0);
prot:映射区的保护权限(内存访问权限):
PROT_READ:可读;
PROT_EXEC:可执行;
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)
页缓存与延迟写入
内存映射依赖内核的页缓存(Page Cache) 机制:
首次访问映射区时,内核会将文件内容从磁盘加载到物理内存(页缓存);
进程修改映射区时,内核先更新页缓存,再通过 “延迟写回” 机制异步将脏页(修改过的页)写入磁盘(可通过 msync() 强制同步);
msync()函数:手动同步映射区与文件,确保修改持久化:
1 2
| int msync(void *addr, size_t length, int flags);
|
使用示例:通过 mmap 读写文件
读取文件内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <unistd.h> #include <sys/stat.h>
int main() { int fd = open("test.txt", O_RDONLY); struct stat st; fstat(fd, &st);
char *addr = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0); if (addr == MAP_FAILED) { perror("mmap failed"); return 1; } printf("File content: %s\n", addr); munmap(addr, st.st_size); close(fd); return 0; }
|
写入文件内容(共享映射)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <unistd.h> #include <sys/stat.h> #include <string.h>
int main() { int fd = open("test.txt", O_RDWR | O_CREAT, 0644); ftruncate(fd, 1024);
char *addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (addr == MAP_FAILED) { perror("mmap failed"); return 1; } strcpy(addr, "Hello, mmap!"); msync(addr, 1024, MS_SYNC); munmap(addr, 1024); close(fd); return 0; }
|
注意事项与限制
- 文件大小与映射长度:
length 不能超过文件大小(除非文件可扩展,且以 O_RDWR 打开);
- 映射超过文件大小的部分写入时,可能导致文件扩展(取决于文件系统支持)。
- 对齐要求:
offset 必须是系统页大小(通常 4KB)的整数倍,否则 mmap() 失败(EINVAL)。
- 文件描述符关闭:
- 映射建立后,可关闭
fd(映射仍有效),但建议保持打开直至映射解除(便于错误排查)。
- 信号安全:
mmap() 和 munmap() 不是异步信号安全的,避免在信号处理函数中调用。
- 性能陷阱:
- 小文件使用
mmap() 可能因页缓存 overhead 导致性能不如传统 IO;
- 频繁修改小区域时,
MAP_PRIVATE 的写时复制可能带来额外开销。
应用场景
内存映射适用于以下场景:
- 大文件处理:如数据库表文件、日志文件,避免频繁
read()/write() 拷贝;
- 进程间共享内存:通过
MAP_SHARED 或匿名映射实现高效通信(比管道、消息队列快);
- 动态加载代码:如操作系统加载可执行文件到内存执行(映射二进制文件并设置
PROT_EXEC);
- 零拷贝 IO:结合网络编程(如
sendfile()),实现文件数据从页缓存直接发送到网络,避免用户态拷贝