跳转至

mmap

是什么 / 解决什么问题

mmap 将文件(或匿名内存)映射到进程的虚拟地址空间。读写映射区域直接操作 page cache,跳过 read/write 系统调用的数据拷贝。

使用模式

文件映射

#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int fd = open("data.bin", O_RDWR);
struct stat st;
fstat(fd, &st);

// 映射整个文件
void *addr = mmap(NULL, st.st_size,
                  PROT_READ | PROT_WRITE,  // 权限
                  MAP_SHARED,               // 共享映射
                  fd, 0);                   // 文件 fd 和偏移

// 直接当内存用
char *data = (char *)addr;
printf("first byte: %c\n", data[0]);
data[100] = 'x';  // 直接修改文件内容

// 解除映射
munmap(addr, st.st_size);
close(fd);  // close 后映射仍然有效

私有映射 vs 共享映射

// MAP_SHARED: 修改会写回文件,其他映射该文件的进程可见
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, 0);

// MAP_PRIVATE: COW (Copy-on-Write)
// 修改只对本进程可见,不影响文件
void *private = mmap(NULL, size, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE, fd, 0);

匿名映射(不关联文件)

// 分配大块内存(malloc 底层对大块分配用的就是这个)
void *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
                 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// fd = -1, 不关联文件

// 释放
munmap(mem, size);

进程间共享内存

// 方法1: MAP_SHARED + 文件
int fd = open("/tmp/shared", O_RDWR | O_CREAT, 0644);
ftruncate(fd, size);
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

// 方法2: MAP_SHARED | MAP_ANONYMOUS(只能父子进程间)
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE,
                    MAP_SHARED | MAP_ANONYMOUS, -1, 0);
pid_t pid = fork();  // 子进程继承映射

// 方法3: shm_open + mmap(推荐)
int fd = shm_open("/myshm", O_RDWR | O_CREAT, 0644);
ftruncate(fd, size);
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

行为与语义

延迟加载(demand paging)

mmap() 返回时,内存页并未实际分配!
只是建立了虚拟地址空间到文件的映射关系。

首次访问某页时:
  → 触发 page fault
  → 内核从磁盘读取该页到 page cache
  → 建立页表映射
  → 程序继续执行(对程序透明)

这意味着 mmap 一个 10GB 的文件是立即返回的,实际内存按需分配。

MAP_SHARED 的写入行为

写入 MAP_SHARED 区域:
  → 直接修改 page cache 中的页
  → 页被标记为 dirty
  → 后台 writeback 线程定期刷盘
  → 或者调用 msync() 手动刷盘

// 显式刷盘
msync(addr, length, MS_SYNC);   // 同步,等落盘
msync(addr, length, MS_ASYNC);  // 异步,只标记 dirty

mmap vs read/write

维度 read/write mmap
数据拷贝 2次(磁盘→page cache→用户buf) 1次(磁盘→page cache,用户直接访问)
syscall 每次操作一个 syscall 映射后无 syscall(通过 page fault)
随机访问 需要 lseek 直接用指针
小文件 性能差异不大 mmap 的 setup 开销相对大
大文件顺序读 read + readahead 更优 page fault 开销大
大文件随机读 频繁 syscall 更优(无 syscall 开销)

mmap 的限制

  • 映射大小受虚拟地址空间限制(64位系统几乎无限)
  • 文件扩大后需要重新 mmap(或用 mremap
  • SIGBUS:访问超出文件实际大小的映射区域会收到 SIGBUS 信号
  • 不适合流式数据(socket/pipe 不能 mmap)

性能考量

预读优化

// 告诉内核访问模式
madvise(addr, length, MADV_SEQUENTIAL);  // 顺序访问,增大预读
madvise(addr, length, MADV_RANDOM);      // 随机访问,关闭预读
madvise(addr, length, MADV_WILLNEED);    // 即将访问,提前加载

// 预热:强制加载到内存
madvise(addr, length, MADV_POPULATE_READ);  // 5.14+ 一次性缺页

大页 (Huge Pages)

// 使用透明大页(2MB 页代替 4KB 页)
// 减少 TLB miss,提升大内存访问性能
madvise(addr, length, MADV_HUGEPAGE);

// 或显式使用 hugetlbfs
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                  MAP_SHARED | MAP_HUGETLB, -1, 0);

何时用 mmap

适合: - 大文件随机访问(数据库文件、索引文件) - 多进程共享内存 - 只读映射配置文件/资源文件 - 内存映射 I/O(嵌入式设备寄存器)

不适合: - 小文件(mmap 的 setup/teardown 开销不划算) - 顺序扫描大文件(read + fadvise 更优) - 需要精确控制 I/O 时机(mmap 的 page fault 时机不可预测) - 文件大小频繁变化

常见陷阱与 FAQ

1. SIGBUS 信号

// 文件被其他进程 truncate 缩小后,访问超出范围的映射区域
// → SIGBUS(不是 SIGSEGV)

// 预防:
// - 用 flock 保护
// - 处理 SIGBUS 信号

2. close(fd) 后映射仍有效

void *addr = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);  // 没问题!映射不依赖 fd
// addr 仍然可用,直到 munmap

3. MAP_PRIVATE 的写时复制开销

首次写入 MAP_PRIVATE 映射页时触发 COW:分配新物理页 + 拷贝内容。大范围写入时可能不如直接 read 到 buffer 修改。

4. 内存映射文件的一致性

MAP_SHARED 写入 ≠ 立即落盘。其他进程的 MAP_SHARED 可以看到修改(因为共享 page cache),但 fsync/msync 前可能丢失。

观测与调试

# 查看进程的内存映射
cat /proc/<pid>/maps
# 或 pmap -x <pid>

# 查看文件在 page cache 中的驻留情况
vmtouch /path/to/file

# 查看 page fault 统计
ps -o pid,min_flt,maj_flt -p <pid>
# min_flt: 次缺页(page cache 命中)
# maj_flt: 主缺页(需要磁盘 I/O)

# perf 统计 page fault
perf stat -e page-faults,major-faults ./app

延伸阅读

  • man 2 mmap — 完整参数说明
  • man 2 madvise — 访问模式提示
  • man 2 msync — 刷盘控制
  • man 2 mremap — 调整映射大小