背景

swap 子系统为每个 swap slot 维护若干元数据:它属于哪个 cgroup(swap cgroup,用一张静态数组)、是否是零页(zeromap,一张独立映射)、落在哪个 cluster。这几套结构各自为政,静态数组的开销随 swap 设备大小线性增长,大约 per-slot 2 字节,挂一个 1TB 的 swap 设备就要吃掉约 512MB 内存,哪怕一个 slot 都没用到。

┌──────────────────────────────────────┐
│         swap 元数据分散管理          │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 计费数组、零页映射、cluster 表各自为 │
│ 政                                   │  per-slot 约 2 字节,1TB swap 占约 512MB
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐  高阶 swapin 查状态与分配分开,留竞态窗
│    匿名与 shmem 分配计费各自实现     │  口
└──────────────────────────────────────┘

分配与计费的路径也不统一。匿名页和 shmem 各自实现 swap folio 的分配与 fallback;高阶 swapin 要先查 swap cache 状态、再分配、再 fallback,几步分开做,中间留着 TOCTOU 竞态窗口。memcg 的查找与计费也散落在各处。同步上的不足,在重压下还会让 VM_FAULT_OOM 偶发泄漏到缺页处理路径。

问题

  • swap 元数据分散:计费数组、零页映射、cluster 表各自为政
  • 静态数组开销随 swap 大小线性增长(per-slot 约 2 字节)
  • 匿名与 shmem 分配计费各自实现,留竞态窗口
  • 压力下同步不足,偶发缺页 OOM 泄漏

方案

整系列围绕一个"统一"展开:把分散的元数据收敛进 swap table,把各自实现的分配与计费并成一条路径。

旧:元数据分散,路径各自实现
┌──────────────────────────────────────┐
│ 计费数组、零页映射、cluster 表多套结 │
│ 构                                   │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│    匿名与 shmem 分配计费各走各的     │  压力下同步不足,偶发缺页 OOM 泄漏
└──────────────────────────────────────┘

新:收敛进统一 swap 表
┌──────────────────────────────────────┐
│ cgroup 编号存进 cluster 表,零页映射 │
│ 并入 swap 表                         │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐  静态元数据降到近零,高阶 swapin 在紧凑
│    匿名与 shmem 走统一分配与计费     │  循环内完成
└──────────────────────────────────────┘

分配上,让 swap cache 直接支持稳定的大阶分配后,匿名与 shmem 都改走它,不再各写一套。原先高阶 swapin 的"查 swap cache 状态、分配、fallback"几步合并进同一个紧凑循环,TOCTOU 窗口随之收窄;cgroup 与零页检查也挪进这个循环,进一步去冗余。order 的 fallback 与统计一并统一,调用方只需给出可接受的 order 位掩码。

计费上,swapin 时把 memcg 的查找与计费延迟并统一起来,不再散落各处。

元数据上,把 cgroup 编号直接存进 cluster table,于是那张独立的 swap cgroup 静态数组可以整张删掉,这正是 512MB 内存节省的来源;zeromap 也合并进 swap table,又少一张独立映射。这几处合起来,静态元数据开销降到接近零(per-slot 约 0.09375 字节),同步也更稳。

收益

作者在四个 workload 上做了对比,外加内存节省与 thrashing 统计。

静态元数据内存节省(挂载 1TB swap 设备):

指标 改动前 改动后
内存占用(used) ~805MB ~277MB(省 ~512MB)
per-slot 静态元数据 ~2 bytes ~0.09375 bytes

Redis(2G VM + 6G ZRAM swap,redis-benchmark get):

指标 改动前 改动后
RPS 3,385,017 3,433,309(+1.42%)

内核构建(48c96t,8G 内存 + 12G ZRAM + THP,make -j96 defconfig,24 次):

指标 改动前 改动后
user time 2904.59s 2909.38s(持平)
system time 4773.99s 4641.55s(-2.77%)

usemem(32c,48G brd ramdisk + 16G RAM,12 次):

指标 改动前 改动后
Throughput 6482.58 MB/s 6539.28 MB/s
Free Latency 371371.67us 363059.88us

thrashing(压力测试中 VM_FAULT_OOM 泄漏计数):

指标 改动前 改动后
VM_FAULT_OOM 泄漏计数 18 0

主要收益是静态元数据近乎清零(1TB swap 省 512MB)与压力下的稳健性(缺页 OOM 泄漏 18→0);各 workload 性能持平或略升(Redis +1.42%、内核构建 sys time -2.77%)。