一、内存映射文件:mmap 函数的使用与高性能文件操作

各位同学,今天我们来聊一个让我当年眼前一亮的技术——内存映射文件。说白了,就是通过 mmap 这个函数,把磁盘上的文件直接映射到进程的虚拟地址空间里。你想想看,这样一来,读写文件就跟读写内存一样简单,而且性能还贼高。

我记得刚入行那会儿,接手一个日志分析工具。每次读取几百 MB 的日志,用传统的 fread 要等好几秒,用户都抱怨卡顿。后来我换成 mmap,加载时间直接降到毫秒级。嗯,这就是我们今天要讲的核心——用内存映射实现高性能文件操作

1. 什么是内存映射文件?

传统文件读写,你得先 open,然后 readwrite,数据在内核缓冲区和用户缓冲区之间来回拷贝。而 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 指针就指向了文件内容。你可以用 memcpystrstr 等标准函数直接操作它。

注意: 映射完成后,文件描述符 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 图解)

为什么会这么快?我画了一张图帮你理解:

传统读写 vs mmap 内存映射 传统 read/write 磁盘文件 内核缓冲区 用户缓冲区 ① 读入内核 ② 拷贝到用户 ③ 两次拷贝 mmap 内存映射 磁盘文件 虚拟内存 进程地址空间 ① 建立映射 ② 直接访问 零拷贝! 传统方式:磁盘 → 内核缓冲区 → 用户缓冲区(两次拷贝) mmap:磁盘 ↔ 虚拟内存 ↔ 进程地址空间(缺页时按需加载,无额外拷贝)

从图中可以清楚看到,传统方式需要两次数据拷贝,而 mmap 直接把文件映射到进程地址空间,CPU 只需要处理一次缺页中断。这就是高性能的根源。

6. 避坑指南与最佳实践

最后,我结合踩过的坑,给你几条实用建议:

我曾经犯过的错:

  • 忘记检查 mmap 返回值是否为 MAP_FAILED,结果程序崩溃在奇怪的地方
  • 映射长度超过文件实际大小,访问越界内存导致 SIGBUS
  • 多线程同时写 MAP_SHARED 映射区域,没有加锁,数据出现错乱

我的最佳实践:

  • 映射前用 fstat 获取精确文件大小,并向上对齐到页大小(sysconf(_SC_PAGE_SIZE)
  • 写操作完成后,及时调用 msync 刷盘,避免意外断电丢数据
  • 如果映射区域需要频繁修改,考虑使用 MAP_PRIVATE 做写时拷贝,不影响原文件
  • 用完记得 munmap,否则会造成虚拟内存泄漏

嗯,关于 mmap 的核心用法就讲到这里。你想想看,一个函数就能让文件操作性能翻倍,是不是很值得掌握?下次遇到大文件处理,不妨试试内存映射,你会回来感谢我的。


公众号:蓝海资料掘金营,微信 deep3321