mm/swap:swap cache 稳定大分配 — 大对象直接入 swap cache 减少页表/分配开销

内存管理 · swap cache 分配基础设施 · mTHP swap-in 前提

💡 一句话总结

在内存压力下按大 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
特性等级
★★★
实测提升
未提供数据

特性等级依据:无实测基准、收益来自减少解卷回退与统一分配路径(逻辑推断)→ 幅度分低;但纯内核改动、无硬件/配置门槛、向后兼容且为后续 mTHP swap-in 铺路 → 落地与场景覆盖分高,综合 ★★★。

🎯 解决什么问题

系列整体定位(本补丁所属 swap-table 系列,v5 共 12 补丁)
该系列(作者 Kairui Song,腾讯)整体重构 swap 表 + swap cache 基础设施:让大 folio(mTHP)能直接在 swap cache 里分配并 swap-in,并统一 anon/shmem/readahead 的 folio 分配路径,最后顺带简化 swap cgroup 与 cluster 结构(把 cgroup id 存进 cluster 表、合并 zeromap 等)。

本补丁(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 分配全部改走本助手。
背景 / 原始动机
内核持续推进 mTHP(multi-size THP,可按 2MB/64KB/16KB 等大 folio 分配)。swap-in 侧也要能一次把一整块大 folio 从 swap 设备读回,而不是拆成单页——这样能减少页表项数量、缺页次数与 I/O 次数(commit message 明确说"make it possible to allocate large folios directly in swap cache")。但旧 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 会做无谓分配。
系统层面:swap cache 分配流程"检查与插入不同步"
执行路径:swap-in 缺页 → do_swap_page()(anon)/shmem_swapin_folio()(shmem)/swap_cache_read_folio()(readahead)→ 调 swap_cache_alloc_folio() 分配 folio 并加入 swap cache → 对锁定 folio 发起读 I/O。
机制缺陷:旧实现把"槽位可用性检查"放在分配之前、用无锁路径完成,与"插入"不是原子的一步;且每类调用方各写一套槽位计数/缓存状态检查。这导致两个可测问题——① 大分配被假阴性冲突打回,产生解卷回退(分配浪费 + 重复检查);② 已释放槽位未提前识别,readahead 路径反复做无效分配。
场景层面:内存压力下的大 folio swap-in / readahead
执行路径 → 高频触发:内存超卖容器/数据库把大块 anon working set 换出后再次访问,触发 mTHP swap-in;swap readahead 在顺序访问换出页面时批量预读;zswap 在写回时也要把压缩页解出回 swap cache。这些路径在内存压力负载下以每秒数万次触发。
为什么该场景遇到缺陷:mTHP 分配的是 2MB 级别的大 folio,覆盖的 swap 槽位范围更大,与并发 free/并发 swap-in 撞上"范围被占用"的概率随之上升——无锁检查的假阴性窗口被大 folio 放大,解卷回退的浪费也更大(一次回退 = 释放一整块 2MB folio + 重新走单页路径)。
受影响负载:mTHP 大对象 swap-in / swap readahead / zswap writeback · 为什么此特性解决此场景:把检查放进 cluster 锁临界区并"分配前+插入前"双检 → 假阴性窗口消失,大分配不必解卷,无效槽位提前中止

🧩 核心机制

核心逻辑点(框架 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 大分配 before/after 流程对比图
图 1:Before/After 对比——左边旧路径:anon/shmem 各自分配、无锁冲突检查可能假阴性,插入时才冲突导致整段解卷回退;右边本补丁:统一 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) 夹着一次页分配,锁只保护检查与插入这两段短临界区,分配本身不持锁。

order 回退循环(分配失败时的降阶逻辑)
+	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 的降阶率。

📈 性能影响

提升角度(方法论分类)
主类是 开销降低(on-CPU):省掉三笔开销——① 假阴性冲突导致的大 folio 整段解卷回退(释放一块 2MB folio + 重新走单页路径);② readahead/swapin 对已释放槽位的无效分配;③ 各路径重复实现的槽位计数/缓存状态检查。次类是 off-CPU 竞态窗口收窄:双重检查 + 紧凑循环使"分配 folio 与真正 swap-in"之间的竞态窗口变小(commit message:"The race window of swapping is also reduced")。

按 Brendan Gregg 的 on/off-CPU 视角:本补丁主要在 on-CPU 减指令/减无效工作(不触发 I/O、不涉及锁等待主项),收益以 CPU 计算侧为主。
受益场景
mTHP swap-in:mTHP 大 folio 覆盖槽位范围大,撞上并发 free/swap-in 的概率高,旧无锁检查的假阴性解卷在 2MB 级别最贵——本补丁直接消除该回退(解读(AI 分析),依据:大 folio 范围更大 → 冲突概率与解卷代价更高,路径见"解决什么问题")。
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)。早期版本演进信息受数据源限制(见下):

系列结构与本补丁角色
1/12 simplify swap cache allocation helper(前导:简化旧助手)→ 2/12 move common swap cache operations into standalone helpers(前导:拆分公共操作)→ 3/12 move THP gfp limit helper into header(前导:THP gfp 助手入 header,供本补丁调用 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 覆盖面。

⚠️ 风险与局限

收益成立的前提
mTHP swap-in 必须实际启用(有 orders 传入大阶数),单页 swap-in/readahead 只传 BIT(0),走同一条新路径但只享受"提前中止无效槽位"的次要收益(解读(AI 分析))。收益最明显的场景是"大 folio + 高并发 swap 活动";无 swap 压力时该路径不被触发,近 no-op。
生产落地影响
cluster 锁争用边际增加:作者自述"only a marginal increase in cluster-lock contention (the lock is very lightly contended and stays local in the first place)"——检查从无锁变成每次持锁,但该锁本身极轻争用且本地。生产上值得观察大规模多核 + 高并发 swap-in 时此锁是否变热(解读(AI 分析))。
接口签名变更:swap_cache_alloc_folio() 增加 orders 与 vmf 参数,调用方(zswap.c、readahead)已同步更新;out-of-tree 模块若直接调用该内核实函数会编译失败,但该函数非导出 API,影响面限于内核 mm 内部。
生态/兼容性
无用户态 API 变化,无 sysctl/配置项。对工具链无依赖。新增 MTHP_STAT_SWPIN_FALLBACK 统计可观测 mTHP swap-in 降阶率(运维可观察性收益)。
review 质疑(若有)
本环境 lore 未检索到该补丁的公开 review 回复(如实标注);无法呈现具体质疑。补丁最终合入 mainline,说明常规 review 流程通过。
严重度:MINOR(无 CRITICAL/MAJOR 回归点)· 落地场景:最需关注的是大并发 swap-in 下 cluster 锁争用是否上升,以及 mTHP swap-in 回退率是否如预期下降

🔗 交叉引用

📌 关联工作 / 系列其他补丁
系列 1/12 · mm, swap: simplify swap cache allocation helper — 前导补丁,简化旧分配助手
系列 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 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。