lruvec · 2–4 个 generation 的滑动窗口
gen0
最老
gen1
次老
gen2
gen3
最年轻
min_seq
回收只从最老 generation 挑页
aging · 第一步
遍历页表:仍被访问 → gen 标记上调
链表位置不动(惰性提升)
aging · 第二步
inc_max_seq()
创建新 generation 时,窗口已满 4 个
没有空位 → 先退役最老 generation
inc_min_seq() 退役最老 generation
回收未必扫空:剩余 folio 提升一代,搬进次老 generation
min_seq 前进一代,回收继续扫新的最老 generation
全程持有 lruvec 锁
逐 folio 处理
每提升一个 folio 的开销(持锁循环内)
1 × cmpxchg
更新 generation 标记
2 × WRITE_ONCE
nr_pages 一减一加
1 × WRITE_ONCE
protected 计数
1 × 链表摘挂
list_move_tail
倒置一 · 提升路径
目标 generation 的链表 · 从左到右为头到尾
C1
C2
C3
H
已提升 · 更热
未提升的冷页
list_move_tail 追加到尾部
尾部是回收先扫的一端
热页排在冷页之后
回收方向
倒置二 · 从尾到头取,再追加尾部
最老 generation · 从尾到头逐个取出
H
C2
C1
逐个追加
次老 generation · 逐个追加到尾部
C1
C2
H
H 落到尾部
相对冷热顺序反转
旧逻辑
已提升的 folio 仍计入 protected
sort_folio() 的先例
只统计真正提升一代的 folio
计数与实际提升次数脱节
计数批量化 · 每个 zone 写回一次
原来
每个 folio 立刻写回 nr_pages
老 generation −1,新 generation +1
改为
现在
循环内只累计 delta
每个 zone 扫完一次写回
窗口满 4 个 generation 时
gen0
非 active
gen1
非 active
gen2
active
gen3
active
compat 视图只有最年轻两个算 active
最老与次老都不是 active
两者间移动不改变状态
逐 folio 更新整个跳过
同一 zone 的 nr_pages 更新合并成一次
aging 遍历页表时本来就批量更新,inc_min_seq() 沿用同一做法。
inc_min_seq 遍历
最老 gen 的剩余 folio 分两类
aging 中已提升
list_move 到目标 gen 头部
不计 protected
未提升
提升一代
追加到目标 gen 尾部
遍历改为从头到尾,配合向尾部追加
最老 generation
H
C2
C1
相对顺序不变
整段追加到尾部
次老 generation
H
C2
C1
protected 只统计提升一代的 folio;已提升的跳过,与 sort_folio() 一致。
链表搬移批量化:只记区间端点
最老 generation · 连续待搬移的 folio
F1
F2
F3
P
batch_end · 只记这一段
已提升
遍历时先不逐个摘挂,遇到已提升的 folio 再结算这一段。
遇到已提升 folio
先结算攒下的一段
list_cut_position()
切下整段
list_splice_tail_init()
拼到次老 generation 尾部
一次切割、一次拼接,省掉逐 folio 的摘挂。
一个 zone 扫完,剩下的一段照样切拼一次
回收偏好单一类型时,另一类型的 folio 会在最老 generation 大量积压。
连续段越长,省下的摘挂越多。
收益 · aging 耗时对照
512MB 匿名页 memcg(上限 1GB)· debugfs lru_gen 强制 100 次 aging · 单位 ms
指标
Before
After
改善
x86 单次
4.990 ms
2.343 ms
降 53.0%
x86 100 次累计
503.946 ms
236.635 ms
降 53.0%
arm64 PTE(THP=never)
7.644 ms
2.964 ms
降 61.2%
arm64 PMD(THP=always)
0.0373 ms
0.0292 ms
降 21.8%
x86 与 arm64 都下降;PMD 路径本身已极快,改善幅度最小。
arm64 数据由 Xueyuan 在 24 核 4K 页机器上测得。
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x