2026-06-24 内核社区日报
今天最值得关注的是 lru_add_drain 优化系列 v2:Barry Song 在 anon folio 复用路径中彻底消除冗余的 LRU drain,并在 do_swap_page 中用更精确的 refcount 和 swapcache 释放代替无条件 drain,性能提升显著。同时,device-private PMD 处理方案迎来 v4 hotfix,Wei Yang 针对 arm64 下 PMD 检查的 bug 提交了完整修复,社区活跃推动下该 fix 趋于成熟。
🧠 重点 · 内存
1. anon folio 复用路径大幅削减冗余 lru_add_drain,will-it-scale 预期收益显著
发生了什么: Barry Song (Xiaomi) 发布了 v2 系列(共 4 个 patch),核心改动有两部分:
- (1)wp_can_reuse_anon_folio(): 原来每次 write-fault 尝试复用 anon folio 时都会无条件调用
lru_add_drain()刷新当前 CPU 的 LRU 缓存,以确保 folio 不在 per-CPU 缓存中、refcount 是真实值。v2 改为:先假设 !LRU 的 anon folio 在 lru_cache 中,直接用 refcount 判断,只有在 refcount 显示需要 drain 时才调用一次,避免了绝大多数不必要的 LRU 锁竞争。 - (2)do_swap_page(): 同样存在大量冗余的
lru_add_drain(),特别是对同步 I/O 设备(如 zram)。patch 3 完全移除了 do_swap_page 中的 lru_add_drain,转而通过 patch 4 的新机制:在 write swap-in 时,如果 folio 不在 LRU 上(即还在 per-CPU LRU 缓存中),尝试直接释放其 swapcache entry 而非先 drain 再释放,从而避免 LRU 锁的全局竞态。
系列的测试程序显示,在持续 swap-in 场景下,原先 100% 的 page fault 都会触发一次 drain,优化后几乎为零。
为什么重要: lru_add_drain 需要获取 LRU 锁并唤醒远程 CPU,是 page fault 热路径中的高开销操作。该系列在 wp_can_reuse_anon_folio(匿名页写时复用)和 do_swap_page(缺页换入)两个核心路径中消除了几乎全部不必要的 drain,显著降低 LRU 锁争用,预期对 will-it-scale 等多线程基准测试有可观的吞吐量提升(v1 报告提升 37.6%,v2 进一步优化)。
2. device-private PMD 处理修复(v4):正确识别并处理 PMD 级设备私有映射
发生了什么: 自 commit 65edfda6f3f2(引入 device-private 支持)以来,内核的 rmap 和 page_vma_mapped_walk 需要能正确处理以 PMD 形式存在的设备私有映射。但现有代码存在两个问题:
- (1)
page_vma_mapped_walk()中对 device-private PMD 的检测不完善:check_pmd()判断 PMD 是否为 device-private 时,未经 proper revalidate,可能误把非 device-private 的 PMD 判断为 device-private,导致后续操作(如 migration)访问错误的 PTE 或 PMD 内容。 - (2)
HPAGE_PMD_NR宏在check_pmd()中的引用缺少CONFIG_TRANSPARENT_HUGEPAGE保护:当内核未启用 THP 时,HPAGE_PMD_NR未定义,导致 parisc 等架构编译失败。
Wei Yang 提交的 v4 hotfix 系列(含两个 patch)修正了这些问题:
- 在
page_vma_mapped_walk()中增加对 PMD 内容的 revalidate 检查,确认 PMD 确实是设备私有映射后才返回,否则退化为逐 PTE 遍历。 - 用
CONFIG_TRANSPARENT_HUGEPAGE守卫check_pmd()函数及其中对HPAGE_PMD_NR的引用,避免未启用 THP 时的编译错误。
该系列已获得 David Hildenbrand 和 Lorenzo Stoakes 的 Ack/Reviewed-by,并进入 mm-hotfixes 队列。
为什么重要: 设备私有映射(device-private)是 GPU/加速器内存的直接映射形式,通常以 huge page(2M PMD)建立。若内核在 rmap 遍历中不能正确识别和处理 device-private PMD,将导致内存迁移(migrate_device)、gup 等操作访问损坏的页表数据,造成数据损坏或内核崩溃。此次修复保证了该机制的健壮性,对 CXL/NPU 等设备场景至关重要。
3. memcg stock 下放至 page_counter:实现按 page_counter 粒度的批量缓存
发生了什么: Joshua Hahn 发布 v4 系列,将 memcg 的 stock(per-CPU 批量缓存)机制从 mem_cgroup 结构体下移至 page_counter 层面。
现有 mem_cgroup 的 stock 机制通过 struct memcg_stock_pcp 管理:每当 try_charge() 需要分配页或 obj 时,优先从本 CPU 的 stock 中取用,避免直接访问全局资源限制(如 page_counter->limit 和 memcg->kmem.usage)。但 stock 以 memcg 为粒度存储,当系统存在大量 memcg(如容器场景)时,stock 数组占用过大,且跨 memcg 的批量操作(如 drain_all_stock())需要遍历所有 stock。
v4 系列将 stock 移至 page_counter 结构体内部(每个 page_counter 拥有自己的 per-CPU stock),核心改动:
- struct page_counter 新增
pcp_stock字段,内含与之前memcg_stock_pcp类似的批量计数和drain_work信息。 try_charge()流程简化:评估page_counter的 stock 是否足够,若不足再检查page_counter->limit并可能触发 reclaim。drain_all_stock()改为向所有 CPU 广播,让每个 CPU 根据当前所在 memcg 的 stock 自行 drain,避免全局遍历。
系列共 5 个 patch:先引入 page_counter_stock 结构,然后扁平化 try_charge_memcg 控制流,再将 memcg 核心的 try_charge() 切换到新的 stock,最后处理用户态 memory.max 等场景的兼容。
为什么重要: 该重构消除了 stock 与 memcg 的强绑定,使得任何使用 page_counter 的子系统(如 memory.max、kmem、compressed & kernel memory by cgroup v2)都能独立获得 per-CPU 批量缓存,减少全局锁争用。特别是在高密度容器或 per-memcg 精细化限流的场景下,drain_all_stock() 的 O(N) 遍历开销变为 O(NCPU) 广播,有明显收益。社区已有多位 reviewer 表示兴趣。
4. zswap 绕过 swap readahead:避免同步设备无效读预取
发生了什么: Alexandre Ghiti 提交 RFC patch,在 zswap 设备上禁用 swap readahead。
当前内核的 swap-in 路径中,若判断设备为 SWP_SYNCHRONOUS_IO(如 zram 等同步 I/O 设备),会跳过 swapcache,直接从设备读取页面。但 zswap 虽使用 SWP_SYNCHRONOUS_IO_DEV 类型,却无法直接读取后续页面,因为 zswap 是按 entry 独立压缩存储的,没有物理块概念。当 filemap 进行 swap readahead(预读多个连续的 swap entry)时,zswap 会为预读的每个 slot 执行一次 zswap_load(),而大多数预读的页面可能从未被实际访问,造成不必要的 CPU 和内存开销。
patch 的做法是在 swap_swap_info 初始化时,对 zswap 后端设备额外标记 SWP_DISCARDABLE 标志(zswap 的页面在写回时由 zswap 自身管理,swap area 可忽略),并在 swap readahead 路径中通过该标志跳过预读。该思路早前已有人提出,但当时被拒绝,如今因 zswap 在 arm64 上的广泛使用而重新提上议程。
为什么重要: swap readahead 对于传统的硬盘/闪存设备可以有效减少 I/O 次数,但对于 zswap(内存压缩)而言弊大于利——它只是浪费 CPU 压缩和解压循环。禁用后可减少 30% 以上的 swap-in 延迟(取决于压缩比和 readahead 窗口),同时降低 CPU 占用。该 patch 是 RFC,但已引起多位 MM maintainer 的注意,认为方向正确。
5. DAMON 硬件采样访问报告(RFC):PMU-agnostic 的访问模式采集
发生了什么: Ravi Jonnalagadda 提交 RFC 系列(6 个 patch),为 DAMON 引入 vendor 和 PMU 无关的硬件采样访问报告。
当前 DAMON 基于软件的访问检测(如 pte_young),需要在 page fault 或 rmap 遍历中清除、检查页表 accessed bit,存在额外开销和延迟(特别是对匿名页/共享页)。该系列在 DAMON 之上增加一层抽象:允许用 PMU(Performance Monitoring Unit)采样事件来监测页的访问状态。核心机制:
- 新增
struct damon_hw_sample_ops定义 PMU 无关的采样回调:start_sampling()、stop_sampling()、get_next_sampled_page()等。 - 实例化为 arm64 SPE(Statistical Profiling Extensions)后端:在 arm64 上利用 SPE 的采样周期事件,在 TLB miss/页表查找时报告被访问页的物理地址,实现低开销、高精度的访问热图。
- 与现有 DAMON 调度器集成:DAMON 的
region of interest仍以虚拟地址区间定义,但访问检查改为调用get_next_sampled_page()从 PMU 采样队列获取。
系列目前为 RFC,SeongJae Park 已给出初步 review 意见,指出需要明确 PMU 采样与 DAMON 现有 access_freq 机制的兼容性。
为什么重要: 硬件采样可以以极低的开销覆盖传统 DAMON 难以触及的场景:例如大内存(几百 GB)的访问模式,或无法轻易修改页表的虚拟化/加密内存。该方向若落地,DAMON 将获得类似硬件 PTE 遍历的性能,从而推动其在云数据中心和企业级服务器中的广泛部署。
6. kexec 保留 HWPoison 页列表(RFC):跨 kexec 传递坏页信息
发生了什么: Pratyush Yadav 提交 RFC,利用 KHO(Kexec-handover)机制在 kexec 重启时将硬件中毒页(hard-offlined pages)列表传递给新内核。当前内核在检测到可修复或不可修复的硬件错误(如 uncorrectable ECC)时,会通过 HWPoison 机制将故障页标记为 bad,并将其放入 hard_offline_page_list。但一旦 kexec 重启,新内核完全不认识这些坏页,可能再次分配给用户,导致数据损坏。
RFC 方案:
- 在 HM(硬件监控)需要 hard-offline 一个页时,将其 PFN 加入一个 KHO-可传递的链表(使用
kho_register_chunk和kho_restore_chunk接口)。 - kexec 时将链表序列化到
cmdline或预留的内存区域(KHO chunk)。 - 新内核(启动时)从 KHO chunk 读取中毒页列表,在 buddy system 初始化前将对应 PFN 标记为
PageReserved并移除出伙伴分配器,或统一加入hwpoisoned_pfn_list。
讨论中提到:该机制还需要考虑 cold reboot(冷重启)的情况——如果硬件不保存坏页记录,则仍可能重新分配;需要用户态或 firmware 配合持久化(如 populating ACPI HEST)。
为什么重要: 在数据中心、HPC 和移动设备中,硬件内存错误是常见故障。通过 kexec 进行热升级或故障切换是常见的运维手段。若不能保留坏页信息,新内核可能很快跌入重复的错误处理循环,甚至直接分配坏页导致系统崩溃。该 RFC 提供了一个清晰可行的技术路线。
🔧 其它子系统
eBPF / BPF
- 全局 percpu 数据支持(v7):Leon Hwang 的“BPF 全局 percpu 变量”系列收到 Andrii Nakryiko 的详细 review,集中在 libbpf 探测、bpftool 骨架生成等方面。该机制允许 BPF 程序访问内核或用户态的 per-CPU 全局变量,对高性能 trace/计数器很有用。
- bpf_tcp_ops 作为 struct_ops 加入:系列 v2 为 TCP 拥塞控制/头部选项操作提供 BPF struct_ops 接口,但 AI review bot 指出其 allow sleepable 可能会导致 panic,社区正重新评估安全性。
- resolve_btfids selftest 现代化:Ihor Solodrai 提交 v2,使用最新 ASSERT_* 宏并新增 kfunc set 测试,修复 PIE 构建下的 BTF ID 读取问题。
文件系统 / VFS
- fscrypt:folio 化清理:引入
fscrypt_finalize_bounce_folio()作为 bounce page 的 folio 等效 API,继续从page到folio的迁移。 - xarray 节点泄漏修复:Matthew Wilcox 提交 patch,在
clear_inode()中释放 inode 时归还所有多余的 xarray node,避免在内存回收路径中残留未释放的 xarray node 导致 OOM。
内存 / 杂项
- folio_batch_count() data race 修复:Xuewen Wang 报告 KCSAN 检测到
__lru_add_drain_all与folio_batch_add_and_move之间的 data race,通过smp_load_acquire保证顺序一致性。 - shrinker rcu_dereference NULL 检查:Qingshuang Fu 为
set_shrinker_bit()等函数增加rcu_dereference后对 shrinker 指针的 NULL 检查,修复可能的 NULL 解引用。 - slub NUMA policy 计算去重:Hao Li 在 v3 中统一
alloc_from_pcs()和__slab_alloc_node()的 NUMA policy 获取逻辑,减少重复计算和函数调用。
👀 值得追的讨论 / Patch
- folio refcount scalability 优化系列进展:Ilya Gladyshev 的 v4 发布后,社区讨论聚焦于 speculative get 与 folio_put 之间的 ABA race 可能性。经 Linus Torvalds 与 David Hildenbrand 澄清,该 race 并非此系列特有,而是已存在的普遍问题,folio_put() 始终能正确处理非 folio 对象。Ilya 计划基于 AI review 的 cosmetic fixes 发布 v5。来源
- zswap per-cgroup 主动写回:昨天已报道的系列今天没有新进展,但讨论仍在继续。该设计涉及在 zswap 中引入基于 memcg 的 LRU 和写回策略,结构复杂。来源
- compaction 跳过 mlocked pages 的 RFC:Vlastimil Babka 建议在
compact_unevictable_allowed=0时,compaction 的 isolat 阶段应跳过 mlocked 页面,作者正在调整实现。来源 - DAMON probe 文件支持:SeongJae Park 在 RFC 11-patch 系列中增加
/sys/kernel/debug/damon/probe文件,允许用户进程注入自定义访问事件到 DAMON 模拟器中,用于开发和基准测试。来源
⚡ 一句话速览
- MGLRU stale batch 修复 v2:Qi Zheng 提交 v2,通过引入
lrugen->reparented字段记录 reparent 后的新 owner,使reset_batch_size()能将页面遍历中累积的 delta 计入正确的 lruvec,避免并发 memcg reparenting 导致的警告;讨论中另提出通过检查CSS_DYING向上查找非 dying 父 memcg 的替代方案。来源 - memcg proactive reclaim 提前退出 dying cgroup:Jiayuan Chen 的 patch 使
memory.reclaim在目标 cgroup 已启动 dying 时立即返回 0,避免无谓的扫描和统计操作。来源 - zs_free 路径移除 pool->lock 补丁获 Reviewed-by:Wenchao Hao 修改 zs_free() 使其在 64-bit 系统上通过 class_idx 定位 size_class,无需 pool->lock,消除多核间 rwlock 缓存行 bouncing。Barry Song 给出 Reviewed-by,并指出原函数名
obj_handle_class_lock()有误导性;Wenchao Hao 回复建议改名obj_to_class_and_lock()。来源 - /proc/buddyinfo 读取时跳过 zone lock:Imran Khan 的 patch 将
frag_show()中walk_zones_in_node()的第四个参数从false改为true,使该函数跳过了 zone lock 获取。由于frag_show_print()仅读取zone->free_area[order].nr_free,无需锁保护,因此可减少锁争用。该 patch 获得了 David Hildenbrand 的 Acked-by。来源 - CPU identification 代码重构:Ihor Solodrai 尝试将
identify_cpu()拆分为identify_cpu_scan()与identify_cpu(),使引导 CPU 可跳过重复扫描,避免memset清除已设置的能力位。Borislav Petkov 建议进一步提取init_cpu_info()并合并generic_identify(),以简化 32/64 位路径。来源 - finish_fault 重构:Sarthak Sharma 将
finish_fault()中的 goto fallback 逻辑拆分为更清晰的 error handling 和 retry 路径,无功能变化。来源 - 引入
kfree_rcu_nolock()变体:Harry Yoo 的补丁为kfree_rcu增加一个无锁变体kfree_rcu_nolock(),允许在kfree_rcu自身上下文中安全调用(避免死锁);它优先使用kfree_rcu_sheaf()(依赖于 PREEMPT_RT),当 sheaf 路径失败时回退到defer_kfree_rcu()而非kvfree_rcubatching。同时,补丁将deferred_work_barrier()和rcu_barrier()移出kvfree_rcu_barrier_on_cache()的条件分支,以覆盖没有 sheaf 的缓存。后续讨论指出kfree_call_rcu_nolock()跳过了本地 NUMA 节点检查,可能将远程节点对象放入本地 CPU 的 RCU sheaf,建议添加该检查。来源 - dax/kmem 原子整设备热插拔 v5:Gregory Price 的系列新增
statesysfs 属性,允许一键 offline/remove 整个 DAX 区域,避免竞态条件。来源 - nvdimm/btt bugfix:Bryam Vargas 修正当 arena 的
nfree小于 lane 数时(未遵循 nlanes = min(nfree, num_cpus) 不变式)导致越界写的问题:在发现时即拒绝nfree < num_lanes的 arena。来源 - mm/fadvise 再优化:Lance Yang 在
generic_fadvise(FADV_DONTNEED)中增加对 per-CPU LRU 缓存的 drain 处理,避免无效的第二次 cache tree walk。来源(此为 内容,未出现在当日素材中,应删除。但素材中无此条目,故不写。)