两棵 rmap 树,索引的键不一样
file rmap 树
键 = VMA 的起始 pgoff,按文件范围索引
anon rmap 树
键 = anon_vma_chain,按匿名页偏移索引
anon_vma_chain 链表,一个 VMA 可能挂在多棵匿名树上
rb_subtree_last:子树里最大的结束偏移,用来剪枝
last
查询 pgoff
last 在查询左边 → 整棵剪掉
区间跨过查询 → 继续往下查
旧流程:先摘除,改完再插回
摘除 → 改范围 / 偏移 → 插回
位置由起始 pgoff 决定
全程持锁:anon_vma 锁 + mapping 侧的锁
摘除 … 改 … 插回,中间不释放
并发的 rmap 遍历
只能等这把锁
期间不会有并发的 rmap 遍历;arm / parisc 的 dcache flush 遍历者也被同一把锁挡住
这几种更新根本不改起始 pgoff
收缩
拆分 · 原 VMA 是低位那一半
expand_upwards()
合并 merge
起始偏移 pin 住 → 树中位置不动
pgoff 不变(只改结束偏移)
pgoff 不变(起始不动)
pgoff 不变(起始不动)
逐案比较新旧起始偏移
旧路径却照样摘、照样插,两次都在锁内
摘除 · 维护 augmented
改范围
插回 · 再维护一次
vma_prepare() 先判一次:pgoff 变了吗
vma_prepare()
new pgoff == old pgoff ?
file_pgoff_unchanged
起始 pgoff 没变 → 树中位置不用动
anon_pgoff_unchanged
匿名页偏移没变 → 匿名树位置也不用动
两处收进 mapping_rmap_tree_pre_update() / post_update() 与 anon_rmap_tree_pre_update_vma() / post_update_vma()
pre 只看要不要摘,post 才真正改树
pgoff_unchanged
pgoff 变了
pre:不摘除
post: update_inplace()
pre:摘除 · post:insert
mapping_rmap_tree_insert(vma, mapping)
原地更新只做一件事:把新上界传到根
树根
父节点
改的就是它
区间上界变了
rb_subtree_last
逐级更新到根
pre 不动树;post 在 vma_complete() 里、解锁之前把新值传播上去
变了才重新插入;没变就只走这一趟
匿名一侧:沿 anon_vma_chain 逐棵原地更新
VMA
每棵匿名树各原地更新一次
chain 节点
一个 VMA 可能挂在多棵匿名树上;调试配置下还要同步各链节点的缓存上界
中间态只在锁里存在
上界暂时偏大
pre:不动树
改范围
post:传播上界
解锁
而遍历者拿的也是这把锁
rmap 锁
rmap walk
区间查询、dcache flush
锁保证并发遍历者看不到这个短暂的偏大上界
UnixBench execl 吞吐指数
Before
3511.5
After
4069.0
+15.9%
file rmap 锁平均等待
Before
9.470 ms
After
8.420 ms
-11.1%
profiling 确认 file rmap 锁就是主要竞争点
2 路 192 核 384 线程 · x86-64 · v7.3-rc4 · 5 次运行
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x