C++中如何使用mmap实现高效文件读写?

在传统的文件I/O中,读写操作往往涉及系统调用(如read、write)和用户空间与内核空间之间的数据拷贝。对于大文件或频繁随机访问的场景,这种方式会带来显著的性能瓶颈。C++通过标准库并不直接提供mmap接口,但可以利用 POSIX API 或第三方库轻松实现内存映射文件(memory‑mapped file, mmap),从而让文件内容像普通内存一样访问,减少拷贝次数,提高 I/O 性能。

下面以 POSIX 代码为例,演示如何在 C++ 程序中使用 mmap:

#include <iostream>
#include <fcntl.h>      // open
#include <sys/mman.h>   // mmap, munmap
#include <sys/stat.h>   // fstat
#include <unistd.h>     // close
#include <cstring>      // memcpy

// 简易异常包装
struct MMapError : std::runtime_error {
    MMapError(const std::string &msg) : std::runtime_error(msg) {}
};

class MappedFile {
public:
    MappedFile(const std::string &path, size_t offset = 0, size_t length = 0, bool writable = false)
        : data_(nullptr), length_(0), fd_(-1), writable_(writable)
    {
        fd_ = ::open(path.c_str(), writable ? O_RDWR : O_RDONLY);
        if (fd_ == -1) throw MMapError("open failed");

        // 若 length==0,映射整个文件
        struct stat st;
        if (fstat(fd_, &st) == -1) { ::close(fd_); throw MMapError("fstat failed"); }
        length_ = (length == 0) ? st.st_size : length;

        int prot = writable ? PROT_READ | PROT_WRITE : PROT_READ;
        int flags = MAP_SHARED;  // 共享映射,修改会写回文件

        data_ = static_cast<char*>(::mmap(nullptr, length_, prot, flags, fd_, offset));
        if (data_ == MAP_FAILED) { ::close(fd_); throw MMapError("mmap failed"); }
    }

    ~MappedFile() {
        if (data_) ::munmap(data_, length_);
        if (fd_ != -1) ::close(fd_);
    }

    char* data() const { return data_; }
    size_t size() const { return length_; }

private:
    char *data_;
    size_t length_;
    int fd_;
    bool writable_;
};

// 简单使用示例
int main() {
    try {
        MappedFile mf("sample.txt", 0, 0, true);  // 读写映射整个文件
        std::cout << "文件大小: " << mf.size() << " 字节\n";

        // 修改文件内容(例如将第 0 个字节改为 'A')
        mf.data()[0] = 'A';

        // 直接拷贝一段数据到映射区
        const char *msg = "Hello mmap!";
        std::memcpy(mf.data() + 10, msg, std::strlen(msg));

        // 当对象销毁时,mmap 自动同步修改到文件
        std::cout << "修改已完成,文件内容已同步。\n";
    } catch (const MMapError &e) {
        std::cerr << "错误: " << e.what() << '\n';
        return 1;
    }
    return 0;
}

关键点解析

  1. 文件描述符
    通过 open() 打开文件,获取文件描述符。若需写入,则打开 O_RDWR;若只读则 O_RDONLY

  2. 文件大小
    fstat() 获取文件大小,若在构造时未指定映射长度,则映射整个文件。

  3. 映射属性

    • prot:访问权限。读写需要 PROT_READ | PROT_WRITE;只读仅 PROT_READ
    • flags:映射方式。MAP_SHARED 共享映射,写入会回写文件;MAP_PRIVATE 私有映射,写入不会影响原文件。
  4. offset
    映射文件的偏移位置。若想从文件中间开始映射,可指定非零 offset。注意偏移值必须是页面大小(通常 4k)的整数倍,否则会导致 mmap 失败。

  5. 同步与资源释放
    munmap() 负责释放映射区。若使用 MAP_SHARED,在 munmap 时系统会把修改同步回文件。若使用 MAP_PRIVATE,则不会同步,除非手动 msync()

  6. 异常安全
    采用 RAII 封装文件描述符和映射区,确保异常或正常退出时资源正确释放。

性能对比

场景 传统 read/write mmap
大文件顺序读取 需要多次系统调用 + 复制 仅一次系统调用,直接在内核页缓存中读取
随机访问 每次定位 + 读写 通过指针直接访问映射区
写入 write() 需要复制 直接修改映射区即可,写回自动完成

实际测试表明,在读取多百 MB 文件时,使用 mmap 可以将 I/O 延迟压缩到 30% 左右,并减少 CPU 使用率。

进阶技巧

  • 延迟映射:使用 MAP_POPULATE(Linux)可以在映射时一次性把页调入内存,避免后续延迟页缺失。
  • 异步 I/O:结合 O_DIRECTmmap 可以实现无缓存的高效 I/O。
  • 跨平台:Windows 提供 CreateFileMapping + MapViewOfFile,语义类似但 API 不同。
  • 对齐与页大小:使用 sysconf(_SC_PAGE_SIZE) 获取系统页大小,确保 offset 与长度满足对齐要求。

小结

mmap 让文件内容与进程地址空间耦合,从而将文件 I/O 变成了普通内存访问。它在大文件处理、数据库、图像处理等场景中具有显著优势。只需注意映射大小、访问权限、偏移对齐以及异常安全,即可在 C++ 程序中安全、轻松地使用 mmap。

发表评论