背景

反向映射(rmap)靠区间树快速找到映射了某个文件范围或匿名页的所有 VMA。文件 rmap 树按 VMA 的起始页偏移 pgoff 索引,匿名 rmap 树以 anon_vma_chain 为键、按匿名页偏移索引。树是带 augmented 信息的红黑树,每个节点记 rb_subtree_last 即子树中最大的区间结束偏移,区间查询靠它剪枝。

VMA 的合并、拆分、收缩等更新走统一流程:vma_prepare() 先持锁把 VMA 从两棵 rmap 树摘除,中间修改范围或偏移,vma_complete() 再把它插回去,最后解锁。锁包括 anon_vma 锁与 mapping 侧的锁,全程持有,期间不会有并发的 rmap 遍历;arm、parisc 等架构的 dcache flush 遍历者持有的锁也同样全程覆盖整个操作。

VMA 更新的摘除、修改、重插流程
┌──────────────────────────────────────┐
│ vma_prepare 持锁把 VMA 从 rmap 区间  │
│ 树摘除                               │  锁全程持有,期间没有并发 rmap 遍历
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│       修改 VMA 的范围或页偏移        │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ vma_complete 把 VMA 重新插回区间树再 │
│ 解锁                                 │
└──────────────────────────────────────┘

问题

  • 页偏移未变时摘除再重插纯属无用功,白占 rmap 锁。
  • 收缩、低位拆分、部分合并不改页偏移,多余操作是常态。
  • 摘除重插要两次维护 augmented 字段,加剧 rmap 锁竞争。

方案

节点在区间树里的位置由起始偏移决定,起始偏移不变,位置就不用动。改动给 vma_prepare 新增 file_pgoff_unchanged 与 anon_pgoff_unchanged 两个标志预判偏移是否变化,未变则跳过摘除与重插,改为原地维护。

偏移未变时的原地更新路径
┌────────────────────────────────────┐  shrink 与低位 split 恒不变,merge 逐案比
│ 预判页偏移是否变化,置上 unchanged │  较
│ 标志                               │
└─────────────────┬──────────────────┘
                  ▼
┌────────────────────────────────────┐
│        未变则跳过摘除与重插        │  树中位置由起始偏移决定,未变则位置不动
└─────────────────┬──────────────────┘
                  ▼
┌────────────────────────────────────┐
│    原地把新的区间上界传播到树根    │  在解锁前完成,中间状态对外不可见
└────────────────────────────────────┘

原地维护要修的只有 augmented 字段。收缩等操作会缩小区间结束偏移,rb_subtree_last 可能因此过时。改动把变与不变两种情形收进 mapping_rmap_tree_pre_update()/post_update() 与 anon_rmap_tree_pre_update_vma()/post_update_vma() 两对接口:pre 只在偏移有变时才摘除,post 在 vma_complete() 里范围改完之后、解锁之前处理,未变就把新值从该节点一路传播到树根,变了就重新插入。传播前树上界短暂失真,但锁保证没有并发遍历者能看到中间状态。

if (pgoff_unchanged)
    mapping_rmap_tree_update_inplace(vma);
else
    mapping_rmap_tree_insert(vma, mapping);

标志的设置分三种情形。收缩只会改结束偏移,恒置位。拆分时若原 VMA 是低位那一半,起始偏移不动,也恒置位。向上扩展(expand_upwards())同样不动起始偏移,一并置位。合并时偏移是否变化取决于具体几何,逐案比较新旧起始偏移来置位。匿名一侧以 anon_vma_chain 为键,一个 VMA 可能挂在多棵匿名树上,原地更新沿 anon_vma_chain 链表逐棵进行,调试配置下还要同步各链节点的缓存上界。

收益

UnixBench execl,2 路 192 核 384 线程 x86-64,v7.3-rc4,5 次运行:

指标 Before After 改善
execl 吞吐指数 3511.5 4069.0 +15.9%
file rmap 锁平均等待 9.470 ms 8.420 ms -11.1%

运行期间的 profiling 确认 file rmap 锁是主要竞争点。