背景
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%,新旧实现没有差异。