缺页 do_swap_page
anon / shmem 换入
SWP_SYNCHRONOUS_IO?
ZRAM 等同步设备
旧:可绕过 swap cache
sync && count == 1
旧门控:count == 1 才绕
if (data_race(si->flags & SWP_SYNCHRONOUS_IO) &&
__swap_count(entry) == 1) {
/* skip swapcache */
count > 1 的共享页:仍走 readahead
想禁预读
同步设备本来就不该读整窗
想启用大块 swapin
大 folio 一次换入更省
历史优化把三件事绑在一起:绕 cache、禁预读、大块 swapin
swap count 检查让绕过在多数共享页上失效,三个收益都拿不到
旧同步:swap_map 的 SWAP_HAS_CACHE
第一格 0x40
0x40
count
count
count
count
count
谁先置位,谁负责实际换入
竞争者看到什么
位已置位,但不知道持有者
swapcache_prepare() 竞争时返回 -EEXIST
只能 schedule_timeout_uninterruptible(1)
睡一拍再重试,延迟不可控
两个 CPU 的竞争
A
置位 0x40
做 I/O
清位
B
看到 0x40
睡 1 tick
重试
B 的等待时间由 A 决定
没有对象可等,只能定时器轮询
长尾集中在共享 swap entry
BGSAVE / COW 产生大量 count > 1 的 entry,恰是位锁最难受的场景
根因:位锁不是对象锁——失败者不知道谁持有,只能睡一拍
phase I:swap table 接管 swap cache
旧:swap_map + XArray
查找 / 插入 cache 要碰 XArray
开销可见,绕过 cache 才有诱惑
新:per-cluster swap table
cache 状态由 swap table 直接维护
查找 / 插入变成轻量操作
旧:cache 有存在感
多一层结构,多一份锁竞争
绕过像是“省掉”了它
现在:开销可忽略
swap table 就位,cache 不再是瓶颈
绕过失去性能理由
前提变了:统一走 cache 反而更快
remove bypass → 才有条件重做同步与标志位
旧:按 count 分叉
if (sync && count == 1)
/* skip swapcache */
else
swapin_readahead()
count > 1:共享页走慢路径
绕过只对单映射生效
新:一律走 swap cache
if (sync)
alloc_swap_folio() + swapin_folio()
else
swapin_readahead()
所有 entry 都经过 cache
不再有 count 分叉
拆掉 count 门控
__swap_count(entry) == 1 不再是分叉条件
两个行为独立生效
预读:同步设备总是禁用
大块:总是启用
anon 与 shmem 都统一
mm/memory.c 与 mm/shmem.c 不再走 bypass
统一路径:先移除 bypass,后面的同步与标志位清理才有基础
do_swap_page
sync 分支
alloc + swapin_folio()
readahead 分支
swapin_readahead()
__swap_cache_prepare_and_add()
swapin_folio()(节选)
pgoff_t offset = swp_offset(entry);
unsigned long nr_pages = folio_nr_pages(folio);
entry = swp_entry(swp_type(entry),
round_down(offset, nr_pages));
swapcache = __swap_cache_prepare_and_add(
entry, folio, 0, true);
if (swapcache == folio)
swap_read_folio(folio, NULL);
swap_cache_alloc_folio()(节选)
folio = swap_cache_get_folio(entry);
if (folio)
return folio;
if (!swap_entry_swapped(si, entry))
return NULL;
folio = folio_alloc_mpol(...);
result = __swap_cache_prepare_and_add(
entry, folio, gfp_mask, false);
共用准备路径
① 设置 locked / swapbacked
② 校验 swap_map(count > 0)
③ 插入 swap cache
④ 失败返回已有 folio / NULL
sync 与 readahead 共用
差别只在谁提供 folio
旧:swap_map 位锁
A
置位 0x40
做 I/O
清位
B
看到 0x40
睡 1 tick
重试
B 不知道 A 何时完成
只能定时器轮询
新:cache 插入 + folio 锁
A
folio_lock
add_folio
读 swap
B
get_folio
folio_lock
等 A 完成
谁先插入谁做 I/O
后来者等 folio 锁
__swap_cache_prepare_and_add()(节选)
__folio_set_locked(folio);
ret = swap_cache_add_folio(folio, entry,
&shadow);
等锁,不轮询
后来者拿到已锁定的 folio
folio_lock() 等 A 完成
没有 1 tick 定时器
根因:位锁不记录持有者;对象锁天然排队
旧:特殊位混在 swap_map
SWAP_HAS_CACHE 0x40
SWAP_MAP_SHMEM 0xbf
缓存状态 / shmem 所有权
和引用计数混在一起
新:swap_map 只做计数
COUNT_CONTINUED 0x80
剩下的 continuation 标志
SWAP_HAS_CACHE / SHMEM 移除
swap count 回归本位
分配时不再 pin 0x40
folio_alloc_swap() 直接插入 cache
释放时不再清 0x40
统一由 cache 删除 / entries free 处理
swap_map 回归引用计数
只剩 count + COUNT_CONTINUED;为下一阶段并入 swap table 铺路
Redis / Valkey:BGSAVE 是主战场
ARM64 VM 1.5G · x86_64 VM 4G · ZRAM · redis-benchmark -t get
BGSAVE 吞吐(RPS,越高越好)
共享 / COW 场景,旧位锁长尾最重
ARM64 BGSAVE
before
311,591.19
after
371,379.06 (+19.2%)
x86 BGSAVE
before
102,745.88
after
125,313.28 (+22.0%)
无持久化:基本持平
ARM64 460,475.84 → 451,943.34 (-1.9%)
x86 306,044.38 → 309,645.44 (+1.2%)
ARM64 略降:swap_map 仍有开销
其余 workload 基本持平
内核构建 1379.91 s → 1364.22 s
ZSWAP+NVME 1822.52 s → 1803.33 s
MySQL / vm-scalability 持平
① 统一路径
所有换入走 swap cache
移掉 bypass 门控
② 对象锁同步
cache 插入 + folio 锁
不再睡眠轮询
③ 清理标志
SWAP_HAS_CACHE 移除
swap_map 只计数
旧
绕过 cache 受 count 门控
SWAP_HAS_CACHE 位锁
竞争者睡 1 tick 重试
新
所有换入走 swap cache
cache 插入 + folio 锁
swap_map 只记引用计数
后台保存约 +20%
ARM64 +19.2% / x86 +22.0%;无持久化基本持平
统一路径,为 swap count 并入 swap table 铺路
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x