系列:[RFC PATCH v2 0/4] mm/zsmalloc: reduce zs_free() latency on swap release path作者: Wenchao Hao,Xueyuan Chen、Barry Song版本: v2(4 个 patch)
在 Android 等内存受限场景中,Low Memory Killer(LMK)会集中杀掉多个进程以回收内存。进程退出时需要解除 VMA 中大量 swap 条目的引用,而其中每一条 swap 条目都会穿过 zram 的 zram_slot_free_notify() 路径调用 slot_free(),最终由 zs_free() 释放 zsmalloc 句柄。作者在分析中发现,slot_free() 成本的 80%~87% 花在 zs_free() 上——因为它既要获取 pool->lock、class->lock,也可能在 zspage 引用计数归零时把整个 zspage 还给 buddy(需再抢 zone lock)。这条慢路径把"匿名 folio 释放"也拖慢了,直接影响系统回收速度。社区此前 Justin Jiang 和 Lei 的两版 RFC(2024/2025)尝试在 mm 核心引入独立线程异步回收,但架构复杂且侵入性大;Barry Song 最近 RFC 则从 zram 层单独尝试异步释放。本系列综合了上述思路,把优化下沉到 zsmalloc 层,使得 zram 与 zswap 都能受益。
zs_free() 在 class->lock 保护下调用 __free_zspage() 把页归还 buddy,期间会等 zone lock,导致同一 class->lock 被长时间持有,阻塞其他 CPU 的 zs_free()。zram_slot_free_notify() 和 zswap_invalidate() 同步执行 zs_free(),延迟匿名 folio 的整体释放,恶化 LMK 场景下的回收速率。slot_free() 把"slot metadata 清理"与"zsmalloc 句柄释放"耦合在一起,难以独立推迟后者。系列采用两条互补路径。第一条是压缩锁窗口(patch 1):把 __free_zspage() 中实际把页归还 buddy 的逻辑拆成 __free_zspage_lockless(),zs_free() 在 class->lock 内只做 remove_zspage() 和 class_stat_sub(),随后先 spin_unlock(&class->lock) 再调用 __free_zspage_lockless() 归还页面,避免 zone lock 等待污染 class 锁。若 trylock_zspage 失败则退回 kick_deferred_free()。
第二条是异步延迟释放(patch 2):新增 zs_free_deferred()/zs_free_deferred_flush() 两个 API。在 struct zs_pool 中加入一块固定大小的 handle 环形区:
#define ZS_DEFERRED_FREE_MAX_BYTES (128 << 20)
#define ZS_DEFERRED_FREE_CAPACITY (ZS_DEFERRED_FREE_MAX_BYTES >> PAGE_SHIFT)
#define ZS_DEFERRED_FREE_THRESHOLD (ZS_DEFERRED_FREE_CAPACITY / 2)
容量按 128MB 未压缩数据预算计算,随 PAGE_SIZE 自然缩放。zs_free_deferred() 使用 spin_trylock() 入队,队列满或锁争用时 fallback 到同步 zs_free();达到阈值时 queue_work(system_wq, &pool->deferred_free_work)。Pool 销毁前 zs_free_deferred_flush() 保证 drain。
第三、四条是使用者改造:patch 3 将 zram 的 slot_free() 拆成 slot_free_extract()(只做 metadata 清理并返回 handle)和外部的 zs_free(),zram_slot_free_notify() 把 handle 交给 zs_free_deferred();其他同步路径(write/discard/meta_free)保持不变。Patch 4 对 zswap 做类似改造,把 zswap_entry_free() 封装成 __zswap_entry_free(entry, deferred),仅 zswap_invalidate() 走延迟路径,load/writeback/store 错误路径仍用同步释放。
zs_free()(patch 1 压缩锁窗口) | |||
作者原话:"slot_free() accounting for more than 80% of the total cost of freeing swap entries",延迟释放同时让 do_swap_page() 不再等待 slot_free 完成,这是一个有益的副作用。
系列:[PATCH v4 0/2] mm/memory hotplug/unplug: Optimize zone contiguous check when changing pfn range作者: Yuan Liu,Tianyou Li版本: v4(2 个 patch)
zone->contiguous 是 mm 的一个优化位:当为真时 pageblock_pfn_to_page() 可直接 pfn_to_page() 而无需 pfn_valid()/__pageblock_pfn_to_page() 逐 pageblock 验证,显著降低 compaction、isolation 等热路径开销。但它是一个"all-or-nothing"属性:只要 zone span 内出现任意一个未初始化的 memmap hole,就必须清零;反过来要置位则需要线性扫描整条 zone。在 move_pfn_range_to_zone() 和 remove_pfn_range_from_zone() 两条 hotplug 路径上,当前实现每次调整一段 pfn 范围都要 clear_zone_contiguous() + set_zone_contiguous(),后者的扫描成本随 zone 大小线性增长。在大内存 VM(几百 GB)或 CXL 设备热插拔场景下,这个扫描动作成为瓶颈——作者报告 256GB 插入需 10s、512GB 需 36s,且很大部分消耗在 set_zone_contiguous()。同时 v4 新增 patch 1 清理了 overlap_memmap_init() 的设计缺陷:该检查原本嵌在 memmap_init_range() 的热循环里逐 pfn 判断 mirrored kernelcore 重叠,实际上应提到更外层的 memmap_init() 做粗粒度跳过。
set_zone_contiguous() 对几百 GB 的 zone 进行全量 pageblock 扫描,hotplug/unplug 延迟随 zone 规模线性恶化。overlap_memmap_init() 作为静态函数在 memmap_init_range 的 PFN 热循环中被调用,既污染热路径又结构割裂。zone->contiguous 缺少可增量维护的计数器,无法快速判断 zone span 内 memmap 是否仍然完整。整体思路是把连续性判定从"全量扫描"换成"增量计数":为 zone 新增 pages_with_online_memmap 计数器,跟踪 zone span 内已有 online memmap 的页数(包括 present 页和已经通过 init_unavailable_range() 初始化的 hole)。核心不变式:
staticinlinevoidset_zone_contiguous(struct zone *zone)
{
if (zone_is_zone_device(zone))
return;
if (zone->spanned_pages == zone->pages_with_online_memmap)
zone->contiguous = true;
}
即只要 spanned_pages == pages_with_online_memmap,zone 即为 contiguous。该 helper 连同 zone_is_contiguous()、clear_zone_contiguous() 一并移到 include/linux/mmzone.h inline,取代原来 mm/mm_init.c 中的 O(N) 扫描版本。
计数器的维护点遍布 hotplug 生命周期:memmap_init_zone_range() 为每段新初始化的 present 区域累加 end_pfn - start_pfn;init_unavailable_range() 改为返回实际初始化的 hole 页数,也累加到 pages_with_online_memmap(并先把 hole_pfn 截断到 zone_start_pfn 以避免跨 zone 误计);adjust_present_page_count() 在 online/offline 时同步更新计数。调用位置也从 move_pfn_range_to_zone()/remove_pfn_range_from_zone() 内部移到 drivers/base/memory.c 的 memory_block_online()/memory_block_offline(),让一整个 memory block 的多次 span 调整共享一次 clear/set 对。另一条独立的清理:把 mirrored_kernelcore 重叠判断从 memmap_init_range() 的 PFN 热循环上移到 memmap_init() 的 region 级循环,热路径更干净。
测试环境为 Intel Icelake server + QEMU 9.0 + v7.0-rc4 Guest kernel,通过 virtio-mem-pci 触发 hotplug,measure 的是从 QEMU 下发命令到 Guest /proc/meminfo MemTotal 反映新内存的总时间。系列已收到 Tim Chen、Qiuxu Zhuo、Yu C Chen、Pan Deng、Nanhai Zou 的 Reviewed-by,CXL 热插拔场景亦有独立验证链接。
系列:[PATCH v2] mm/page_alloc: use batch page clearing in kernel_init_pages()作者: Hrushikesh Salunke版本: v2(单 patch)
init_on_alloc=1 是一项安全加固特性:page allocator 在返回页面前把它们清零,防止敏感数据泄露到下一次分配。它的核心路径是 kernel_init_pages(),对一次 __alloc_pages 返回的 numpages 个连续页逐个调用 clear_highpage_kasan_tagged()。每一次 clear_highpage_kasan_tagged() 都做两件事:kmap_local_page() 建立临时映射、清零完 kunmap_local()。在 !HIGHMEM 的 64-bit 架构上 kmap_local_page() 本质是简单的 page_address(),但函数调用、栈帧、KASAN tag reset 等开销仍然按页累加;更重要的是,清零动作被打碎成一页一页的 memset,阻止了架构级优化原语(例如 x86 的 REP STOSB、ARM64 的 dc zva、非时序写入 movnt 等)对大段连续物理内存一次性发射的机会。在 HugeTLB、大型 Graph500/Pagerank 工作负载中,init_on_alloc 的纯清零成本因此被放大。作者观察到分配 16GB HugeTLB 时清零本身就占用近 0.45s,是个明显可优化点。
kernel_init_pages() 的 per-page loop 产生 numpages 次 kmap_local_page/kunmap_local 往返和函数调用,额外开销随 numpages 线性放大。在 include/linux/highmem.h 新增 helper clear_highpages_kasan_tagged(),把 HIGHMEM 与 !HIGHMEM 分派清晰化(v2 根据 David 的建议从 page_alloc.c 提炼到 highmem.h):
staticinlinevoidclear_highpages_kasan_tagged(struct page *page, int numpages)
{
if (!IS_ENABLED(CONFIG_HIGHMEM)) {
clear_pages(kasan_reset_tag(page_address(page)), numpages);
} else {
int i;
for (i = 0; i < numpages; i++)
clear_highpage_kasan_tagged(page + i);
}
}
在 !HIGHMEM 分支上,先一次 page_address(page) 取得连续线性地址起点,一次 kasan_reset_tag() 去标签,然后单次 clear_pages() 覆盖整段 numpages * PAGE_SIZE,由架构侧的 clear_pages 实现(如 x86 clear_page_rep/clear_page_erms)统一发射最优指令序列。HIGHMEM 分支保留原来的 per-page 逻辑,确保 32-bit 正确。mm/page_alloc.c 里的 kernel_init_pages() 则删除显式 for 循环,只剩 kasan_disable_current(); clear_highpages_kasan_tagged(page, numpages); kasan_enable_current(); 三行。值得注意的是 v2 相比 v1 专门撤掉了 cond_resched() 与分批机制:作者在回信中解释 "kernel_init_pages() runs inside the page allocator and can be called from atomic context, making cond_resched() unsafe"。
系列:[PATCH v3 00/12] mm, swap: swap table phase IV: unify allocation and reduce static metadata作者: Kairui Song版本: v3(12 个 patch)
这是 Kairui Song 推动的 swap 子系统重构第 IV 阶段(前三个阶段已合入 mm 树),目标是把长期以来 per-swap_info_struct 的多张"静态元数据数组"全部下沉到动态分配的 swap cluster table,使 swap 设备开销逼近零。历史上 swap 有若干并列的元信息:swap_map(引用计数)、zeromap(zero-filled page 位图)、swap_cgroup 数组(memcg id 映射)、cached swap cache。这些数组按整盘大小静态分配,一块 1TB swap 设备需要数百 MB RAM 即使完全未使用。同时 anon 与 shmem 两条 swap in/out 路径长期各自实现大 folio 分配、memcg 充值、swap cache 插入,造成代码重复且同步边界不清。Phase IV 之前的 phase 已把 swap map 放进按需分配的 cluster table,本系列则把剩余的元数据(zeromap、swap cgroup id、memcg 关联)也合并进去,并且统一 anon / shmem 的 swap in 分配与充值路径。
mm/swap_cgroup.c 的独立 swap_cgroup_ctrl 数组与 zeromap bitmap 是按设备最大容量一次性分配,不随使用量伸缩。VM_FAULT_OOM leaked out to the #PF handler 的 retry 现象,反映分配同步薄弱。整体思路是把"每张外部 map"折叠进 per-cluster swap_table,并统一 anon/shmem 的 swap in 流水线。首先抽取 __swap_cache_prepare_and_add/__swap_cache_del_folio 通用 helper,新增 add_to_swap_cache_stable 等接口支持直接落位大 folio,THP gfp 辅助函数上移到头文件供 swap 路径复用;再把 anon 与 shmem 的大 folio swap in 分配合并成单一分配器,把 memcg 查找推迟到 folio 已进入 swap cache 后集中处理,并支持把不同 memcg 的 slots 批量释放。其次把 swap_table_alloc/free 改造为 swap_cluster_alloc_table/free_table 的集中接口,然后在每个 cluster 额外挂一张 1024/512 字节的 per-cluster memcg id 表,读写全在 cluster 锁下进行故无需 RCU;整盘 swap_cgroup_ctrl[MAX_SWAPFILES] 全局数组与 mm/swap_cgroup.c(-234 行)整体删除。最后在 swap table entry flags 中多挤出一位存 zero-filled 标记,__swap_table_set_zero/__swap_table_test_zero 取代 set_bit(offset, sis->zeromap);对 entry 位宽不够的 32 位架构,保留一个条件编译的 per-cluster zeromap 字段回退。改动后 struct swap_info_struct 去掉 unsigned long *zeromap,per-slot 静态开销从 ~2 字节降到 ~0.09375 字节(仅每 cluster 48 字节 ci info / 512 slot)。
作者给出了多个负载数据:
redis-benchmark -c 12 -P 32 -t get | ||||
make -j96 THP on | ||||
-n 48 1G | ||||
VM_FAULT_OOM leaked 计数 |
作者原文结论:"The static metadata overhead is now close to zero, and workload performance is slightly improved."
系列:[PATCH v6 0/4] mm/swap, memcg: Introduce swap tiers for cgroup based swap control作者: Youngjun Park版本: v6(4 个 patch)
由 Chris Li 建议的特性,目标是让系统里不同速度的 swap 设备(NVMe、SATA SSD、HDD、NBD/Network swap、zswap 后端)能被显式地分组,再让每个 memcg 选择自己允许 swap 到哪一组。现有机制只能给每个 swap 设备赋一个 priority(plist 顺序),然后 "highest priority first",无法按业务隔离——延迟敏感应用可能被迫换出到慢速设备,或者闪存 wear 无法按工作负载切分。LG 自己的部署场景(移动/嵌入式平台)里存在按冷/热 app 分流、按 VM 分流等需求。社区此前讨论过 BPF-based 的 swap device 选择,作者在 Part 2 的设计阐述里明确论证了:BPF 方案 "operate outside the memcg tree",容易与父 memcg 的约束冲突、在受限/嵌入式系统里不适合做核心 swap 行为,选择把控制面绑在 memcg 上更能复用 swap.max/zswap.writeback 等已有 hierarchy 语义。v6 相比 v5 主要是吸收 Sashiko AI review 与 syzbot CI 的修复。
实现分四层。第一层 mm/swap_tier.c/.h 引入 tier 基础设施:tier 是"按 priority 连续区间 + 用户命名"的分组,覆盖整个 DEF_SWAP_PRIO(-1) 到 SHRT_MAX 的 priority 空间。新增 CONFIG_NR_SWAP_TIERS(默认 4,范围 1–31)。接口为 /sys/kernel/mm/swap/tiers,写入语法 +name:prio 添加、-name 删除,支持单次 batch 输入:解析器左到右执行,先 swap_tiers_snapshot() 再逐 token 应用,任一步失败就 swap_tiers_snapshot_restore() 回滚;添加时按起始 priority 分裂/合并区间,合并方向向低位扩展以保留用户配置的起始值。第二层把 swap 设备挂接到 tier:struct swap_info_struct 新增 tier_mask,swapon 时根据设备 priority 匹配已配置 tier 区间;tier 的动态分裂/合并约束必须不改变已激活设备的归属。第三层是 memcg 接口:struct mem_cgroup 新增 tier_mask 与 tier_effective_mask(READ_ONCE/WRITE_ONCE 保护),并提供 memory.swap.tiers(读写,(+|-)TIER 语法)与 memory.swap.tiers.effective(只读,父 effective 与自身配置的交集);tier 继承 swap_tiers_memcg_inherit_mask() 推迟到 css_online(),关闭早期创建竞态窗口。第四层让 swap allocator 按 tier 过滤:swap_alloc_fast 取 folio_tier_effective_mask(folio) 与 percpu 缓存 si 的 tier_mask 比较,不匹配就落到 slow path;swap_alloc_slow 在 plist_for_each_entry_safe(si, ..., &swap_avail_head) 时用 swap_tiers_mask_test(si->tier_mask, mask) 跳过不合规设备。作者坦承存在两项已知限制,将在后续 per-tier swap_active_head 与 fast/slow path 优化中再解决。
测试环境为 LG 内部平台,用 NBD 作为独立 tier。该接口也为将来 intra-tier 分配策略、inter-tier promotion/demotion、MADV_SWAP_TIER 等演进铺好抽象底座。
系列:[PATCH v2] mm/alloc_tag: replace fixed-size early PFN array with dynamic linked list作者: Hao Ge版本: v2(1 个 patch)
Memory allocation profiling(CONFIG_MEM_ALLOC_PROFILING)通过为每个分配点关联一个 alloc_tag/codetag,把内存计数归属到源代码调用位置。该机制依赖 page_ext 保存每个 page 的 codetag ref,但在 boot 早期,page_ext 尚未初始化时已经发生了相当数量的 page 分配(例如 smp_init 期间 CPU hotplug 回调分配的 trace ring buffer、scheduler per-CPU 数据等)。这些 page 的 codetag ref 保持未初始化;当它们日后被释放时,alloc_tag_sub_check() 会触发 "alloc_tag was not set" 的 WARN。为了解决这个问题,现有代码用 clear_early_alloc_pfn_tag_refs() 在 page_ext 就绪后把这些早期 PFN 的 ref 显式清成 CODETAG_EMPTY。实现上,历史方案使用一个固定大小 EARLY_ALLOC_PFN_MAX = 8192 的 __initdata 数组 early_pfns[] 追踪这些 PFN,超出则 pr_warn_once 丢弃。原有注释已经留下 TODO:「Replace fixed-size array with dynamic allocation using a GFP flag similar to ___GFP_NO_OBJ_EXT to avoid recursion」。
alloc_page() 会造成递归:alloc_page() → post_alloc_hook() → pgalloc_tag_add() → __pgalloc_tag_add() → alloc_tag_add_early_pfn() → alloc_page() → ...,无限递归。early_pfn_pages 链可能丢页(leak),GFP_ATOMIC 在进程上下文里是无谓的收紧。核心思路是把固定数组替换成"slab of nodes"式的动态链表,并通过一个新 GFP 标志打断递归。数据结构:
structearly_pfn_node {structearly_pfn_node *next;unsignedlong pfn; };
#define NODES_PER_PAGE (PAGE_SIZE / sizeof(struct early_pfn_node))
staticstructearly_pfn_node *early_pfn_list;/* 已记录 PFN 的链表 */
staticstructearly_pfn_node *early_pfn_freelist;/* 空闲 node freelist */
staticstructpage *early_pfn_pages;/* 所有承载 node 的 page 链 */
alloc_early_pfn_node() 优先从 freelist 取空闲 node;若耗尽,则调用 alloc_page(gfp | __GFP_NO_CODETAG | __GFP_ZERO) 申请一页,按 NODES_PER_PAGE 切分:第 0 个 node 立即返回给调用者,其余 NODES_PER_PAGE - 1 个 node 用 cmpxchg 原子链入 freelist,page 自身则通过 page->private 串成 early_pfn_pages 链供最终回收。
打断递归的关键是新 flag __GFP_NO_CODETAG,它复用了 __GFP_NO_OBJ_EXT 这一 bit。pgalloc_tag_add() 的签名被扩展携带 gfp_t gfp_flags,__pgalloc_tag_add() 中通过 should_record_early_pfn(gfp_flags) 判断;alloc_tag 自身为了追踪而分配的 page 带上该 flag,__pgalloc_tag_add() 就直接 return,避免再次进入 alloc_tag_add_early_pfn()。
v2 的并发修正:early_pfn_pages 链的更新改用 cmpxchg 循环;同时根据 gfpflags_allow_blocking(gfp_flags) 选择 GFP_KERNEL vs GFP_ATOMIC,避免进程上下文里无谓地 pin 成 atomic。clear_early_alloc_pfn_tag_refs() 扫完 early_pfn_list 后,额外遍历 early_pfn_pages 对承载 node 的 page 本身 clear_page_tag_ref() 并 __free_page() 释放,整个基础设施干净消失,不残留 init 内存。
作者未提供性能数据,从代码逻辑推断的预期收益:消除了大 CPU / 大内存机器上因早期分配超过 8192 条而丢失追踪的"alloc_tag was not set" WARN 噪声,使 alloc tagging 调试在任意规模机器上都具备正确性;由于整个结构仅在 __initdata 路径活跃、在 page_ext 就绪后即被释放,运行时零开销;cmpxchg lock-free 链表也避免了在并发早期分配路径上引入新锁。
系列:[PATCH v2] mm/page_alloc: fix initialization of tags of the huge zero folio with init_on_free作者: David Hildenbrand版本: v2(1 个 patch)
Arm MTE(Memory Tagging Extension)在内核中依赖 __GFP_ZEROTAGS flag 把分配时的 tag memory 也一并清零。此前该 flag 与 __GFP_ZERO 语义紧密耦合:在 post_alloc_hook() 中,只有当内核确定要对页面 payload 做 zero init(即 init=true)时,才会把 zero_tags 也置为 true 并调用 tag_clear_highpages()。在通常路径下这是正确的——set_pte_at() 在第一次把页面映射到用户空间时会检测到 PG_mte_tagged 未设置,由其完成 tag 初始化。但 huge zero folio 是一个显著例外:它通过一个 special 标记的 PMD 被映射(大量 VMA 共享同一张只读零页),并不会走 per-page 的 set_pte_at() tag 初始化路径。于是 Documentation/arch/arm64/memory-tagging-extension.rst 中 "allocation tags are set to 0 when a page is first mapped to user space" 的约定被打破。作者通过修改 arm64 check_buffer_fill MTE selftest,改用 2 MiB 区域复现(测试 17 / 18 fails),定位到根因。
init_on_free=1 时,页面是在 __free_pages_prepare() 阶段被清零的,分配时 init=false,导致 zero_tags 被短路为 false,tag 根本没被清。tag_clear_highpages() 返回值语义"是否完成了页面清零"反直觉,易读错。核心是把 __GFP_ZEROTAGS 与 __GFP_ZERO 解耦,并让 tag_clear_highpages() 同时承担"仅清 tag"与"tag + payload 一起清"两种模式。接口改为 bool tag_clear_highpages(struct page *to, int numpages, bool clear_pages),返回值倒置为"调用方是否仍需自行初始化页面"。在 arm64 实现里,根据 clear_pages 在 mte_zero_clear_page_tags()(同时清 tag 与 payload)与 mte_clear_page_tags()(仅清 tag)之间选择;在 !system_supports_mte() 的 fallback 下直接返回 clear_pages,把责任还给通用代码。post_alloc_hook() 中则将 zero_tags 改为纯粹由 gfp_flags & __GFP_ZEROTAGS 决定,与 init 解耦:
constbool zero_tags = gfp_flags & __GFP_ZEROTAGS;
...
if (zero_tags)
init = tag_clear_highpages(page, 1 << order, /* clear_pages= */init);
同时在 gfp_types.h 里重写 __GFP_ZEROTAGS 注释,显式说明这不再是纯优化 flag,而是保证——即使在 init_on_free 场景下——tag 也会被清零。修改覆盖 arch/arm64/include/asm/page.h、arch/arm64/mm/fault.c、include/linux/gfp_types.h、include/linux/highmem.h、mm/page_alloc.c 五个文件,共 21 增 17 删。v2 相较 v1 撤回了基于 kasan_hw_tags_enabled() 的分支(会漏掉纯 user MTE 无 KASAN 的场景),并继续保留 system_supports_mte() 检查以确保无 MTE 硬件时 fallback 到通用 clear_highpage()。patch 带有 Cc: stable 标签。
作者未提供性能数据(本系列定位为 bug fix 而非优化)。从代码逻辑推断的预期收益:修复 arm64 在 init_on_free=1 配置下使用 huge zero folio 时的 MTE tag 信息泄漏 / 行为偏差,使 check_buffer_fill selftest 的 "Check initial tags ... mmap memory" 用例重新通过;同时通过解耦 __GFP_ZEROTAGS,为后续继续拆分 __GFP_SKIP_KASAN 的 cleanup 打好基础。该 fix 对运行时无额外开销——只是在 __GFP_ZEROTAGS 出现时多执行了一次本来就该做的 tag 清零动作。
系列:[PATCH RFC] mm, slab: add an optimistic __slab_try_return_freelist()作者: Vlastimil Babka版本: v1(RFC,1 个 patch)
本 patch 构建在近期合入的 "mm/slub: defer freelist construction until after bulk allocation from a new slab" 之上。SLUB bulk/refill 路径 __refill_objects_node() 会从一个 slab 中通过 get_freelist_nofreeze() 一次性 detach 整条 freelist:该函数将 slab 的 freelist 置空、把 inuse 设为 objects(从 slab 视角看 slab 已"满"),然后把原 freelist 整条交给调用者,调用者按需消费 max 个对象到目标数组 p[] 里。若 freelist 对象数超出 max,剩余那段就必须"还回去"。老代码的做法是:把剩余段当作 detached freelist,走遍一遍链找到尾 object,然后调 __slab_free(s, slab, head, tail, cnt, _RET_IP_)。这条完整 __slab_free 路径要处理 debug、NUMA partial 管理、cmpxchg 竞争回退等诸多一般性场景,而且首先要从冷缓存里 walk 整条剩余 freelist 来定位 tail,代价不低。
__slab_free() 处理了许多在本场景不可能出现的情形,额外的分支和锁路径成本浪费。get_freelist_nofreeze() 原子地把 slab 的 freelist 清零,在极短时间窗内 slab 大概率还"看起来是满的"(没有并发 __slab_free() 往里 push),应当利用这个假设。新增 __slab_try_return_freelist(s, slab, head, cnt) 走"乐观快路径",只在仅限非 debug cache 的场景使用:
old.freelist = slab->freelist;
old.counters = slab->counters;
if (old.freelist) /* 并发 free 已经挂过东西,快路径放弃 */
returnfalse;
new.freelist = head; /* 直接把剩余段整段接回 */
new.counters = old.counters;
new.inuse -= cnt;
/* 拿 n->list_lock,cmpxchg 更新,add_partial(ADD_TO_HEAD),完成 */
关键是:只需要 head 和 cnt,完全不需要 tail——因为 slab 的 freelist 当前为空,把 head 设为新 freelist 头、inuse -= cnt 即可,原 freelist 的 NULL 终结符天然充当新 freelist 的尾。slab 从"全满"变成"有 cnt 个空位",因此要无条件挂到 n->partial 的头部(ADD_TO_HEAD),并记账 FREE_ADD_PARTIAL。
失败退化:若 old.freelist != NULL(说明另一个 CPU 已经向此 slab __slab_free() 了至少一个对象),或 slab_update_freelist() cmpxchg 失败,函数返回 false;调用方退回原路径,walk 定位 tail 再调 __slab_free(),并累加 REFILL_RETURN_SLOW stat。配套 get_freelist_nofreeze() 改为通过出参 unsigned int *count 返回 old.objects - old.inuse,refill 循环用 count 驱动而不再用 NULL 终结符判断,这样调用 __slab_try_return_freelist() 时直接拿到准确的 cnt。两个新 SLUB_STAT:REFILL_RETURN_FAST、REFILL_RETURN_SLOW,作者称已在调试中观察到乐观路径成功率相当高("show to me the optimistic path is indeed successful")。
作者未提供性能数据,这是明确的 RFC——他写道:"it seems like there should be no downsides (in theory...) so please test if it indeed improves things anywhere and then it could be a better baseline before trying anything that comes with tradeoffs"。从代码逻辑推断的预期收益:refill 归还路径在快路径命中时省掉一次完整的剩余 freelist 链 walk(避免 tail 定位的 cache miss),__slab_try_return_freelist() 较 __slab_free() 指令路径更短、分支更少;作为 refill spilling 讨论的"零 trade-off 基线",它希望先吃掉容易摘的低垂果实,再评估那些有代价的后续优化。
系列:[PATCH 0/2] mm/kmemleak: dedupe verbose scan output作者: Breno Leitao版本: v1(2 个 patch)
kmemleak 是内核内存泄漏检测器:周期性扫描内核数据段和所有已知分配的对象,把未被引用的对象标记为可疑泄漏。在 kmemleak_verbose 模式下(/sys/module/kmemleak/parameters/verbose=1),每一个被判定为 unreferenced 的对象都会以完整格式打印到 dmesg——包括对象头、大小、comm/pid/jiffies、十六进制 dump,以及通过 stackdepot 保存的 16 帧分配栈。作者的动机是:"I am starting to run with kmemleak in verbose enabled in some 'probe points' across the my employers fleet so that suspected leaks land in dmesg without needing a separate read of /sys/kernel/debug/kmemleak"。这样做的副作用是:某些 workload 从单一调用点泄漏成百上千个对象时,dmesg 会被字节级完全相同的 backtrace 淹没——"Hundreds of duplicates per scan are common, drowning out distinct leaks and unrelated kernel messages, while adding no signal beyond the first occurrence"。
object->lock 是 raw spinlock,在其持有期内调用 kmalloc(GFP_ATOMIC) 会取到 n->list_lock 这类更高 wait-context 的锁,被 lockdep 判为非法嵌套。/sys/kernel/debug/kmemleak 与 "N new suspected memory leaks" 计数必须保持不变。在 kmemleak_scan() 的 reporting 阶段引入每次 scan 的 dedup xarray,以 stackdepot 的 depot_stack_handle_t trace_handle 作为 key:
structkmemleak_dedup_entry {
structkmemleak_object *object;/* 第一个代表性对象 */
unsignedlong count; /* 同 backtrace 的泄漏对象总数 */
};
扫描循环中,持有 raw_spin_lock_irq(&object->lock) 时只是读取object->trace_handle 到局部变量,随即释放锁;锁外再调用 dedup_record(&dedup, object, trace_handle)。dedup_record() 先 xa_load 查 key:命中就 entry->count++;miss 则 kmalloc(GFP_ATOMIC) 新 entry,并 get_object(object) 增加 use_count 以便跨 RCU 存活,再 xa_insert。若 kmalloc 失败或 xa_insert 冲突,回退到立即打印(保留"每条泄漏都被记录"的语义)。扫描结束后 dedup_flush(&dedup) 遍历 xarray 打印每个代表对象,并在尾部追加一行 ... and N more object(s) with the same backtrace;然后 put_object() 释放引用,kfree entry,xa_erase,xa_destroy。并发正确性的两点关键:xarray 操作与 GFP_ATOMIC kmalloc 必须在 object->lock 外(避免 raw spinlock 下再抓更高 wait-context 的 slab 锁);stash 的对象指针通过 get_object() 提升的引用计数跨 rcu_read_unlock() 保命,dedup_flush() 打印后再 put_object() 释放。
同系列还新增了 tools/testing/selftests/mm/test_kmemleak_dedup.sh:依赖 CONFIG_SAMPLE_KMEMLEAK=m 的 kmemleak-test 模块——该 sample 在单个 kzalloc() 调用点泄漏 10 个 list entry,共享一个 stackdepot trace_handle;脚本启用 verbose、触发两轮 scan,断言 printed < new_leaks。
作者未提供定量 benchmark 数据,从代码逻辑和 cover letter 描述可推断的预期收益:封面信原话 "Hundreds of duplicates per scan are common"——在高频泄漏场景,单次 verbose scan 的 dmesg 输出量从 O(泄漏对象数) 降至 O(唯一 backtrace 数),极大减少 printk 对 console 的压制和 ring buffer 的冲刷,让"真正不同"的泄漏不再被一个高频泄漏 flood 掉。selftest 直接用 printed < new_leaks 作为 PASS 条件,典型示例输出 "PASS: kmemleak verbose output deduplicated (11 printed for 61 leaks)"——从 61 行缩到 11 行(~82% 缩减)。因为仅改动 verbose reporting 代码路径、/sys/kernel/debug/kmemleak 内容和 "N new suspected memory leaks" 计数保持原样,对现有 tooling 向后兼容。
系列:[PATCH v1 0/3] mm: process_mrelease: expedite clean file folio reclaim and add auto-kill作者: Minchan Kim版本: v1(3 个 patch)
process_mrelease(2) 是 Android LMKD 在内存压力下加速回收被杀进程内存的核心 syscall。但现有实现有两个长期痛点。其一:process_mrelease 只 unmap victim 的 VMA,clean file-backed folio 仍躺在 page cache 里,必须等 kswapd 或 direct reclaim 才能真正释放;在 LMKD 场景下这一延迟会逼迫系统多杀几个无辜后台 app 才能腾出足够内存。其二:UAPI 要求用户态先发 SIGKILL,再拿 pidfd 调用 process_mrelease。这构成一个调度竞态——victim 可能在 reaper 被调度前就走完 do_exit → exit_mm 把 task->mm 清空,process_mrelease 于是返回 -ESRCH;而真正的 exit_mmap 会被推迟到 mm_users 归零,Android 里各类 /proc/<pid>/cmdline 读取、async I/O、远端 VM 访问常会让该引用计数长时间挂着,expedited reclaim 彻底失效。本系列建立在此前的 RFC(2026-04-13)之上,依 David Hildenbrand 的反馈做了 API 与语义调整。
process_mrelease 后仍占据 page cache,阻塞内存回收。folio_mark_accessed()/LRU 移动对即将释放的独占 file folio 是纯浪费——perf 显示其占 unmap_page_range 55.75%。process_mrelease 之间存在不可消除的调度竞态,造成 -ESRCH 与回收被推迟。整套方案围绕一个统一旗标 MMF_UNSTABLE 形成闭环:把 free_pages_and_swap_cache() 统一重命名为 free_pages_and_caches() 并迁入 mm/swap.c,使它对 anon 与 file 两路对称:anon folio 走 free_swap_cache(),file folio 在 mm_flags_test(MMF_UNSTABLE, mm) 为真(即由 OOM reaper 驱动)时调用新引入的 free_file_cache(),后者在 folio_trylock() 成功后通过 mapping_evict_folio() 主动把 folio 从 pagecache evict 掉。在 zap_present_folio_ptes() 中加入短路:当 MMF_UNSTABLE && !folio_maybe_mapped_shared(folio) 时跳过 folio_mark_accessed(),避免把即死的独占 file folio 搬来搬去。UAPI 侧引入 flag PROCESS_MRELEASE_REAP_KILL(aliased 到 1 << 0,并加 PROCESS_MRELEASE_VALID_FLAGS 掩码),把 "kill + reap" 合并为一次原子操作;关键做法是 在发 SIGKILL 之前mmget(mm)(不是 mmgrab)——保持 mm_users > 0,阻止 victim 自身的 exit_mmap,让 reaper 在 process_mrelease 上下文里同步、确定地完成回收:
mmget(mm); /* pin mm_users */
task_unlock(p);
ret = kill_pid(task_tgid(task), SIGKILL, 0);
...
__oom_reap_task_mm(mm);
mmput(mm);
task_will_free_mem() 也新增 ignore_exit 参数,在 reap_kill 路径跳过 SIGNAL_GROUP_EXIT 检查。
作者提供了 perf 数据证明 LRU overhead 的存在:
unmap_page_rangefolio_mark_accessed 占比 | exit_mm → exit_mmap perf call graph | |
__folio_batch_add_and_move | ||
folio_remove_rmap_ptes | ||
page_table_check_clear |
绝对时延 / LMKD 端到端数据未给出,从代码推断预期收益为:LMKD 场景下单个 victim 的 file-backed 内存可在 reaper 上下文内同步释放,消除 kswapd 依赖与 ESRCH 竞态,减少"多杀"造成的用户可见 app kill。
系列:[PATCH RFC v3 00/19] mm/virtio: skip redundant zeroing of host-zeroed reported pages作者: Michael S. Tsirkin版本: v3 RFC(19 个 patch)
virtio-balloon 的 free page reporting 机制允许 guest 把自己不再使用的 free page 告诉 hypervisor,host 侧通常以 MADV_DONTNEED 等方式回收这些页的 backing memory——这一动作天然要求 host 把页内容置零并使 guest 侧 cache line 失效。问题在于,当 guest 随后重新 allocate 这些页并把它们交给用户态时,内核又会零一遍(kernel_init_pages()、clear_user_highpage() 或 folio_zero_user()),这是一次彻底冗余的 2 MB 级别 memset。更糟的是,在 VIPT 等 aliasing cache 架构上 + init_on_alloc=1 配置下,用户页会被 双重 清零一次:先 post_alloc_hook 里的 kernel_init_pages(),再在 callsite 由 user_alloc_needs_zeroing() 触发一次 clear_user_highpage()。本 RFC 一次性解决两类问题:(a)在 mm core 把用户页零清统一进 post_alloc_hook;(b)通过 PG_zeroed / HPG_zeroed 两个 flag 把 "host 已零"的信息从 page reporting 一直传播到 buddy 分配路径的消费端。
user_alloc_needs_zeroing() 语义使 callsite 和 allocator 互相不信任对方是否 zero 过。PG_zeroed(消费于 allocation 时)无法跟踪其状态。方案由两部分交织组成。MM core 侧把用户 fault 地址 user_addr 通过 struct alloc_context 从 vma_alloc_folio() 一路贯穿到 post_alloc_hook()(__alloc_pages、__folio_alloc、folio_alloc_mpol、__alloc_frozen_pages 公开 API 全部获得新参数,引入 USER_ADDR_NONE 哨兵值);把 prep_compound_page() 前移到 post_alloc_hook() 之前(否则 folio_nr_pages() 在 higher-order 上返回 1,只会 zero 首页);在 post_alloc_hook 的 init 分支里,当 (gfp_flags & __GFP_ZERO) && user_addr != USER_ADDR_NONE 时改用 folio_zero_user()——它会把 fault 地址附近的 cache line 留到最后清,保证那些行在随即返回用户态时仍 hot。callsite 侧(vma_alloc_zeroed_movable_folio、alloc_anon_folio、vma_alloc_anon_folio_pmd、hugetlb fault / fallocate、memfd)改为直接传 __GFP_ZERO 并移除自己的 folio_zero_user(),顺手把 alpha / m68k / s390 / x86 上冗余的 arch override 删掉。
跟踪侧新增 PG_zeroed 复用 PG_private 位(从 PAGE_FLAGS_CHECK_AT_PREP 里排除),由 page_reporting_drain 在 host zero 后打上,由 __free_pages_ok / __free_frozen_pages 在 balloon deflate 走 FPI_ZEROED 打上;buddy merge 时"两者皆 zero 才保留"、split / expand 时向子 buddy 传播;post_alloc_hook 消费并清除。hugetlb 单独走 HPG_zeroed(存在 hugetlb folio ->private),在 alloc_surplus_hugetlb_folio 置位、free_huge_folio 清除,由 alloc_hugetlb_folio / alloc_hugetlb_folio_reserve 通过新增 bool *zeroed 输出给调用者。
测试方法为作者提供的 alloc_once.c + bench.sh:mmap(..., MAP_PRIVATE|MAP_ANONYMOUS[|MAP_HUGETLB]) 后 madvise(MADV_POPULATE_WRITE) 再 munmap;每轮前 echo 512 > /sys/module/page_reporting/parameters/flush 触发 reporting,再以 perf stat 采样,n=10 取均值与 stddev。作者注明:优化在 THP 场景效果最佳,因为 2 MiB 页会整块地从 reported order-9 buddy 取出;无 THP 时由于低阶 fragmentation,只有约 21% 的 order-0 分配命中 reported 页。Persistent hugetlb pool 不受益,surplus hugetlb 则受益明显。RFC 仍在推进 virtio spec 侧变更,但作者显式请求 MM 部分(前 10 个 patch)先行独立 merge。
| Swap Table Phase IV | swap_table 吞并 zeromap/swap_cgroup,删除 mm/swap_cgroup.c,anon/shmem swap-in 统一 | |
| Swap Tiers for cgroup | /sys/kernel/mm/swap/tiers 与 memory.swap.tiers(.effective),按 cgroup 选择 swap 速度档 | |
| kmemleak verbose dedup | ||
| process_mrelease auto-kill + file folio evict | PROCESS_MRELEASE_REAP_KILL flag;reaper 上下文主动 mapping_evict_folio clean file folio | |
| virtio skip redundant zeroing | PG_zeroed/HPG_zeroed 标志把 host 已清零信息传递到 buddy 分配端,跳过冗余清零 |
| zsmalloc zs_free_deferred | ||
| memory hotplug zone contiguous O(1) | ||
| kernel_init_pages batch clearing | ||
SLUB __slab_try_return_freelist() | REFILL_RETURN_FAST/SLOW 计数器;无 benchmark |
| huge zero folio MTE tag 初始化(重要) | adfb6609c680;Cc: stable;init_on_free 下修复 MTE tag 信息泄漏,恢复文档承诺;selftest 17/18 通过 | |
| mmap_prepare 兼容路径 detached VMA 误 unmap | ac0a3fc9c07d;Cc: stable;syzbot 报告;堆叠驱动下消除 vma_mark_detached WARN | |
anon_vma_name_reuse() NULL 返回处理 | anon_vma_name_reuse() NULL 返回值做 NULL check,避免潜在解引用 | |
hugetlb_cma per_node 日志取整 |
| alloc_tag 早期 PFN 动态链表 | __GFP_NO_OBJ_EXT 作 __GFP_NO_CODETAG 打破递归 | |
mm/gup pgtable entry accessors 统一 | ||
mm/damon/sysfs filters/ 标记 deprecated | ||
selftests/mm .gitignore 模式匹配 |