TL;DR

统一封装 mmap 与 munmap 等平台接口,管理 ART 进程中的映射区域与元数据;在 64 位但不支持 MAP_32BIT 的平台上,提供低 4GB 线性扫描分配器,满足 JIT 与指针压缩等对 32bit 可寻址区域的要求。

mmap 基础速览

  • 文件映射:将文件页映射到进程虚拟地址空间。
  • 匿名映射:不关联文件(如大块 malloc 背后常用)。
  • 访问:首次触发缺页,内核按需填充页表。
  • 写回:MAP_SHARED 可写回文件;MAP_PRIVATE 写时复制 COW。
  • 解除与同步:munmap 解除映射;msync 同步页缓存到后端。
  • 安全页判定(Linux):对未映射页调用 msync 会立刻返回 ENOMEM,可用来判定一段地址确实空白。低 4GB 分配器在 Linux 上用此技巧确认可用区间。

AOSP 的 MemMap 封装层

// 统一出口(Unix Linux Fuchsia Windows 各有实现)
static void* TargetMMap(void* start, size_t len, int prot, int flags, int fd, off_t off);
static int   TargetMUnmap(void* start, size_t len);
  • Unix / Linux:直接转调 mmap 与 munmap。
  • Windows:翻译为 CreateFileMapping 与 MapViewOfFile,限制较多(不支持 MAP_FIXED,PROT_EXEC 组合受限)。
  • Fuchsia:使用 VMAR/VMO 机制封装底层映射,通常会在低地址段预留“低内存区域”,在其上做匿名映射;文件映射仍经由统一的 TargetMMap 接口。

适配图:

flowchart LR
  A[MemMap API] --> B[Unix Linux 适配 mmap 与 munmap]
  A --> C[Windows 适配 CreateFileMapping 与 MapViewOfFile]
  A --> D[Fuchsia 适配 低地址 VMAR/VMO 与 mmap]

平台差异与宏

  • USE_ART_LOW_4G_ALLOCATOR

    • 在 LP64 且非 Fuchsia 的 aarch64 / riscv / Apple 平台启用低 4GB 线性扫描分配器。
    • x86_64 上可以直接依赖内核 MAP_32BIT 支持低地址映射。
  • kMadviseZeroes 与 HAVE_MREMAP_SYSCALL(Linux)

    • kMadviseZeroes 为真时,madvise 的 DONTNEED(等)可以在逻辑内容不变的前提下,让内核丢弃驻留页、下次访问再按需拉回。
    • HAVE_MREMAP_SYSCALL 为真时,mremap 支持做原子替换映射(用于 ReplaceWith)。

成员布局与生命周期

  std::string name_;
  uint8_t* begin_ = nullptr;   // 用户可用起点 可能不是页对齐
  size_t size_ = 0;            // 用户可用长度

  void*  base_begin_ = nullptr;// 页对齐起点
  size_t base_size_  = 0;      // 页对齐长度
  int    prot_ = 0;
  bool   reuse_ = false;       // 仅视图 不负责 unmap
  bool   already_unmapped_ = false;
  size_t redzone_size_ = 0;    // Sanitizer redzone
  ...
  • IsValid 近似等价于 base_size_ 不为零。
  • 析构与 Reset 负责 munmap(除非 reuse 视图或 already_unmapped)。
  • 对齐与切分:有 AlignBy、RemapAtEnd、TakeReservedMemory 等工具方法。

匿名映射 MapAnonymous

这是 ART 中最常走的入口,兼顾提示地址、低 4GB 与预留或复用的多分支。

