背景

swapoff(sys_swapoff)要把 swap 设备上所有仍在用的 swap entry 换回内存。核心循环 try_to_unuse 反复调用 find_next_to_unuse,从上次的位置往后找到下一个待换回的 slot,找到一个就换回一个,直到整个设备扫完。

swap 设备按 cluster 组织,每个 cluster 含 SWAPFILE_CLUSTER(默认 256)个 slot。slot 的状态存放在每个 cluster 自己的 swap table 里,cluster 还带一个 count 计数,记录其中仍在用的 slot 数。find_next_to_unuse 原来的做法是一次推进一个 offset:对每个 offset 都 swap_table_get 读一次 swap table 的表项,判断该 slot 是否仍在用,没找到就继续下一个。

find_next_to_unuse 逐 offset 扫描整个设备
┌──────────────────────────────────────────┐
│ swapoff 调 try_to_unuse 逐个找下一个     │
│ 待换回的 slot                            │
└────────────────────┬─────────────────────┘
                     ▼
┌──────────────────────────────────────────┐
│ find_next_to_unuse 对每个 offset 读      │
│ 一次 swap table 判断是否在用             │
└────────────────────┬─────────────────────┘
                     ▼
┌──────────────────────────────────────────┐
│ 空 cluster 也要逐个读完 SWAPFILE_CLUSTER │
│ 个表项才知全空                           │
└──────────────────────────────────────────┘

问题

  • find_next_to_unuse 一次一个 offset 遍历整个 swap 设备。
  • 空 cluster(无在用 slot)也要逐个读 SWAPFILE_CLUSTER 个表项才能判定。
  • swap 设备常存在大量空 cluster,逐表项读纯属无谓开销。

方案

既然每个 cluster 已经有一个 count 计数直接表明它是否还有在用的 slot,扫描就不必再逐表项地确认。

find_next_to_unuse 改为按 cluster 推进:进入一个 cluster 时先读一次 cluster_info 的 count,若为 0 说明整簇没有在用的 slot,直接整簇跳过;只有非空的 cluster 才在簇内逐 offset 读 swap table,找到下一个在用的 slot 返回。

if (!READ_ONCE(ci->count)) {
    i = end;
    cond_resched();
    continue;
}

整簇跳过是安全的。判定不持 ci->lock,读 count 用 READ_ONCE() 标注;wait_for_allocation() 已经停住新分配,计数只减不增,而一个 slot 的计数只有在它的 folio 离开 swap cache 之后才会被减掉,所以 count 为 0 意味着这个 cluster 里已经没有任何 slot 还需要 try_to_unuse 去换回,对它逐表项读取本就是空跑。改写之后,空 cluster 的判定从 SWAPFILE_CLUSTER 次表项读压缩为 1 次 count 读。

簇内扫描的上界用 min_t() 钳在 si->max,不完整的尾簇在 si->max 处停住,不会越界。循环每轮至多 SWAPFILE_CLUSTER 次迭代就返回或 reschedule,与 swap_reclaim_full_clusters() 的扫描粒度一致,旧实现为压制 stall 每 256 个 offset 强行 cond_resched() 一次的 LATENCY_LIMIT 计数也随之删除。

按 cluster 扫描,空簇一次计数读整簇跳过
                    ┌────────────────────────────────────┐
                    │ 进入每个 cluster 先读 cluster_info │
                    │ 的 count 计数                      │
                    └─────────────────┬──────────────────┘
                 ┌────────────────────┴────────────────────┐
                 ▼                                         ▼
┌──────────────────────────────────┐      ┌──────────────────────────────────┐
│ count 为 0 即整簇无在用 slot,直 │      │ 非空簇才在簇内逐 offset 找下一个 │
│ 接跳到簇末尾                     │      │ 在用 slot                        │
└──────────────────────────────────┘      └──────────────────────────────────┘

收益

1 TiB swap 设备先填充到指定量再 swapoff,剩余在用 slot 集中在已填充区域的顶部,其下方全是可以整簇跳过的空 cluster。填充 32/128 GiB 档各取 11 组中位数,256/512 GiB 档各取 3 组中位数:

指标 Before After 改善
swapoff 耗时(填充 32 GiB) 92.4 ms 66.3 ms -28.2%
swapoff 耗时(填充 128 GiB) 157.6 ms 94.4 ms -40.1%
swapoff 耗时(填充 256 GiB) 209.7 ms 63.8 ms -69.6%
swapoff 耗时(填充 512 GiB) 391.8 ms 73.4 ms -81.3%

旧实现的耗时随填充量增长,新实现基本与填充量无关。常规场景下 swap 里存的是真实数据,swapoff 的时间花在把数据读回来上:8 GiB 设备填充 4 GiB 时扫描本身只占 try_to_unuse() 的 1.4%,新旧实现没有差异。