系列:[RFC PATCH 00/11] Optimize this_cpu_*() ops for non-x86 (ARM64 for this series)作者: Yang Shi版本: RFC v1(11 个 patch)
x86 架构通过 GS 段寄存器可以直接访问 per-CPU 变量,无需关闭抢占,this_cpu_*() 操作非常高效。但在 ARM64 等非 x86 架构上,this_cpu_*() 必须通过 preempt_disable_notrace()/preempt_enable_notrace() 保护临界区——因为 per-CPU 变量的虚拟地址在每个 CPU 上不同:per_cpu(var, cpu) = per_cpu_offset[cpu] + &var。这导致每次 this_cpu_*() 操作都要执行两次不必要的 preempt 计数原子操作。在 AmpereOne 160 核等大规模多核 ARM64 场景下,大量 per-CPU 操作路径(内存统计、memcg、rss 计数器、rmap 等)均受此开销影响。该系列实现了 LSFMM 2026 提案中的方案:为每个 CPU 创建独立的 per-CPU 页表,将 per-CPU 变量映射到相同虚拟地址,从而消除对 preempt 操作的依赖。
this_cpu_*() 操作必须被 preempt_disable_notrace()/preempt_enable_notrace() 包围,高核数场景下原子操作开销显著核心思路是为每个 CPU 创建独立的 pgd 页表,在 ARM64 内核虚拟地址空间中划出两个 per-CPU 专用区域:全局 per-CPU 区域(Global Percpu)和本地 per-CPU 区域(Local Percpu),每个区域对齐到 PGDIR_SIZE 边界以最小化页表同步开销。per-CPU 分配器不再从 vmalloc 空间分配,而从专用的 PERCPU_START 区域分配。
关键技术路径:为每个 CPU 创建独立的 pgd 页表,所有 CPU 的 pgd 共享相同页表内容,仅 pgd 层级不同。本地 per-CPU 区域被映射到各 CPU 自己的页表中——每个 CPU 看到的本地 per-CPU 虚拟地址相同,但映射到不同的物理页。引入 __per_cpu_local_off(一个对所有 CPU 都相同的偏移量),配合新宏 local_cpu_ptr() 替换 raw_cpu_ptr() 直接计算 per-CPU 地址,完全移除 preempt_disable_notrace()/preempt_enable_notrace():
#define local_cpu_ptr(ptr) \({ \ __verify_pcpu_ptr(ptr); \ SHIFT_PERCPU_PTR(ptr, __per_cpu_local_off); \})页表同步通过 arch_sync_kernel_mappings() 实现:当 CPU0 的 pgd 发生变化时,通过 ARCH_PAGE_TABLE_SYNC_MASK 检测修改级别,将变更同步到所有其他 CPU 的 pgd。本地 per-CPU 区域被排除在同步之外,因为各 CPU 映射到不同物理页。
测试环境:AmpereOne 160 核 ARM64 服务器,baseline 为 v7.1-rc1 内核。
make -j160 | ||
| 8.5% | ||
| 15% |
回归:创建 10K memcg 时多消耗 2112K 虚拟内存。已知限制包括 KPTI 兼容性(需额外处理 trampoline 页表切换)和共享 TLB 的 SMT 机器不适用。
系列:[PATCH v4 0/3] kasan: hw_tags: Disable tagging for stack and page-tables作者: Muhammad Usama Anjum, Dev Jain版本: v4(3 个 patch)
在 KASAN HW Tags(基于硬件内存标记扩展,MTE)模式下,内核栈和页表始终通过 match-all tag(KASAN_TAG_KERNEL)指针访问,硬件 MTE 完全不会对这些访问做 tag 检查。然而,当前分配栈和页表时,KASAN 仍会为每个 4K 页面执行 256 次 tag 设置指令(STG/STGP 或等效),并在释放时设置 invalid tag 进行 poisoning。这些 tag 操作完全不产生任何安全收益——因为 match-all tag 让硬件忽略了所有 tag 不匹配——却在高频场景(fork、多线程、缺页处理)中带来可观的指令开销。
该方案复用 __GFP_SKIP_KASAN 标志,将其语义从常规页面分配器扩展到三类关键分配路径。整体设计遵循"按实际安全检查需求标记"的原则:对始终通过 match-all tag 访问的对象(栈、页表)跳过 tag 设置,但保留 page.flags 中的 KASAN_TAG_KERNEL 标记——这意味着硬件和内核代码仍认为该页面有效(match-all 指针可正常访问),而非 match-all tag 的访问仍然会触发 MTE fault,安全检测覆盖率完全不变。
三类扩展路径分别是:
__vmalloc_node_range_noprof() 中检测 kasan_hw_tags_enabled() && (gfp_mask & __GFP_SKIP_KASAN),成立时跳过页保护位修改(不设置 KASAN_VMALLOC_PROT_NORMAL),且不调用 kasan_unpoison_vmalloc(),页面保持 poison tagTHREADINFO_GFP 和 GFP_VMAP_STACK 添加 __GFP_SKIP_KASAN,所有栈分配路径(fork 中的 alloc_thread_stack_node()、vmap 栈复用)自动跳过 tag 操作GFP_PGTABLE_KERNEL 添加 __GFP_SKIP_KASAN,覆盖所有通过页面分配器分配的页表页(PTE、PMD、PUD、PGD 级别)测试配置:2000 次迭代,每次 fork 子进程并创建 N 个线程。
| 13.4% | |||
| 7.6% |
所有场景下 KASAN 安全覆盖率完全不变,仅移除了对 match-all 指针访问对象进行 tag 设置的无效工作。SLUB 支持的页表分配暂未覆盖(使用不广泛,留待后续)。
系列:[PATCH v5 00/14] Remove CONFIG_READ_ONLY_THP_FOR_FS and enable file THP for writable files作者: Zi Yan版本: v5(14 个 patch)
CONFIG_READ_ONLY_THP_FOR_FS 是历史上为了解决文件透明大页(file THP)与写入冲突而引入的 Kconfig 选项。该机制要求文件必须以只读模式打开才能创建 file THP;一旦文件以写模式打开,内核强制调用 truncate_inode_partial_folio() 将已有 THP 分裂成 order-0 页面。随着 ext4、xfs、btrfs 等文件系统已经支持 large folio(大 folio),文件 THP 的创建条件应该由文件系统 runtime 能力(是否支持 PMD_ORDER 级别大 folio)决定,而非由一个粗粒度 Kconfig 开关控制。
CONFIG_READ_ONLY_THP_FOR_FS 造成不必要的 Kconfig 碎片化和代码路径分支MADV_COLLAPSE collapse 成 THPstruct address_space 中的 nr_thps 计数器仅用于 "fd 变可写时触发提前分裂" 逻辑,造成不必要的内存和代码占用核心改造是将文件 THP 的支持判断从 Kconfig 编译期决定转变为运行时文件系统能力查询。
安全基座:新增 mapping_pmd_folio_support() 辅助函数,检查文件系统的 mapping_max_folio_order() 是否达到 PMD_ORDER。在 collapse_file() 中以该运行时检查替代 CONFIG_READ_ONLY_THP_FOR_FS 编译期检查。同时引入 dirty-after-unmap 检查:在 try_to_unmap() 后对 to-be-collapsed folio 检查是否变脏——若变脏则跳过该 folio。这是实现可写文件 THP 的关键安全机制,因为之前依赖 inode->i_writecount 和 mapping->nr_thps 在 fd 变为可写前提前分裂的策略不再需要。
能力打通:从 file_thp_enabled() 和 hugepage_enabled() 中移除 CONFIG_READ_ONLY_THP_FOR_FS 检查,统一为基于 mapping_pmd_folio_support() 的判断。随后移除 Kconfig 选项本身。
废旧机制清理:移除 filemap_nr_thps_inc()/filemap_nr_thps_dec() 函数及所有调用点,从 struct address_space 中删除 nr_thps 字段。这些仅用于 "fd 变为可写时触发 drop all read-only THPs" 的逻辑。
可写文件 THP 收尾:从 file_thp_enabled() 中移除 inode_is_open_for_write() 检查。collapse_file() 中的 filemap_flush() 现在只对只读文件执行——对可写文件,要求用户态在 collapse 前显式刷脏,或使用 MADV_COLLAPSE(其 retry 路径自动处理 flush),避免 khugepaged 触发无条件系统级 writeback。
改造后的能力矩阵:
作者未提供性能数据。从代码逻辑推断的预期收益:
struct address_space 减少一个 atomic_t nr_thps 字段,大量 inode 场景下节省可观的 slab 内存系列:[PATCH 0/6] mm: Make per-VMA locks available in all builds作者: Dave Hansen版本: RFC v1(6 个 patch)
per-VMA lock(虚拟内存区域锁)自引入以来已稳定运行数年,通过 RCU + refcount 机制实现对单个 VMA 的细粒度读锁,有效缓解了 mmap_lock 在多线程工作负载下的竞争。但其可用性受限于 ARCH_SUPPORTS_PER_VMA_LOCK Kconfig,仅支持特定架构(x86、ARM64、PowerPC、RISC-V 等)且要求 SMP && MMU。这导致通用代码如果要使用 per-VMA lock,必须同时准备 mmap_lock 回退路径,大幅增加代码复杂度。作者在开发 x86 shadow stack(影子栈)代码时遇到递归锁定问题,想用 per-VMA lock 完全替代 mmap_lock,但发现它并非在所有配置下可用。
mmap_lock 回退路径,增加 #ifdef 和复杂分支ARCH_SUPPORTS_PER_VMA_LOCKmmap_lock 的情况下访问用户空间内存的代码路径,无法简单表达核心改造是移除所有架构的 select ARCH_SUPPORTS_PER_VMA_LOCK 并删除 CONFIG_PER_VMA_LOCK Kconfig,使 per-VMA lock 的底层原语(RCU、maple tree、refcount)在所有配置下无条件可用。vm_area_struct 和 mm_struct 中的 per-VMA lock 字段(vm_lock、vm_lock_seq、mm_lock_seq 等)始终存在。
同时引入全新 API lock_vma_under_rcu_wait()。这是一个重要的语义升级:当 per-VMA lock 快速路径(RCU 查找)遇到 writer 时,不再立即返回 NULL,而是通过 mmap_read_lock() 等待 writer 完成后重试(goto retry)。调用方不再需要回退路径——该 API 在缺页上下文中安全,因为它获取 mmap_lock 仅是为了等待 writer 完成,立即释放,从不持有锁,不产生递归锁定问题:
struct vm_area_struct *lock_vma_under_rcu_wait(struct mm_struct *mm,unsignedlong address){structvm_area_struct *vma;retry: vma = lock_vma_under_rcu(mm, address);if (vma)return vma; // 快速路径成功// 慢路径:等待 writer 完成后重试 mmap_read_lock(mm); vma = vma_lookup(mm, address); mmap_read_unlock(mm);if (!vma)returnNULL;goto retry;}应用示范:binder_alloc_free_page() 移除 mmap_read_trylock() 回退路径,shstk_pop_sigframe() 的复杂 VMA 检测循环被简化为单次 lock_vma_under_rcu_wait() 调用。
作者未提供性能数据(系列处于 RFC 早期阶段)。从代码逻辑推断的预期收益:
CONFIG_PER_VMA_LOCK#ifdef 分支,消除一类编译配置组合 | |
!MMU、!SMP),无需逐个架构维护 | |
lock_vma_under_rcu_wait() |
系列:[PATCH 00/12] kho: make boot time huge page allocation work nicely with KHO作者: Pratyush Yadav版本: v1(12 个 patch)
KHO(Kexec HandOver)是内核热升级(live update)机制的基础设施,允许当前内核将状态序列化传递给下一个内核。KHO 使用 scratch space(临时内存空间)为下一内核的早期启动提供内存;scratch 大小由当前内核的内存占用(RSRV_KERN 标记)乘以缩放系数决定。Gigantic huge page(通常 1 GiB 大页)通过 memblock_alloc_*() 在启动时分配,标记为 RSRV_KERN。在 hypervisor 场景中,将大量系统内存专用于 gigantic huge page(供 VM 使用)是常见做法。此外,即将到来的 hugetlb preservation 系列需要支持大页的 KHO 保留。
RSRV_KERN,导致 KHO scratch size 计算膨胀。当大页占用超过系统内存的 50% 时(hypervisor 典型配置),scratch 分配因超过可用内存而失败,KHO 完全不可用方案的核心思路是引入 extended scratch areas(扩展临时空间):在 KHO 启动时遍历 preserved memory radix tree(保留内存基数树)找出空闲内存区间,标记为扩展 scratch,供大页等用户内存分配使用。
整体分三个层次实现:
Radix Tree API 通用化:将 KHO radix tree API 从与 memory preservation 紧耦合中解耦为通用数据结构——kho_radix_add_page()/del_page() 重命名为 kho_radix_add_key()/del_key() 直接接收 key;引入 struct kho_in 存储 radix tree;walk 回调抽象为 struct kho_radix_walk_cb;支持 early-boot 使用和从物理地址初始化。
Extended Scratch 发现机制:新增 MEMBLOCK_KHO_SCRATCH_EXT 标志区别于原始 scratch。kho_extend_scratch() 以 1 GiB(KHO_EXT_SHIFT = 30)为粒度遍历 preserved memory radix tree,将未占用区间聚合为 1 GiB 块后标记为 extended scratch——聚合是为了避免大量细碎 key 导致 memblock.memory 性能退化。
大页分配与 scratch 解耦:引入 memblock_alloc_nid_user()。在 KHO scratch-only 阶段,仅从 extended scratch 满足用户内存分配,不使用核心 scratch。分配完成后调用 memblock_reserved_clear_kern() 清除 RSRV_KERN 标志,使大页不再计入 scratch 大小计算。
测试环境:x86_64 qemu,64G 内存,50 个 1G 大页。
最坏情况下 extended scratch 发现所需额外内存约为 6 个页面(KHO_TREE_MAX_DEPTH == 6),可覆盖 32 TiB 物理内存。方案使 KHO 在配备大量 gigantic huge page 的系统上可正常工作,同时为后续 hugetlb preservation 系列打下基础。
系列:[PATCH RFC 0/4] fs: Deferred inode reclaim作者: Jan Kara版本: RFC v1(4 个 patch)
Linux 内核在内存回收(reclaim)路径中释放 inode 时,某些文件系统的 inode 回收操作非常复杂——需要启动事务(transaction)、读取块位图(block bitmap)、进行 IO 操作。这导致文件系统在 kswapd 或 direct reclaim 上下文中必须发起 GFP_NOFAIL 分配。GFP_NOFAIL 在回收路径中高度危险,因为无法保证前向进展(forward progress),可能触发 MM 子系统的警告甚至死锁。ext4 的典型调用链为 prune_dcache_sb() → shrink_dentry_list() → iput() → sync_lazytime() → __filemap_get_folio_mpol()。此外,有脏时间戳(dirty timestamp,I_DIRTY_TIME)的 inode 不会被插入 LRU 链表,脏时间戳默认 12 小时才过期,导致这些 inode 长时间脱离 LRU 老化。
GFP_NOFAIL 分配,避免 MM 子系统警告和潜在死锁核心思路是引入基于 workqueue(工作队列)的异步回收机制:将复杂的 inode 回收从回收路径卸载到后台工作队列执行,同时建立 EMA 节流(throttling)机制保持系统反压。
首先将 iput() 中的 sync_lazytime()(同步脏时间戳回写)替换为新的 queue_dirtytime_writeback()。新函数不立即 IO,而是将 inode 移到 b_dirty_time 链表头部并篡改时间戳使其在下次周期性回写时被自然写出,从而消除回收路径中的 IO 触发。
随后引入 deferred reclaim 基础设施:新增 I_DEFER_RECLAIM 状态标志;在 inode_lru_isolate() 中将标记的 inode 分到 deferred 链表;prune_icache_sb() 将 deferred 链表通过 per-super_block 的 struct inode_deferred_reclaim(含 work_struct 和 spinlock)提交给 system_dfl_wq 异步回收。文件系统通过 mark_inode_reclaim_deferred() 标记 inode。
节流机制保证系统反压不会失控:当 deferred 链表长度超过 INODE_DEFERRED_RECLAIM_LIMIT(8192)时,throttle_inode_deferred_reclaim() 根据平均 inode 回收耗时(EMA,指数移动平均,权重 1/64)按比例阻塞创建 I_DEFER_RECLAIM inode 的任务,延迟最多为平均回收时间的 4 倍。
最后在 ext4 中接入:当 inode 在 ext4_mb_regular_allocator() 中有预分配块(需要加载块位图并运行事务才能释放)时,调用 mark_inode_reclaim_deferred()。
作者未提供性能数据。该系列仅经极轻量测试。预期收益:消除回收路径中 GFP_NOFAIL 分配,避免 MM 子系统警告和死锁;ext4 等文件系统无需在回收路径执行事务和 IO。
系列:[PATCH v2] mm: process_mrelease: introduce PROCESS_MRELEASE_REAP_KILL flag作者: Minchan Kim版本: v2(1 个 patch)
process_mrelease() 是 Android LMKD(Low Memory Killer Daemon)使用的关键系统调用,用于快速回收被杀进程的内存。当前设计需要用户空间先发 SIGKILL,再调用 process_mrelease()。两步之间存在调度竞争窗口:目标进程收到 SIGKILL 后可能立即进入退出路径(do_exit → exit_mm),一旦清除 task->mm,后续的 process_mrelease() 将因找不到 mm 而返回 -ESRCH。而此时实际的 exit_mmap 被推迟到 mm 引用计数归零——"读取 /proc/<pid>/cmdline 或其他远程 VM 访问经常无限期推迟此过程"(作者原文)。
process_mrelease() 返回 -ESRCH,内存回收延迟引入 PROCESS_MRELEASE_REAP_KILL UAPI 标志((1 << 0)),将 SIGKILL 发送和内存回收集成为一个原子操作。在发送 SIGKILL 之前通过 mmgrab() 持有 mm 引用(防止目标进程立即清除 mm),回收逻辑简化为 reap = reap_kill || task_will_free_mem(p)。对于 MMF_MULTIPROCESS 共享 mm 的进程直接返回 -EINVAL,防止不安全地回收共享内存。
与 OOM killer(内存不足杀手)的关键差异在于:process_mrelease() 由用户空间策略驱动,不强制在所有代价下回收成功,如果 kill_pid() 失败则回退跳过回收。
作者未提供 benchmark 数据。功能收益:消除 kill+reap 竞争窗口,一次系统调用完成回收,避免因延迟回收导致不必要的额外杀进程。Suren Baghdasaryan 的 Reviewed-by 表明该设计获得了 Android 内存管理团队的认可。
系列:[PATCH 0/7] mm/damon/reclaim,lru_sort: monitor all system rams by default作者: SeongJae Park版本: v1(7 个 patch)
DAMON_RECLAIM 和 DAMON_LRU_SORT 默认将系统中最大的 System RAM 资源设置为监控目标。初衷是最小化监控非 System RAM 区域的开销。"the current design trades ease of setup for lower overhead"(当前设计以使用便利性换取更低开销)——但当系统有多个离散的大容量 System RAM 时(如 NUMA 系统各 500 GiB),默认只监控最大的一段,其余被忽略。用户必须在不同类型系统上手动设置 monitor_region_start/monitor_region_end。
新增核心函数 damon_set_region_system_rams_default(),遍历 walk_system_ram_res() 获取所有 System RAM 资源的起止范围(第一个 start 到最后一个 end),覆盖全部物理内存段。DAMON_RECLAIM、DAMON_LRU_SORT 和 DAMON_STAT 三个调用方统一切换至新函数,删除旧的 damon_set_region_biggest_system_ram_default() 及其辅助函数。总体改动:+50 行,-88 行,净减少 38 行。
作者未提供 benchmark 数据。预期收益:用户无需手动配置即可覆盖全部 System RAM,消除 NUMA 系统上的监控盲区;代码库净减少 38 行。作者判断监控额外区域的性能开销 "should be negligible in most setups"(在大多数配置中应可忽略不计)。
系列:[PATCH] mm/memcontrol: Avoid stuck FLUSHING_CACHED_CHARGE on isolated CPU作者: Hui Zhu版本: v1(1 个 patch)
memcg(内存控制组)的 per-CPU stock 机制缓存小额 charge/uncharge,减少全局锁竞争。drain_all_stock() 遍历所有 CPU,在排程 drain work 前设置 FLUSHING_CACHED_CHARGE 标志防止重复排程。目标 CPU 通过 queue_work_on() 异步执行 drain 并清除该标志。
当目标 CPU 处于 isolated(隔离)状态(cpu_is_isolated() == true,由 cpuset CPU partition 设置)时,schedule_drain_work() 中 queue_work_on() 被静默跳过——但 FLUSHING_CACHED_CHARGE 已被置位且永远不会被清除。此后每个 drain_all_stock() 调用检查到该标志位后跳过此 stock,导致 per-CPU stock 被永久钉住("effectively pinned until something else on that CPU runs drain_local_*_stock() and clears the bit -- which on a long-isolated CPU may never happen")。
最小化修复:修改 schedule_drain_work() 增加 unsigned long *flags 参数。当 cpu_is_isolated(cpu) 为 true 时,调用 clear_bit(FLUSHING_CACHED_CHARGE, flags) 清除标志位(在 RCU 保护区外执行)。缓存 charge 保留在原地,将在 CPU 离开隔离状态后由其自身释放,或当隔离 CPU 自身调用 drain_all_stock() 时(cpu == curcpu 分支)完成 drain。涉及 mm/memcontrol.c,+22 行,-6 行。
Fixes:2d05068610a3 ("memcg: Prepare to protect against concurrent isolated cpuset change")
修复了标志位卡死导致后续 drain 全部永久跳过的逻辑缺陷。修复后下次 drain_all_stock() 可正常重试该 stock。
系列:[PATCH] mm/vmscan: fix delayed flusher wakeup in MGLRU作者: Vineet Agarwal版本: v1(1 个 patch)
MGLRU(Multi-Gen LRU)在页面回收过程中,遇到大量脏文件页(dirty file folios)无法直接回收时,需唤醒后台 flusher 线程将脏页写回磁盘。Classic LRU 回收路径在每次 shrink_folio_list() 返回后立即用 per-batch 的局部统计值判断是否唤醒 flusher。而 MGLRU 将此判断推迟到 try_to_shrink_lruvec() 中,使用跨多轮 evict_folios() 累积的全局计数器。
当较早回收批次只隔离了脏文件页(dirty file folios),而较晚批次隔离了干净文件页(clean file folios)时,累积计数器失去同步。例如 batch 1 中 file_taken=100, unqueued_dirty=100;batch 2 中 file_taken+=60, unqueued_dirty+=0——最终比较变为 100 != 160,flusher 唤醒被错误跳过。实际效应:回收因脏页写回不及时而阻塞,可能触发不必要的 OOM,尤其在 cgroup v1 场景下。
将 flusher 唤醒逻辑从 try_to_shrink_lruvec() 移到 evict_folios() 内部,使用 per-batch 的局部变量 file_taken。scan_folios() 将隔离的文件页数量写入调用者传入的 unsigned long *file_taken 指针;isolate_folios() 透传该指针;evict_folios() 在调用 shrink_folio_list() 后立即用当前批次的局部 file_taken 比对 stat.nr_unqueued_dirty,条件满足即唤醒 flusher 并执行写回节流。最后将 file_taken 追加到累积值中。
Fixes:1bc542c6a0d14 ("mm/vmscan: wake up flushers conditionally to avoid cgroup OOM")
使 MGLRU 的脏页回收行为与 classic LRU 完全一致,消除了 flusher 唤醒被错误跳过的窗口。对于高脏页率的文件型工作负载(如数据库、文件服务器),可避免因写回不及时导致的回收停顿和潜在 OOM 触发。
系列:[PATCH] mm/khugepaged: return -EAGAIN for SCAN_PAGE_HAS_PRIVATE in MADV_COLLAPSE作者: Vineet Agarwal版本: v1(1 个 patch)
MADV_COLLAPSE 是用户空间主动触发透明大页(THP)合并的 madvise 操作,其返回值遵循明确语义约定:临时性资源约束映射为 -EAGAIN(提示可重试),目标范围永久不可折叠的特性映射为 -EINVAL。collapse_file() 在折叠文件映射 THP 时调用 filemap_release_folio() 释放 folio 的私有数据(private data),失败时返回 SCAN_PAGE_HAS_PRIVATE,当前在 madvise_collapse_errno() 中走 default 分支映射为 -EINVAL。
filemap_release_folio() 失败通常反映的是临时性的 folio 状态——ext4 上仍有脏日志数据、btrfs 上 extent state 尚未释放、NFS 上回收文件系统私有状态失败——而非永久不可折叠的范围。返回 -EINVAL 误导用户空间以为该地址范围根本上不可折叠,而实际上写回/日志提交完成后重试即可成功。这与 SCAN_PAGE_DIRTY_OR_WRITEBACK 等临时性失败返回 -EAGAIN 的处理方式不一致。
在 madvise_collapse_errno() 的 switch 语句中,将 SCAN_PAGE_HAS_PRIVATE 从 default 分支(映射为 -EINVAL)移至已有 -EAGAIN 返回组,与 SCAN_PAGE_LRU、SCAN_PAGE_DIRTY_OR_WRITEBACK 同等对待。一行改动。
纠正了 MADV_COLLAPSE 的错误返回码,使用户空间程序在 filemap_release_folio() 因临时状态失败时正确重试。对 ext4/btrfs/NFS 上使用 MADV_COLLAPSE 的应用(如虚拟机、数据库)有直接收益——原本因 -EINVAL 放弃折叠的地址范围可在写回/日志提交后通过重试成功折叠,提高 THP 合并成功率。
系列:[PATCH] mm/madvise: preserve uprobe breakpoints across MADV_DONTNEED作者: Darko Tominac版本: v1(1 个 patch)
uprobes(用户空间探针)通过在文件映射的目标地址处插入软件断点指令(swbp)实现函数级别 instrumentation,广泛用于 eBPF 安全监控和性能追踪。uprobe_mmap() 仅在 VMA(虚拟内存区域)创建时被调用以安装断点,不会在单个页面 fault-in 时重新插桩。MADV_DONTNEED 允许用户空间提示内核丢弃指定文件映射范围内的页面内容——当这些页面再次被访问时从后备文件重新读取,但断点已永久丢失。
当文件映射页面包含 uprobe 断点指令时,MADV_DONTNEED 会丢弃这些页面,断点永久丢失,探针静默停止触发且无任何错误提示。复现路径:用户空间内存回收子系统对文件映射代码段调用 madvise(MADV_DONTNEED) → 包含 uprobe 断点的页面被丢弃 → eBPF 安全/追踪工具的 uprobe 在首次回收操作后数秒内停止工作。唯一的恢复方式是注销并重新注册受影响的 uprobes。
方案采用 "跳跃式 zapping" 策略,保护含 uprobe 断点的页面不被丢弃。
新增三个 uprobes 子系统 API:any_uprobes_registered() 快速全局检查是否有任何 uprobe 注册(!no_uprobe_events());vma_has_uprobes() 检查指定 VMA 范围内是否存在 uprobe(利用全局 rbtree 的 find_node_in_range());vma_first_uprobe_addr() 返回 VMA 区间内第一个 uprobe 的页对齐地址(rbtree 查找后向左遍历取最小 offset)。所有三个函数在 CONFIG_UPROBES=n 时有空的内联实现,零开销。
在 madvise_dontneed_free() 中的跳过逻辑:快速路径——若无 uprobe 注册(likely(!any_uprobes_registered()))、或 VMA 非文件映射、或范围内无 uprobe——直接执行完整的 MADV_DONTNEED。慢速路径——用 vma_first_uprobe_addr() 从 rbtree 跳至每个 uprobe 页地址,zap 各 uprobe 页之前的干净区间然后跳过该页,循环覆盖整个 range,复杂度 O(M * log N)(M 为区间内 uprobe 数,N 为系统总 uprobe 数)。
作者未提供性能数据。该修复解决了 MADV_DONTNEED 与 uprobes 之间的静默数据损坏问题——断点不再被意外清除。快速路径零额外开销,慢速路径利用 rbtree 跳跃避免逐页线性扫描。对使用 eBPF uprobe 进行安全监控、性能剖析或函数追踪的生产系统具有重要正确性保障。
系列:[PATCH v2] x86/mm: fix freeing of PMD-sized vmemmap pages作者: David Hildenbrand版本: v2(1 个 patch)
commit bf9e4e30f353("x86/mm: use pagetable_free()")将 x86 非启动页表的释放路径统一迁移到 pagetable_free()。但 vmemmap 页面(通过 vmemmap_alloc_block() 分配的普通单页)被错误地纳入该路径。vmemmap 页不是 compound page(复合页)(HugeTLB Vmemmap Optimization 使用 altmap 的例外场景除外)。
pagetable_free() 内部调用 __free_pages(page, compound_order(page))。由于 vmemmap 页非 compound page,compound_order() 返回 0,因此在释放 PMD 大小(2 MB)的 vmemmap 页时,只释放了第一页(4 KB),其余 511 页(~2 MB)被泄漏free_pagetable() 中处理 SECTION_INFO 类型——但只有 vmemmap 页面被标记为 SECTION_INFO,页表页面不会有此标记,导致语义混淆page_ptdesc() 在语义上错误Fixes:bf9e4e30f353 ("x86/mm: use pagetable_free()"),Cc: stable
复现方式:使用一个带 virtio-mem 设备的简单 VM,反复添加和移除内存。
将 vmemmap 和页表的释放路径彻底解耦,引入专用函数 free_vmemmap_pages(page, order, altmap)。该函数接收显式的 order 参数而非依赖 compound_order(),正确处理三种场景:altmap 页面调用 vmem_altmap_free();bootmem 保留页面(SECTION_INFO 类型)通过 put_page_bootmem() 逐页释放;普通页面直接 __free_pages(page, order)。同时从 free_pagetable() 中移除 SECTION_INFO 处理,使页表释放语义更精确。
消除了每次释放 PMD 大小 vmemmap 页面时泄漏约 2 MB 的内存泄漏问题。作者通过代码审查发现("Found by code inspection while working on bootmem_info removal"),影响所有使用 PMD 大小 vmemmap 的 x86 系统。
系列:[PATCH v3] mm: memcontrol: fix rcu unbalance in get_non_dying_memcg_end()作者: Qi Zheng版本: v3(1 个 patch)
在 cgroup v2 混合模式环境下,memory controller 可在运行时从 cgroup v1 层次结构切换到 v2(hierarchy rebinding)。get_non_dying_memcg_start() 和 get_non_dying_memcg_end() 是一对函数,用于 mod_memcg_state() 和 mod_memcg_lruvec_state() 中以安全方式获取非 dying 状态的 memcg。两者各自独立评估 cgroup_subsys_on_dfl():v2 下不需要 RCU 保护,v1 下需要 rcu_read_lock() 来安全地遍历 memcg 父链(跳过 dying memcg)。
cgroup_subsys_on_dfl() 在运行时动态变化。如果 _start() 调用时 memory controller 在 v2(不获取 RCU 锁),随后发生 rebinding 切换到 v1,_end() 调用时看到 v1 却执行 rcu_read_unlock(),导致锁不平衡,内核产生 WARNING。复现路径为标准缺页处理 do_user_addr_fault → handle_mm_fault → mod_memcg_state / mod_memcg_lruvec_state。
Fixes:8285917d6f38 ("mm: memcontrol: prepare for reparenting non-hierarchical stats")
将 "RCU 是否已持有" 的状态从隐式(依赖两次独立调用 cgroup_subsys_on_dfl())改为显式追踪。get_non_dying_memcg_start() 新增 bool *rcu_locked 参数,持有 RCU 锁时设为 true,否则 false。get_non_dying_memcg_end() 改为接收 bool rcu_locked 参数,不再重复调用 cgroup_subsys_on_dfl()。调用点(mod_memcg_state()、mod_memcg_lruvec_state())通过栈上局部布尔变量传递锁状态,保证 RCU 锁的获取和释放严格配对。
消除由 cgroup hierarchy rebinding 触发的 RCU 锁不平衡 WARNING,避免污染内核锁定调试基础设施。rcu_locked 是栈上局部变量,额外开销可忽略不计。已获 Shakeel Butt Acked-by 和 Muchun Song Reviewed-by。
系列:[PATCH hotfix] mm: fix pmd_special() fallback to observe huge_zero作者: Hugh Dickins版本: v1(1 个 patch)
在 x86 32 位且启用 THP(透明大页)的配置下,zap_huge_pmd() 用于清除 PMD 级别的大页映射。该函数调用 vm_normal_folio_pmd() → __vm_normal_page() 时使用 pmd_special() 判断 PMD 映射是否为 special 映射。零页和大零页(huge_zero page)被视为 special 映射——但 PMD 级别的 special 位由 CONFIG_ARCH_SUPPORTS_PMD_PFNMAP 保护,该配置项在任何 32 位架构上均未启用。因此 pmd_special() 的回退实现简单返回 false。
__vm_normal_page() 中的 VM_WARN_ON_ONCE(is_huge_zero_pfn(pfn)) 断言,触发 WARNING__vm_normal_page() 为 huge_zero PMD 返回非 NULL 的 struct page 指针,zap_huge_pmd() 调用者将其误认为普通 folio 而调整 RSS 计数器,触发 "Bad rss-counter state" BUGshrink_huge_zero_folio_scan() 时遇到已被错误处理的 folio,触发 "Bad page state" BUGFixes:d80a9cb1a64a ("mm/huge_memory: add and use normal_or_softleaf_folio_pmd()")
复现路径:在 x86 32 位 + THP 配置下,分配 huge_zero PMD → 触发 zap_huge_pmd() → pmd_special() 返回 false → __vm_normal_page() WARNING → RSS 误差累积 → reclaim 路径 Bad page state BUG。
单行改动,位于 include/linux/mm.h:pmd_special() 的回退实现从 return false 改为 return is_huge_zero_pmd(pmd)。is_huge_zero_pmd() 仅检查 PMD 的 PFN 是否等于 huge_zero_pfn,是轻量级操作。不引入新 Kconfig(如 CONFIG_ARCH_HAS_PMD_SPECIAL),采取最务实的方案。
修复了 x86 32 位 + THP 配置下的三个级联问题(WARNING + Bad rss-counter BUG + Bad page state BUG),恢复 zap_huge_pmd() 路径的正确性。额外开销为零——is_huge_zero_pmd() 仅在 CONFIG_ARCH_SUPPORTS_PMD_PFNMAP 未启用时被调用,不影响支持 PMD PFNMAP 的架构。
| ARM64 per-CPU 页表优化 | ||
| KASAN HW Tags 跳过栈和页表 | ||
| 移除 READ_ONLY_THP_FOR_FS | struct address_space 减少 nr_thps 字段;配置简化 | |
| per-VMA 锁全配置可用 | CONFIG_PER_VMA_LOCK;新 API 无需回退路径 | |
| KHO 启动大页与 KHO 协作 |
| Deferred Inode Reclaim | I_DEFER_RECLAIM + EMA 节流 + tracepoint | |
| PROCESS_MRELEASE_REAP_KILL | ||
| DAMON 默认监控全部 System RAM |
| FLUSHING_CACHED_CHARGE 卡死 | 2d05068610a3 | |
| MGLRU flusher 延迟唤醒 | 1bc542c6a0d14 | |
| MADV_COLLAPSE -EAGAIN | -EINVAL 改为 -EAGAIN,使临时失败可重试 | |
| MADV_DONTNEED 保留 uprobe | ||
| vmemmap PMD 释放泄漏 | bf9e4e30f353,Cc: stable | |
| RCU 锁不平衡 | rcu_read_unlock() 无配对。Fixes: 8285917d6f38 | |
| pmd_special huge_zero | d80a9cb1a64a |