关键点:

  • 复用 / 预留

    • 复用已有 MemMap 区间时,将标志加入 MAP_FIXED,让新映射强制覆盖在原地址上。
    • 使用预留(reservation)时,同样加入 MAP_FIXED,并在成功后把预留对象释放、移交所有权。
  • 匿名映射

    • 默认使用 MAP_PRIVATE MAP_ANONYMOUS。
    • 若调用方设置了 MAP_FIXED,则完全按调用方要求执行,失败直接返回。
  • 低 4GB

    • 若启用线性扫描分配器且未指定地址(USE_ART_LOW_4G_ALLOCATOR && low_4gb && addr == nullptr),先以去掉执行位的 prot 通过 MapInternalArtLow4GBAllocator 探测映射,成功后再 mprotect 加回执行位。
    • 否则在 x86_64 上,若需要 low_4gb 且未指定地址,则给 flags 加上 MAP_32BIT,交给内核选择低 2GB/4GB 范围的地址。

流程图:

flowchart TD
  A[MapAnonymous 入口] --> B[校验 size 大于零 并向页对齐]
  B --> C[是否复用或使用预留]
  C -- 复用 --> C1[检查在既有映射内 标志加入 MAP_FIXED]
  C -- 预留 --> C2[检查预留覆盖 标志加入 MAP_FIXED]
  C -- 否 --> C3[标志使用 MAP_PRIVATE 和 MAP_ANONYMOUS]

  C3 --> D[进入 MapInternal<br/>根据 low_4gb / 平台选择 mmap 路径]
  C1 --> D
  C2 --> D

  D --> F[获得实际地址 actual]
  F --> G[actual 是否失败]
  G -- 是 --> G1[记录错误并返回 Invalid]
  G -- 否 --> H[提示地址 addr 非空 且 非 MAP_FIXED]
  H -- 是 --> H1[检查 actual!=addr 则 munmap 并报错]
  H -- 否 --> I[设备端设置匿名 VMA 名称]
  H1 -->|失败| G1
  I --> J[是否来自预留]
  J -- 是 --> J1[释放预留并移交所有权]
  J -- 否 --> K[返回 MemMap]

MapInternal 内部逻辑(精简视图)

flowchart TD
    S1a[LP64 且 low4gb 且 指定地址超出低4GB范围]
    S1a -- 是 --> S1err[直接返回失败]
    S1a -- 否 --> S1b[USE_ART_LOW_4G_ALLOCATOR 且 未指定地址 且 low4gb]
    S1b -- 是 --> S1c[临时去除执行位 使用低4GB线性扫描]
    S1c -->|失败| S1fail[失败返回]
    S1c -->|成功| S1d[是否需要执行位]
    S1d -- 是 --> S1e[mprotect 恢复执行位]
    S1d -- 否 --> S1ok[返回地址]
    S1b -- 否 --> S1f[x86_64 支持 MAP_32BIT 且 low4gb 且 addr 为 nullptr]
    S1f -- 是 --> S1g[flags 加 MAP_32BIT 后调用 mmap]
    S1f -- 否 --> S1h[直接调用 mmap 使用 flags]

低 4GB 线性扫描分配器

从 low_4gb 请求到分配器入口

当调用方传入 low_4gb = true 时,整体从 API 到低 4GB 扫描分配器的大致路径如下(仅 LP64 场景):

flowchart TD
    Call[调用 MemMap::MapAnonymous / MapFileAtAddress<br/>参数 low_4gb = true] --> MInternal[进入 MemMap::MapInternal]

    MInternal --> C1{是否 LP64}
    C1 -- 否 --> Path32[32 位进程 本身地址空间<br/>已经在 4GB 内 直接 mmap 返回]
    C1 -- 是 --> CCheck[如果 addr 非空<br/>检查 addr 和 addr+length 是否都在低 4GB]
    CCheck --> CCheckFail{检查失败?}
    CCheckFail -- 是 --> FailRange[打印错误<br/>返回 MAP_FAILED]
    CCheckFail -- 否 --> C2{USE_ART_LOW_4G_ALLOCATOR 为 1?}

    C2 -- 否 --> X86Path[x86_64 等平台<br/>flags 加 MAP_32BIT(如有)<br/>然后直接 mmap]
    C2 -- 是 --> C3[线性扫描逻辑]

ART_LOW_4G_ALLOCATOR 线性扫描逻辑

