一、内存映射文件:mmap 函数的使用与高性能文件操作
各位同学,今天我们来聊一个让我当年眼前一亮的技术——内存映射文件。说白了,就是通过 mmap 这个函数,把磁盘上的文件直接映射到进程的虚拟地址空间里。你想想看,这样一来,读写文件就跟读写内存一样简单,而且性能还贼高。
我记得刚入行那会儿,接手一个日志分析工具。每次读取几百 MB 的日志,用传统的 fread 要等好几秒,用户都抱怨卡顿。后来我换成 mmap,加载时间直接降到毫秒级。嗯,这就是我们今天要讲的核心——用内存映射实现高性能文件操作。
1. 什么是内存映射文件?
传统文件读写,你得先 open,然后 read 或 write,数据在内核缓冲区和用户缓冲区之间来回拷贝。而 mmap 呢?它直接把文件内容映射到进程的地址空间,应用程序可以像访问数组一样访问文件数据。
核心优势:
- 省去了一次数据拷贝(内核态到用户态)
- 按需分页加载,不需要一次性读入全部数据
- 多个进程可以共享同一份映射,节省内存
我在项目中遇到过一个大文件解析场景,文件有 2GB 以上。如果用 fread 全部读到内存,机器直接卡死。换成 mmap 后,系统只加载实际访问的页面,内存占用极低。这就是按需分页的魅力。
2. mmap 函数原型与参数
先看函数签名,我习惯把它记成「五参数一返回值」:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
每个参数都有讲究,我一个个说:
| 参数 | 含义 | 我的建议 |
|---|---|---|
addr |
映射起始地址,通常传 NULL 让内核选 | 传 NULL 最省心,别自己瞎指定 |
length |
映射的字节数 | 一般传文件大小,注意对齐到页大小 |
prot |
保护标志:PROT_READ / PROT_WRITE 等 | 读写就传 PROT_READ | PROT_WRITE |
flags |
映射类型:MAP_SHARED / MAP_PRIVATE | 共享文件用 MAP_SHARED,私有用 MAP_PRIVATE |
fd |
文件描述符 | 必须提前 open 打开 |
offset |
文件偏移量,必须是页大小的整数倍 | 通常传 0,从文件头开始 |
小技巧: 我个人习惯在 mmap 之前先调用 fstat 获取文件大小,然后传给 length。这样能保证映射整个文件。
3. 代码示例:用 mmap 读取文件
光说不练假把式,直接上代码。这是一个完整的例子,把文件内容映射到内存,然后打印出来:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
const char *filepath = "example.txt";
int fd = open(filepath, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 获取文件大小
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
exit(EXIT_FAILURE);
}
size_t length = sb.st_size;
// 映射文件
void *mapped = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
// 直接像内存一样访问
printf("文件内容:\n%.*s\n", (int)length, (char *)mapped);
// 清理
munmap(mapped, length);
close(fd);
return 0;
}
你看,代码很简洁。映射完成后,mapped 指针就指向了文件内容。你可以用 memcpy、strstr 等标准函数直接操作它。
注意: 映射完成后,文件描述符 fd 可以立即关闭,不影响映射的使用。但 munmap 之前不要关闭映射区域,否则会出段错误。
4. 高性能写入:MAP_SHARED 与 msync
如果要对文件进行写入,需要把 prot 加上 PROT_WRITE,并且 flags 使用 MAP_SHARED。这样对映射内存的修改会自动写回文件。
我曾经写一个数据库的缓存模块,用 mmap 做持久化。数据直接写到映射内存,然后定期调用 msync 刷盘。性能比 fwrite 提升了 3 倍以上。
// 写入示例
int fd = open("data.bin", O_RDWR | O_CREAT, 0644);
// 先设置文件大小
ftruncate(fd, 1024 * 1024); // 1MB
void *map = mmap(NULL, 1024 * 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 写入数据
sprintf((char *)map, "Hello, mmap!");
// 强制同步到磁盘
msync(map, 1024 * 1024, MS_SYNC);
关键点:
ftruncate用来扩展文件大小,否则映射区域可能超出文件实际大小msync的三个标志:MS_ASYNC(异步)、MS_SYNC(同步)、MS_INVALIDATE(使缓存失效)- 我建议在关键数据写入后使用
MS_SYNC,确保数据落盘
5. 内存映射的底层原理(SVG 图解)
为什么会这么快?我画了一张图帮你理解:
从图中可以清楚看到,传统方式需要两次数据拷贝,而 mmap 直接把文件映射到进程地址空间,CPU 只需要处理一次缺页中断。这就是高性能的根源。
6. 避坑指南与最佳实践
最后,我结合踩过的坑,给你几条实用建议:
我曾经犯过的错:
- 忘记检查
mmap返回值是否为MAP_FAILED,结果程序崩溃在奇怪的地方 - 映射长度超过文件实际大小,访问越界内存导致 SIGBUS
- 多线程同时写
MAP_SHARED映射区域,没有加锁,数据出现错乱
我的最佳实践:
- 映射前用
fstat获取精确文件大小,并向上对齐到页大小(sysconf(_SC_PAGE_SIZE)) - 写操作完成后,及时调用
msync刷盘,避免意外断电丢数据 - 如果映射区域需要频繁修改,考虑使用
MAP_PRIVATE做写时拷贝,不影响原文件 - 用完记得
munmap,否则会造成虚拟内存泄漏
嗯,关于 mmap 的核心用法就讲到这里。你想想看,一个函数就能让文件操作性能翻倍,是不是很值得掌握?下次遇到大文件处理,不妨试试内存映射,你会回来感谢我的。