第 49 章 内存映射 (Memory Mappings)

      +

      核心结论

      • mmap() 本质:把文件或匿名内存映射进调用进程的虚拟地址空间——后续像访问普通内存一样访问映射区;页按需从文件加载或按需分配。

      • 四种映射组合:file/anonymous × private/shared = 四种用途——private file (初始化内存)、private anon (malloc 大块)、shared file (mmap I/O + IPC)、shared anon (fork 后父子共享)。

      • mmap/munmap/m syncmmap() 创建;munmap() 解除(exec 自动解除;fork 子进程继承);msync() 显式把 SHARED 映射刷回磁盘(MS_SYNC 阻塞 / MS_ASYNC 后台 / MS_INVALIDATE)。

      • MAP_PRIVATE 靠 COW:多个进程初始共享同一物理页;任一进程写入时内核复制一份新页给它(copy-on-write);写入不持久化到文件、不影响他进程。

      • MAP_ANONYMOUS vs /dev/zero:Linux 两种等价的创建匿名映射方式——MAP_ANONYMOUS (BSD 派) 或 open /dev/zero + mmap (SysV 派);都把字节初始化为 0。

      • mmap I/O 优劣:避免 read/write 的「内核缓冲→用户缓冲」二次拷贝——随机大文件访问有性能优势;顺序小块访问无优势;小 I/O 开销反而更大(page fault + TLB miss)。

      • SIGSEGV vs SIGBUS:访问超出映射范围 → SIGSEGV;映射超出文件大小(且无对应页) → SIGBUS(仅 file mapping);违反 PROT_* 权限 → SIGSEGV。

      • 非线性映射:remap_file_pages() (Linux 特有) 操纵页表把文件页重排——避免为每个非线性段创建独立 VMA;仅适用于 MAP_SHARED。

      本章主旨

      本章是第 50 章「虚拟内存操作」(mprotect/mlock/madvise)和第 54 章「POSIX 共享内存」的前置。mmap() 是 Linux 一切内存映射机制的母调用——文件 I/O、进程间通信、动态库加载、malloc 大块、JIT 代码生成都基于它。读者需要建立四象限思维:(1) 映射有没有 backing file?→ file vs anon;(2) 写入是否可见于他进程?→ shared vs private。掌握这两个维度之后,所有 mmap 衍生 API(shm_open、mremap、remap_file_pages)都变得自然。

      一、核心概念

      本章围绕 6 个核心概念展开:从映射四象限、mmap/munmap、mmap 标志、匿名映射、msync 到非线性映射。

      概念 定义 + 重要性 实现提示

      映射四象限 (file/anonymous × private/shared)

      file/anonymous 决定 backing;private/shared 决定写入可见性;组合得到 private-file(init text/data)、private-anon(malloc 大块)、shared-file(mmap I/O + IPC)、shared-anon(fork 父子共享)

      表 49-1;private 用 COW 实现;shared 写入即时可见且持久到文件

      mmap() / munmap() / 保护位

      mmap(addr, len, prot, flags, fd, offset) 创建;munmap(addr, len) 解除;prot ∈ {PROT_NONE, READ, WRITE, EXEC};flags 必含 MAP_PRIVATE 或 MAP_SHARED

      §49.2;addr=NULL 让内核选(推荐);offset 必须页对齐;length 向上取整到页;SUSv4 放松对齐要求

      prot × flags × open mode 矩阵

      写入映射需要 PROT_WRITE + O_WRONLY/O_RDWR;只读映射允许 O_RDONLY;MAP_PRIVATE + O_RDONLY 可任意 prot(不写回文件)

      §49.4.4;O_WRONLY + mmap 报 EACCES(硬件不允许 write-only 页);PROT_WRITE 隐含 PROT_READ

      mmap I/O 性能模型

      read/write 双缓冲(kernel buf cache + user buf);mmap 单缓冲(kernel 与 user 共享同一物理页);随机大文件访问性能优、顺序小 I/O 无优势

      §49.4.2;性能提升需 msync/sync_file_range 帮助 write-back;mmap 不保证写入时机

      MAP_ANONYMOUS 与 /dev/zero

      两种等价匿名映射创建方式;MAP_ANONYMOUS 来自 BSD(需 _BSD_SOURCE 或 _GNU_SOURCE),/dev/zero 来自 SysV;都把字节初始化 0;MAP_SHARED

      MAP_ANONYMOUS 可让父子共享

      §49.7;MAP_SHARED

      MAP_ANONYMOUS 自 Linux 2.4 才支持;glibc malloc() 用 MAP_PRIVATE anon 实现大块分配(≥ MMAP_THRESHOLD=128KB)

      msync() 与非线性映射

      二、详细笔记

      49.1 映射四象限

      What:mmap 创建的映射有两维度——(1) 是否 backing 文件(file vs anonymous);(2) 写入可见性(private vs shared)。

      Why:四象限对应四种典型用途——理解后能一眼看出 mmap 参数如何选择。

      How

      类型 用途 写入行为

      Private file

      初始化进程 text 段、data 段;加载 .so;mmcat 类似 cat 的简单映射

      写入不持久化到文件,不影响他进程;COW

      Private anonymous

      malloc 大块(≥128KB);guard page;分配零填充内存

      写入私有;fork 后父子各自一份(COW)

      Shared file

      内存映射 I/O;无关进程通过同一文件 IPC

      写入立即可见于他进程;最终持久化到文件

      Shared anonymous

      fork 后父子共享内存;MAP_SHARED

      MAP_ANONYMOUS,-1,0

      When:写私有内存块 → private anon;加载 .so → private file;进程间共享文件数据 → shared file;fork 后父子共享小段内存 → shared anon。

      Example:glibc malloc() 对 ≥ MMAP_THRESHOLD (默认 128 KB) 的分配用 mmap(MAP_PRIVATE|MAP_ANONYMOUS, …​)——大块 free 时直接 munmap() 归还 OS,避免碎片。

      49.2 mmap() / munmap() / 保护位

      Whatmmap(addr, len, prot, flags, fd, offset) 创建新映射;munmap(addr, len) 撤销;prot 是 PROT_NONE/READ/WRITE/EXEC 的位或。

      Why:mmap 是其他一切映射机制的母调用;munmap 对应解除。

      How

      // 摘自《The Linux Programming Interface》 第 49 章
      #include <sys/mman.h>
      
      void *mmap(void *addr, size_t length, int prot, int flags,
                 int fd, off_t offset);
      int   munmap(void *addr, size_t length);
      
      /* mmcat:mmap + write 实现 cat */
      int fd = open(argv[1], O_RDONLY);
      struct stat sb;
      fstat(fd, &sb);
      char *addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
      if (addr == MAP_FAILED) errExit("mmap");
      write(STDOUT_FILENO, addr, sb.st_size);
      munmap(addr, sb.st_size);

      prot 取值(§49.2 表 49-2):

      • PROT_NONE — 区域不可访问。

      • PROT_READ — 可读。

      • PROT_WRITE — 可写。

      • PROT_EXEC — 可执行。

      flags 必含其一:MAP_PRIVATEMAP_SHARED。违反保护位访问 → SIGSEGV。

      When:始终 addr = NULL 让内核选地址(最可移植);offset 必须页对齐(SUSv3;SUSv4 放松);length 自动向上取整到页。

      Example:第 49 章 t_mmap.cmmap(NULL, MEM_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0) 把命令行第二个参数写入共享文件映射——多次运行能累积修改。

      49.3 mmap I/O 与 read/write 性能对比

      What:用 MAP_SHARED 文件映射替代 read/write——程序直接对映射区做指针操作;内核自动把修改传播到 backing file。

      Why:随机大文件访问场景能省掉一次内核↔用户缓冲的拷贝。

      How:数据流对比(§49.4.2):

      操作 read/write 路径 mmap 路径

      输入

      disk → kernel buf cache → user buf → 程序

      disk → 同一物理页(unified VM) → 程序

      输出

      程序 → user buf → kernel buf cache → disk

      程序 → 同一物理页 → kernel 自动刷盘(msync 显式)

      多进程同文件

      每个进程独立 user buf

      所有进程共享同一 kernel 页

      When:mmap 适合——(1) 随机访问大文件;(2) 多进程共享;(3) 把文件当内存结构访问(cast struct)。不适合——(1) 顺序小块 I/O;(2) 小 I/O(page fault + TLB miss 开销大于 read/write);(3) 网络 socket/pipe(不能用 mmap)。

      Example:第 49 章 mmcat.c 把整个文件 mmap 进内存后直接 write(STDOUT, addr, size)——比 read+write 少一次 copy。

      49.4 匿名映射(MAP_ANONYMOUS / /dev/zero)

      What:没有 backing file 的映射——常用于分配进程私有内存或父子共享内存。

      Why:替代 malloc;实现父子进程间零成本共享。

      How

      // 摘自《The Linux Programming Interface》 第 49 章
      /* 方式 1:MAP_ANONYMOUS(BSD 派) */
      addr = mmap(NULL, length, PROT_READ|PROT_WRITE,
                  MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
      
      /* 方式 2:open /dev/zero(SysV 派) */
      int fd = open("/dev/zero", O_RDWR);
      addr = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
      close(fd);
      
      /* 父子共享:MAP_SHARED|MAP_ANONYMOUS + fork */
      addr = mmap(NULL, sizeof(int), PROT_READ|PROT_WRITE,
                  MAP_SHARED|MAP_ANONYMOUS, -1, 0);
      *addr = 1;
      switch (fork()) {
      case 0: (*addr)++; break;           /* child */
      default: wait(NULL); printf("%d\n", *addr); break;  /* parent 看到 2 */
      }

      glibc 用 MAP_PRIVATE|MAP_ANONYMOUS 实现大块 malloc(≥128 KB)——free 时直接 munmap 归还 OS。

      WhenMAP_PRIVATE 用于分配零填充私有内存;MAP_SHARED|MAP_ANONYMOUS 用于 fork 后父子共享。Linux 2.4+ 才支持 MAP_SHARED anon。

      Example:第 49 章 anon_mmap.c 演示两种创建方式(USE_MAP_ANON 宏切换)——父子共享一个 int,子进程 (*addr)++ 后父进程看到 2。

      49.5 msync() 同步控制

      Whatmsync(addr, length, flags) 显式把 MAP_SHARED 映射刷回 backing file,或让文件的他进程写入可见于本进程。

      Why:内核不保证写入时机——msync 给程序显式控制点。

      How

      // 摘自《The Linux Programming Interface》 第 49 章
      #include <sys/mman.h>
      int msync(void *addr, size_t length, int flags);

      flags 三选一:

      • MS_SYNC — 阻塞直到所有修改页写入磁盘。

      • MS_ASYNC — 后台异步刷盘;可调 fsync(fd) 阻塞等完成。

      • MS_INVALIDATE — 让文件的他进程写入失效本地缓存,下次访问从文件重新加载。

      When:(1) 数据库/事务系统需强制持久化 → MS_SYNC;(2) 让他进程 read() 看到本进程写入 → MS_SYNCMS_ASYNC;(3) 让本进程看到他进程 write() 写入文件 → MS_INVALIDATE

      Example:第 49 章 t_mmap.cmemset + strncpy 后调 msync(addr, MEM_SIZE, MS_SYNC)——保证命令行参数真的写盘,下次进程能读到。

      49.6 其他 mmap 标志与非线性映射

      What:除 MAP_PRIVATE/MAP_SHARED 外,Linux 支持 MAP_ANONYMOUS、MAP_FIXED、MAP_LOCKED、MAP_HUGETLB、MAP_NORESERVE、MAP_POPULATE、MAP_UNINITIALIZED;mremap() 调整大小;remap_file_pages() 做非线性映射。

      Why:高级场景——大页(HugeTLB)、预读、稀疏数组、嵌入式优化。

      How

      // 摘自《The Linux Programming Interface》 第 49 章
      #include <sys/mman.h>
      
      /* mremap 调整大小(Linux 特有) */
      void *mremap(void *old_addr, size_t old_size, size_t new_size,
                   int flags, ...);
      #define MREMAP_MAYMOVE  1   /* 允许内核搬迁 */
      #define MREMAP_FIXED    2   /* 必须配合 MREMAP_MAYMOVE */
      
      /* remap_file_pages 非线性(Linux 特有,自 2.6) */
      int remap_file_pages(void *addr, size_t size, int prot,
                           size_t pgoff, int flags);

      MAP_NORESERVE/proc/sys/vm/overcommit_memory 配合——overcommit=0(默认)下,私有可写映射需预留 swap;MAP_NORESERVE 跳过预留(适合稀疏数组)。

      Whenremap_file_pages() 用于数据库、虚拟机(避免为每个 view 创建独立 VMA);mremap() 用于 realloc() 大块 mmap 内存(glibc realloc 内部用 mremap)。

      Example:第 49 章 §49.11 例:mmap(0, 3*ps, …​)remap_file_pages(addr, ps, 0, 2, 0) 把 file page 0 映射到 memory page 2。

      三、关键图表

      非可视化条目(系统调用 / 标志)
      系统调用 / 标志 描述 / 用途

      mmap(addr, len, prot, flags, fd, offset)

      创建内存映射;返回起始地址或 MAP_FAILED

      munmap(addr, len)

      撤销映射;exec 自动撤销;fork 子继承

      msync(addr, len, flags)

      把 MAP_SHARED 映射刷回文件;MS_SYNC/MS_ASYNC/MS_INVALIDATE

      MAP_PRIVATE

      写入私有(COW);不持久化到文件

      MAP_SHARED

      写入可见于共享者;持久化到文件(MAP_SHARED file)

      MAP_ANONYMOUS

      匿名映射;fd 传 -1;字节初始化 0

      MAP_FIXED

      强制 addr 精确解释(要求页对齐)

      MAP_HUGETLB

      使用 huge pages(2.6.32+)

      MAP_LOCKED

      锁页不换出(2.6+)

      MAP_NORESERVE

      不预留 swap

      MAP_POPULATE

      预读;避免后续 page fault

      MAP_UNINITIALIZED

      不清零(嵌入式 + CONFIG_MMAP_ALLOW_UNINITIALIZED)

      PROT_NONE/READ/WRITE/EXEC

      保护位;写入映射需 PROT_WRITE + O_WRONLY/O_RDWR

      mremap()

      调整映射大小;MREMAP_MAYMOVE 允许搬迁

      remap_file_pages()

      非线性映射;仅 MAP_SHARED;Linux 特有

      /proc/PID/maps

      所有映射;每行一个 VMA

      /proc/PID/oom_score

      OOM killer 权重;/proc/PID/oom_adj 调整(-17 免疫)

      四、思维导图

      mindmap
        root((第 49 章 内存映射))
          映射四象限
            file vs anon
            private vs shared
            private file 初始化
            shared file IPC
            shared anon fork 共享
            private anon malloc
          mmap munmap
            addr NULL 推荐
            prot 保护位
            length 页对齐
            exec 自动撤销
            fork 继承
          mmap I/O 性能
            随机大文件优
            顺序小块无优势
            少一次拷贝
            unified VM
          匿名映射
            MAP_ANONYMOUS
            dev zero
            MAP_SHARED anon fork
            glibc malloc
          msync 同步
            MS_SYNC 阻塞
            MS_ASYNC 后台
            MS_INVALIDATE 失效
          高级标志
            MAP_FIXED
            MAP_HUGETLB
            MAP_LOCKED
            MAP_NORESERVE
            mremap
            remap_file_pages

      五、重点与易错点

      1. 「mmap 创建的是虚拟地址映射,不是物理内存」——物理页按需分配;第一次访问触发 page fault 才真正从文件加载或分配。

      2. MAP_PRIVATE 写入靠 COW——多个进程初始共享同一物理页(fork 也共享);任一写入触发内核复制新页;写入不持久化、不影响他进程。

      3. prot 与 open mode 必须匹配——PROT_WRITE + MAP_SHARED 要求 O_RDWRO_WRONLY + mmap 报 EACCES(硬件不允许 write-only 页)。

      4. MAP_SHARED file = mmap I/O + IPC——修改持久化到文件;多进程 mmap 同一文件立即看到互相写入(unified VM 下)。

      5. MAP_SHARED anon 必须 fork 后才共享——同进程多次 mmap SHARED|ANONYMOUS 互不共享(每次调用创建独立映射);只有 fork 继承才共享。

      6. MS_SYNC 阻塞刷盘 vs MS_ASYNC 后台——MS_SYNC 等磁盘写入完成;MS_ASYNC 后台启动,可调 fsync(fd) 阻塞等。

      7. SIGSEGV vs SIGBUS——超出映射区 → SIGSEGV;映射超出文件大小且无可对应页(file mapping) → SIGBUS;违反 PROT_* → SIGSEGV。

      8. mmap 偏移必须页对齐——offset % PAGE_SIZE == 0;SUSv3 严格要求;SUSv4 放松。

      9. mmap 不保证写入时机——必须 msyncfsync 才能保证持久化;否则可能进程崩溃导致数据丢失。

      10. mmap 性能优势场景——随机大文件 + 多进程共享;顺序小 I/O 用 read/write 更简单也更快。

      11. glibc malloc 大块用 mmap——默认阈值 128 KB;mallopt(M_MMAP_THRESHOLD, …​) 调整;大块 free 直接 munmap 避免碎片。

      12. remap_file_pages() 已废弃——Linux 4.0+ 内核仍保留但不再推荐;新代码用 MAP_FIXED 多 mmap 或 fallocate + mmap。

      13. mremap() 仅 Linux——可移植代码不能用;BSD/Solaris 不支持;用 munmap + mmap 替代。

      14. fork 继承映射 + MAP_NORESERVE——子进程继承父进程的 MAP_NORESERVE 设置。

      15. exec 撤销所有映射——exec 后进程地址空间全新;mmap 创建的映射全部消失。

      16. 跨章衔接:第 50 章「虚拟内存操作」讲 mprotect(改保护)/mlock(锁页)/madvise(建议)/posix_madvise;第 54 章「POSIX 共享内存」用 shm_open + mmap 实现无关进程 IPC;第 55 章「文件锁」解决 mmap I/O 的并发写入协调。