mm/swap:swap cache 稳定大分配 — 大对象直接入 swap cache 减少页表/分配开销
💡 一句话总结
在内存压力下按大 folio(mTHP)swap-in 的场景,此前 anon/shmem 各自的 mTHP 分配路径在 swap cache 之外用无锁冲突检查,可能把"已被占用的槽位范围"误判为可用(假阴性),分配出大 folio 后插入时才冲突,被迫整段解卷回退;readahead 还会对已释放槽位做无效分配。本补丁在 swap cache 层重写 swap_cache_alloc_folio(),把"槽位检查 → 分配 → 插入"统一收进 cluster 锁保护下的双重检查流程,大分配不再因假阳性冲突被解卷、对无效槽位提前中止,并统一了 anon/shmem/readahead 的分配路径。补丁未提供基准数据,收益为逻辑推断(解读(AI 分析))。
📋 补丁基本信息
| 项目 | 内容 |
|---|---|
| 补丁类型 | 优化(performance)+ 基础设施重构(为 mTHP swap-in 提供稳定分配助手) |
| 性能类别 | 页回收(swap cache 大 folio 分配路径,属"开销降低/热路径") |
| 状态 | 状态(Merged)· 合入版本:Linux 7.2(git describe 确认首个包含 commit 的 tag 为 v7.2-rc1) |
| 当前版本 | v5(swap-table 系列 04/12)· 当前版 lore 链接 |
| 版本演进 | 系列经历 v1→v5 五版;本环境 lore 对早期版本查询超时、cover letter 未索引到,各版改动点无法从可及数据源确认(如实标注)。可确认 v5 系列 12 补丁结构,本补丁为第 4 个。 |
| 作者机构 | Kairui Song(腾讯 Tencent) |
| 提交日期 | 2026-05-17(合入 mainline 的 commit 日期) |
| 改动范围 | mm/swap.h(3 行)、mm/swap_state.c(+236/-69)、mm/zswap.c(2 行);3 文件 +170/-71 行 |
| 核心函数 | swap_cache_alloc_folio() / __swap_cache_alloc() / __swap_cache_add_check() |
| 原始链接 | git.kernel.org commit · lore Message-ID |
📊 速览卡片
特性等级依据:无实测基准、收益来自减少解卷回退与统一分配路径(逻辑推断)→ 幅度分低;但纯内核改动、无硬件/配置门槛、向后兼容且为后续 mTHP swap-in 铺路 → 落地与场景覆盖分高,综合 ★★★。
🎯 解决什么问题
本补丁(04/12)是系列"基础设施主体"之一:提供稳定的"大分配"助手
swap_cache_alloc_folio(),依赖前导补丁 1/12(简化分配助手)、2/12(把公共 swap cache 操作拆成独立助手)、3/12(把 THP gfp 限制助手移到 header);并为后一个补丁 5/12 "unify large folio allocation" 提供前提——后者把 anon/shmem 原有的大 folio 分配全部改走本助手。
swap_cache_alloc_folio() 只支持 order 0(单页),大 folio swap-in 只能由 anon/shmem 各自在 swap cache 之外单独实现(commit message 原文:"all the separate swap folio allocation that is being done by anon / shmem before")。这些各自实现既重复又容易出错:- 用无锁冲突检查判断槽位范围是否可用,检查与插入之间没有原子保护,可能返回假阴性(说"可用"其实已被占用)——大 folio 分配出来后在插入阶段才发现冲突,只能整段解卷回退;
- 对已释放(swap 计数为 0)的槽位没有提前中止,普通 swapin 和 readahead 会做无谓分配。
do_swap_page()(anon)/shmem_swapin_folio()(shmem)/swap_cache_read_folio()(readahead)→ 调 swap_cache_alloc_folio() 分配 folio 并加入 swap cache → 对锁定 folio 发起读 I/O。机制缺陷:旧实现把"槽位可用性检查"放在分配之前、用无锁路径完成,与"插入"不是原子的一步;且每类调用方各写一套槽位计数/缓存状态检查。这导致两个可测问题——① 大分配被假阴性冲突打回,产生解卷回退(分配浪费 + 重复检查);② 已释放槽位未提前识别,readahead 路径反复做无效分配。
为什么该场景遇到缺陷:mTHP 分配的是 2MB 级别的大 folio,覆盖的 swap 槽位范围更大,与并发 free/并发 swap-in 撞上"范围被占用"的概率随之上升——无锁检查的假阴性窗口被大 folio 放大,解卷回退的浪费也更大(一次回退 = 释放一整块 2MB folio + 重新走单页路径)。
🧩 核心机制
核心逻辑点(框架 B):① 检查函数重写:目标槽快速失败 + 批量槽冲突返回 -EBUSY ② 新助手 __swap_cache_alloc():cluster 锁下"检查 → 分配 → 复检 → 原子插入" ③ swap_cache_alloc_folio() order 回退循环 ④ 调用方统一改走新助手——四步构成"大 folio 分配不再被假阴性冲突打回"的完整链条。
__swap_cache_add_check() 改为按目标 entry + nr 检查——先检查目标槽(已缓存 → -EEXIST,已释放 → -ENOENT,都提前返回不再分配),再检查批量范围;批量内任何槽冲突统一返回 -EBUSY(表示"这次拿不到,换 order 再试"),调用方据此降阶。逻辑点②:新助手
__swap_cache_alloc() 的双重检查——第一次持 cluster 锁跑 __swap_cache_add_check() 确认范围可用,解锁后做真正的页分配(这是慢操作,不该持锁);分配成功后第二次再持同一把锁复检,并把"复检 + 标记 folio locked/swapbacked + 插入 swap cache"合进同一临界区——插入前一刻仍持锁,杜绝检查与插入之间的竞态。commit message 原文:"The slot-count and cache-conflict check is now always performed with the cluster lock held before allocation, and repeated under the same lock right before cache insertion."逻辑点③:order 回退——
swap_cache_alloc_folio() 接受 orders 位图,从最高阶开始试;-EBUSY/-ENOMEM 时降阶重试,并记 MTHP_STAT_SWPIN_FALLBACK;非冲突错误(-ENOENT/-EEXIST)直接返回,不再徒劳降阶。逻辑点④:调用方统一——readahead(
swap_cache_read_folio())与 zswap writeback 改用新签名(单页传 BIT(0)),并删除它们自己头上的 swap_cache_get_folio() 预检与 swap_entry_swapped() 检查——这些职责收进助手内部。
swap_cache_alloc_folio(),cluster 锁下①检查→②分配(可降阶)→③复检+原子插入→④charge/LRU→⑤发起 swap-in。来源:基于真实 commit diff(mm/swap_state.c)绘制
| 逻辑点 | 操作 | 目的 |
|---|---|---|
| ① 检查函数重写 | __swap_cache_add_check(ci, targ_entry, nr, &shadow) | 目标槽 -EEXIST/-ENOENT 提前中止,批量冲突 -EBUSY(前提) |
| ② 双重检查 | __swap_cache_alloc() 分配前检查 + 分配后复检并入插入临界区 | 消除假阴性窗口,大分配不再解卷(主体) |
| ③ order 回退 | swap_cache_alloc_folio() 按 orders 位图降阶重试 | 大分配失败退化为更小 folio/单页,不整体失败(落地) |
| ④ 调用方统一 | readahead/zswap 改新签名,删重复预检 | 职责收进助手,减少重复实现(收尾) |
+static struct folio *__swap_cache_alloc(struct swap_cluster_info *ci,
+ swp_entry_t targ_entry, gfp_t gfp,
+ unsigned int order, struct vm_fault *vmf,
+ struct mempolicy *mpol, pgoff_t ilx)
+{
+ int err;
+ swp_entry_t entry;
+ struct folio *folio;
+ void *shadow = NULL;
+ unsigned long address, nr_pages = 1UL << order;
+ struct vm_area_struct *vma = vmf ? vmf->vma : NULL;
+
+ VM_WARN_ON_ONCE(nr_pages > SWAPFILE_CLUSTER);
+ entry.val = round_down(targ_entry.val, nr_pages);
+
+ /* Check if the slot and range are available, skip allocation if not */
+ spin_lock(&ci->lock);
+ err = __swap_cache_add_check(ci, targ_entry, nr_pages, NULL);
+ spin_unlock(&ci->lock);
+ if (unlikely(err))
+ return ERR_PTR(err);
+
+ /*
+ * Limit THP gfp. The limitation is a no-op for typical
+ * GFP_HIGHUSER_MOVABLE but matters for shmem.
+ */
+ if (order)
+ gfp = thp_shmem_limit_gfp_mask(vma_thp_gfp_mask(vma), gfp);
+
+ if (mpol || !vmf) {
+ folio = folio_alloc_mpol(gfp, order, mpol, ilx, numa_node_id());
+ } else {
+ address = round_down(vmf->address, PAGE_SIZE << order);
+ folio = vma_alloc_folio(gfp, order, vmf->vma, address);
+ }
+ if (unlikely(!folio))
+ return ERR_PTR(-ENOMEM);
+
+ /* Double check the range is still not in conflict */
+ spin_lock(&ci->lock);
+ err = __swap_cache_add_check(ci, targ_entry, nr_pages, &shadow);
+ if (unlikely(err)) {
+ spin_unlock(&ci->lock);
+ folio_put(folio);
+ return ERR_PTR(err);
+ }
+
+ __folio_set_locked(folio);
+ __folio_set_swapbacked(folio);
+ __swap_cache_do_add_folio(ci, folio, entry);
+ spin_unlock(&ci->lock);
+▲ 这段是机制成立的关键:第一次持锁检查(确认范围可用)→ 解锁后分配(vma_alloc_folio/folio_alloc_mpol,这是不能持锁的慢操作)→ 第二次持锁复检 + 原子插入。第二次检查与 __swap_cache_do_add_folio() 在同一个临界区里,插入前一刻仍持有 cluster 锁——这正是消除"无锁检查假阴性"的手段。两个 spin_lock(&ci->lock) 夹着一次页分配,锁只保护检查与插入这两段短临界区,分配本身不持锁。
+ do {
+ ret = __swap_cache_alloc(ci, targ_entry, gfp, order,
+ vmf, mpol, ilx);
+ if (!IS_ERR(ret))
+ break;
+ err = PTR_ERR(ret);
+ if (!order || (err && err != -EBUSY && err != -ENOMEM))
+ break;
+ count_mthp_stat(order, MTHP_STAT_SWPIN_FALLBACK);
+ order = next_order(&orders, order);
+ } while (orders);
▲ 关键:只在 -EBUSY(范围冲突)或 -ENOMEM(页分配失败)时降阶重试;-ENOENT(槽已释放)/-EEXIST(已缓存)这类"没得谈"的错误直接返回,不再徒劳重试。每次降阶记 MTHP_STAT_SWPIN_FALLBACK 统计,供运维观察 mTHP swap-in 的降阶率。
📈 性能影响
按 Brendan Gregg 的 on/off-CPU 视角:本补丁主要在 on-CPU 减指令/减无效工作(不触发 I/O、不涉及锁等待主项),收益以 CPU 计算侧为主。
swap readahead:预读批量分配时,对已释放/坏槽位的无效分配被
-ENOENT 提前中止(commit message 明确点名"helps ordinary swapin and especially readahead")。zswap writeback:解出回 swap cache 的路径同样受益于统一助手(调用点同步更新)。
| 场景/用例 | 运行环境 | 改进前 | 改进后 |
|---|---|---|---|
| mTHP swap-in / readahead / zswap writeback | (无基准) | (未提供) | (未提供) |
说明:补丁未提供基准(commit message 无 benchmark 数据,属纯基础设施补丁)。以上收益为基于代码路径的逻辑分析,标"解读(AI 分析)",未独立测量。作者在 commit message 中只给出定性描述:"avoids the false-negative conflict checks that the lockless path can return — large allocations no longer have to be unwound"、"aborts early for already-freed slots, which helps ordinary swapin and especially readahead"。
🔄 方案演进
本补丁属 swap-table 系列 v5(12 补丁),已在 mainline 合入(Linux 7.2)。早期版本演进信息受数据源限制(见下):
thp_shmem_limit_gfp_mask())→ 4/12 本补丁(稳定大分配助手) → 5/12 unify large folio allocation(消费本助手,把 anon/shmem 大 folio 分配统一接入)。系列 6/12 起转向 swap 表与 cgroup 侧:tidy up cgroup v1 memsw swap helpers(6)→ flexible batch freeing(7)→ delay and unify memcg lookup for swapin(8)→ consolidate cluster allocation helpers(9)→ store cgroup id in cluster table(10)→ remove no longer used swap cgroup array(11)→ merge zeromap into swap table(12)。整体目标是把 swap 表/cgroup 结构简化并最终让大 folio swap-in 落地。
v5 04/12 合入 mainline。系列经历 v1→v5 五版,但:① cover letter(…-v5-0-…)未被 lore 索引(lore_message / lore_find 均空);② 早期版本(v1–v4)的 lore 查询在本环境超时;③ 该补丁在 lore 中未检索到公开 review 回复(in_reply_to 查询返回 0 条、downstream 为空)。因此 v1→v4 的具体改动点与 review 推动细节数据不可得,此处不作编造;背景/动机以上述 commit message 与系列结构为准。社区参与证据(来自 commit message trailer):补丁 Cc 了
David Hildenbrand、Hugh Dickins、Johannes Weiner、Zi Yan、Baolin Wang、Barry Song、Chris Li、Nhat Pham、Muchun Song、Roman Gushchin、Shakeel Butt、Lorenzo Stoakes 等 mm 维护者,并经 Andrew Morton 合入 mm 树——属常规 review 覆盖面。
⚠️ 风险与局限
orders 传入大阶数),单页 swap-in/readahead 只传 BIT(0),走同一条新路径但只享受"提前中止无效槽位"的次要收益(解读(AI 分析))。收益最明显的场景是"大 folio + 高并发 swap 活动";无 swap 压力时该路径不被触发,近 no-op。
接口签名变更:
swap_cache_alloc_folio() 增加 orders 与 vmf 参数,调用方(zswap.c、readahead)已同步更新;out-of-tree 模块若直接调用该内核实函数会编译失败,但该函数非导出 API,影响面限于内核 mm 内部。
MTHP_STAT_SWPIN_FALLBACK 统计可观测 mTHP swap-in 降阶率(运维可观察性收益)。
🔗 交叉引用
系列 2/12 · mm, swap: move common swap cache operations into standalone helpers — 前导补丁,本补丁复用的
__swap_cache_do_add_folio() 等公共操作系列 3/12 · mm/huge_memory: move THP gfp limit helper into header — 前导补丁,本补丁调用的
thp_shmem_limit_gfp_mask() 来源系列 5/12 · mm, swap: unify large folio allocation — 消费本补丁助手,把 anon/shmem 大 folio 分配统一接入(系列的真正收益补丁)
git.kernel.org commit e1e6750df3b4 — 本补丁主线 commit
本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。