flowchart TD
    C2[开始]  -->  C3{addr 是否为 nullptr}

    C3 -- 否 --> DirectMmap["直接 mmap(addr, ...)<br/>caller 自己保证低 4GB"] --> ReturnDirect[返回结果或失败]
    C3 -- 是 --> Low4GBAlloc[进入 ART 低 4GB 分配器<br/>使用 next_mem_pos_ 线性扫描]

    Low4GBAlloc --> Scan1[第一轮 利用 MemMap::maps_<br/>跳过已知映射 找空洞 调用 TryMemMapLow4GB]
    Scan1 --> R1{mmap 成功且在低 4GB?}
    R1 -- 是 --> OK1[更新 next_mem_pos_ = actual + length<br/>返回指针]
    R1 -- 否 --> SpaceCheck{4GB 内剩余空间是否不足 length?}

    FailNoSpace[没有足够连续空间 ENOMEM]

    SpaceCheck -- 是 --> FirstRun{当前是否 first_run?}
    FirstRun -- 否 --> FailNoSpace
    FirstRun -- 是 --> Restart[ptr 从 LOW_MEM_START 重新扫描<br/>second run] --> Scan1

    SpaceCheck -- 否 --> Scan2[第二轮(Linux): 用 msync 探测真实空洞<br/>找到全 ENOMEM 区域再试 TryMemMapLow4GB]
    Scan2 --> Done2[成功映射则返回 否则继续扫描直到 4GB 边界]

仅在启用低 4GB 扫描、addr 为空且 low4gb 为真时走。 目标是在 4GB 以下找到连续空闲页:使用随机化起点、用 gMaps 跳过已占区间,并在 Linux 上通过对每页 msync 期待 ENOMEM 的方式确认这一段没有其他匿名/文件映射。


文件映射与对齐

对齐规则

  • 起始偏移向下页对齐;
  • 实际映射长度加上页内偏移后再向上页对齐;
  • 最终 begin 等于 base_begin 加上页内偏移,size 为用户可见长度。
flowchart LR
  A[文件偏移 start] --> B[计算 base_off 向下到页边界]
  B --> C[以 base_off 调用 mmap 获得 base_begin]
  C --> D[begin 等于 base_begin 加上 start 减去 base_off]
  C --> E[size 等于用户长度]
  D --> F[得到用户可见区间]

提示地址检查

  • 若传入 addr 但未使用 MAP_FIXED,成功后需检查实际 begin 是否等于 addr,否则立刻 munmap 并报错。

保护、同步与回收

  • Protect:调用 mprotect,更新 prot_。
  • Sync:调用 msync(如 Linux 上使用 MS_SYNC),用于持久化或失效缓存。
  • ZeroMemory / MadviseZero(Linux 上的典型行为):

    • 对齐到页边界后,对目标区间调用 madvise(…, MADV_DONTNEED)。
    • 内核可以丢弃驻留物理页,逻辑内容仍被视为零页,会在后续访问时按需重新拉入。
flowchart TD
  Z0[MemMap 区间] --> Z1[按页对齐待回收区间]
  Z1 --> Z2["madvise(..., MADV_DONTNEED)"]
  Z2 --> Z3[逻辑视角不变 下次访问触发缺页<br/>物理可被回收与清零]

某些 GC 空间实现可能在此基础上使用 mincore 聚合驻留页,以便只对“实际驻留的页面”调用 madvise;这一层属于上层策略,MemMap 自身只提供基础接口。


原子替换映射 ReplaceWith

仅 Linux 支持,依赖 mremap。 前置条件:

  • 源与目标不重叠,且都拥有真实映射(非 reuse 视图)。
  • redzone、页对齐、页内偏移等参数一致。
  • 目标区间足以容纳源区间。
sequenceDiagram
  participant Src as Source MemMap
  participant Dst as Dest MemMap
  participant OS as Kernel mremap

  Src->>OS: mremap(Source, Dst.base_begin, MREMAP_FIXED)
  OS-->>Src: 返回结果
  alt 成功
    Src->>Dst: 转移所有权 Source 失效
  else 失败
    Src->>Src: 保持不变 返回错误
  end