背景

内核语境里的「匿名」内存其实有四种语义:

  • 纯匿名映射:匿名 VMA 与匿名 folio,swap 支撑,回收按匿名对待,语义最简单。
  • shmem:文件 VMA 与文件 folio,挂在 page cache,但由 swap 支撑,回收按匿名对待。
  • MAP_PRIVATE 映射的 /dev/zero:vm_file 挂着但 vm_ops 被清空,恰好满足 vma_is_anonymous(),其余方面都按匿名对待。
  • 其他 MAP_PRIVATE 文件映射:文件 VMA 与文件 folio,CoW 之后产生的 folio 是匿名。

本系列解决的是最后一类。

rmap 靠 folio->mapping 与 folio->index 两个字段反查 VMA:前者决定去哪棵树上找,匿名 folio 指向 anon_vma、文件 folio 指向 address_space;后者说明 folio 在对象内的起始偏移。文件 folio 的 index 就是文件页偏移;纯匿名 folio 的 index 是匿名页偏移,即首次 fault 时的虚拟页号(匿名 VMA 的 vm_pgoff 初始化就是 vm_start >> PAGE_SHIFT)。而 MAP_PRIVATE 文件映射 CoW 出的匿名 folio,mapping 已指向 anon_vma,index 却仍按文件页偏移写入,与纯匿名是两套坐标。

匿名 folio 的 rmap 索引现状
                       ┌──────────────────────────────┐
                       │ 匿名 folio 写入 folio->index │
                       └──────────────┬───────────────┘
                 ┌────────────────────┴────────────────────┐
                 ▼                                         ▼
┌──────────────────────────────────┐      ┌──────────────────────────────────┐
│ 纯匿名 VMA 的 folio 按匿名页偏移 │      │ MAP_PRIVATE 文件映射的 CoW folio │
│ 与虚拟地址同坐标                 │      │ 按文件页偏移,与虚拟地址两套坐标 │
└──────────────────────────────────┘      └──────────────────────────────────┘

这个不一致长期无害,但 scalable CoW(进行中的后续工作)会被它卡死:其快路径按 folio->index << PAGE_SHIFT 在 maple tree 里直接找 VMA,找不到再退回 remap 追踪。文件页偏移与 VMA 地址不是一个坐标系,由 index 换算出的地址经常落到别的 VMA 上,快路径对这类 folio 永远不可用,只能全部挤到 remap 追踪上,争用严重。

问题

  • CoW 出的匿名 folio 按文件页偏移建立索引,与纯匿名的虚拟页号两套坐标。
  • scalable CoW 快路径按 folio->index 直接找 VMA,坐标不符则永远失效。
  • 快路径用不了的 folio 全挤到 remap 追踪,争用严重。

方案

给 VMA 增加 anonymous page offset(匿名页偏移)属性,让 MAP_PRIVATE 文件映射 CoW 出的匿名 folio 按照匿名页偏移建立 rmap 索引,与纯匿名对齐。

新属性存放在 vm_area_struct 的新字段里,64 位下拆成高低两半打包进 VMA 现有 cacheline,不占额外空间。取值规则:

  • 初始为 vm_start >> PAGE_SHIFT,与纯匿名 VMA 的 vm_pgoff 一致。
  • remap 移动 VMA 时,未 fault 的更新为新地址页号;已 fault(vma->anon_vma 非空)的保持首次 fault 时的值,因为已有 folio 按旧值建立了索引。

底层铺好后,切换就是把写索引与读索引的入口换坐标系:__folio_set_anon() 改用 linear_anon_page_index() 给 CoW folio 写入匿名页偏移;vma_address() 按用途拆成 vma_filebacked_address() 与 vma_anon_address();page_vma_mapped_walk 增加 pgoff_is_anon 标记,vma_address_end() 遇到匿名 pgoff 时按 VMA 的匿名页偏移计算。

对 merge 的影响被限制在边角:纯匿名 VMA 的两个偏移恒等、共享文件映射仍只按文件偏移,二者的合并行为不变;MAP_PRIVATE 文件映射则要求匿名页偏移与文件页偏移同时对齐才能合并,要先 CoW、再找到一个文件相同且偏移兼容的 VMA 挪到旁边才会合并失败,属极偏的边角案例。

坐标统一之后,scalable CoW 的快路径对这类 CoW folio 也能命中。

索引统一后 scalable CoW 快路径
┌────────────────────────────────────┐
│  CoW folio 的 index 存匿名页偏移   │
└─────────────────┬──────────────────┘
                  ▼
┌────────────────────────────────────┐
│ index 左移 PAGE_SHIFT 还原虚拟地址 │
└─────────────────┬──────────────────┘
                  ▼
┌────────────────────────────────────┐
│   maple tree 按地址直接命中 VMA    │  找不到才退回 remap 追踪
└────────────────────────────────────┘

收益

作者未提供性能数据,从代码逻辑推断的预期收益:

  • CoW folio 的 rmap 索引与纯匿名统一坐标,scalable CoW 快路径对其可用。
  • 未来 remap 时可 unshare 深层 fork 层次的匿名 folio,绝大多数情形不再需要 remap 追踪。
  • 新字段打包进 VMA 现有 cacheline,64 位下不占额外空间。