在传统的文件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;
}
关键点解析
-
文件描述符
通过open()打开文件,获取文件描述符。若需写入,则打开O_RDWR;若只读则O_RDONLY。 -
文件大小
用fstat()获取文件大小,若在构造时未指定映射长度,则映射整个文件。 -
映射属性
prot:访问权限。读写需要PROT_READ | PROT_WRITE;只读仅PROT_READ。flags:映射方式。MAP_SHARED共享映射,写入会回写文件;MAP_PRIVATE私有映射,写入不会影响原文件。
-
offset
映射文件的偏移位置。若想从文件中间开始映射,可指定非零 offset。注意偏移值必须是页面大小(通常 4k)的整数倍,否则会导致mmap失败。 -
同步与资源释放
munmap()负责释放映射区。若使用MAP_SHARED,在munmap时系统会把修改同步回文件。若使用MAP_PRIVATE,则不会同步,除非手动msync()。 -
异常安全
采用 RAII 封装文件描述符和映射区,确保异常或正常退出时资源正确释放。
性能对比
| 场景 | 传统 read/write | mmap |
|---|---|---|
| 大文件顺序读取 | 需要多次系统调用 + 复制 | 仅一次系统调用,直接在内核页缓存中读取 |
| 随机访问 | 每次定位 + 读写 | 通过指针直接访问映射区 |
| 写入 | write() 需要复制 |
直接修改映射区即可,写回自动完成 |
实际测试表明,在读取多百 MB 文件时,使用 mmap 可以将 I/O 延迟压缩到 30% 左右,并减少 CPU 使用率。
进阶技巧
- 延迟映射:使用
MAP_POPULATE(Linux)可以在映射时一次性把页调入内存,避免后续延迟页缺失。 - 异步 I/O:结合
O_DIRECT与mmap可以实现无缓存的高效 I/O。 - 跨平台:Windows 提供
CreateFileMapping+MapViewOfFile,语义类似但 API 不同。 - 对齐与页大小:使用
sysconf(_SC_PAGE_SIZE)获取系统页大小,确保 offset 与长度满足对齐要求。
小结
mmap 让文件内容与进程地址空间耦合,从而将文件 I/O 变成了普通内存访问。它在大文件处理、数据库、图像处理等场景中具有显著优势。只需注意映射大小、访问权限、偏移对齐以及异常安全,即可在 C++ 程序中安全、轻松地使用 mmap。