系列:[PATCH v5 0/5] KSM: performance optimizations for rmap_walk_ksm作者: xu xin, Wang Yaxin版本: v5(5个patch)
当系统内存极度紧张导致 KSM 页面被换出(swap out),或存在严重内存碎片化导致 THP 触发内存压缩(compaction)时,内核会调用 rmap_walk_ksm() 执行反向映射,定位所有引用该 KSM 页面的页表项。该函数需要遍历所有共享同一 anon_vma 的 VMA。大量 VMA 附着到同一 anon_vma 并非仅由 fork() 引起——mprotect/madvise 等操作导致的 VMA 分裂也能在无 fork 场景下产生数万个 VMA 共享同一个 anon_vma。作者的调试追踪分析发现,当 VMA 数量达到约 20,000 时,anon_vma_interval_tree_foreach 消耗了绝大部分延迟,导致 anon_vma 锁持有时间超过 500ms,进而阻塞上层应用线程。anon_vma 锁被缺页中断、回收、迁移、压缩、mlock、进程退出、cgroup 计费等众多关键路径所争用,长时间持锁会造成严重的延迟抖动和容器超时。
anon_vma_interval_tree_foreach 内 99.9% 的迭代因"addr 不在 VMA 范围内"而被跳过——因为传入的搜索范围是整个地址空间 [0, ULONG_MAX]anon_vma 锁被缺页中断、回收、迁移/压缩等路径共享,长时间的 KSM rmap walk 会阻塞所有这些操作该系列采取"诊断→基准→数据准备→核心优化→测试"的渐进路线。核心洞察在于:KSM folio 始终是 order-0 普通页,在范围内区间树中只有唯一的 VMA 包含该页。实现上分三步走:
数据准备:在 struct ksm_rmap_item 中新增 pgoff 字段,记录 KSM 页面在其 anon_vma 中的线性页面偏移(linear page index)。关键设计——使用 union 将 pgoff 与仅在非稳定树中使用的 oldchecksum、age、remaining_skips 放在同一联合体中,使结构体大小保持在 64 字节不变。pgoff 在 try_to_merge_with_ksm_page() 中通过 linear_page_index(vma, rmap_item->address) 设置,在树节点移除和 COW 断开时清零。
核心优化:在 mm/ksm.c 的 rmap_walk_ksm() 中,将 anon_vma_interval_tree_foreach 的搜索范围从 0, ULONG_MAX 缩小为 pgoff, pgoff。由于区间树的搜索条件是 vm_pgoff <= pgoff <= vm_pgoff + vma_pages(v) - 1,给定精确的 pgoff 后,树遍历可以直接定位到包含该偏移的唯一 VMA,将迭代次数从 >22,000 降至 ~3。
诊断基础设施:在 mm/rmap.c 中新增 trace_rmap_walk_start/trace_rmap_walk_end 追踪点,使用静态分支(static branch)实现零开销禁用;新增 tools/testing/rmap/rmap_benchmark.c 基准测试工具。
在 QEMU 环境中使用 rmap_benchmark 测试(20,000 个 VMA 共享同一 anon_vma):
| 降低 422 倍 | |||
| 降低 369 倍 | |||
anon_vma | |||
系列:[PATCH] mm/page_alloc: skip high atomic reservation at or below costly order作者: JP Kobryn版本: v1(1个patch)
在生产环境中观察到一种模式:2MB THP(order-9)分配因碎片化失败并触发回收,尽管系统仍有大量空闲内存。通过 kprobe 探测发现,zone 有足够的空闲页供 order-9 分配,但这些页面并未被使用。进一步检查 /proc/pagetypeinfo 揭示了根因:order-9 块全部堆积在 zone 的 HighAtomic 桶中,而 Movable 桶中为零。THP 无法从 HighAtomic 桶分配页面,因为该桶不在 fallback 列表中。HighAtomic 预留的启发式规则是:任何大于 order-0 的原子分配都会导致整个页块(pageblock)被捕获。这意味着一次 order-1 的原子分配会过度预留 256 倍——整整一个 512 页(2MB)的页块。
在 mm/page_alloc.c 的 reserve_highatomic_pageblock() 函数开头增加一个 order 检查:若 order <= PAGE_ALLOC_COSTLY_ORDER(即 order 0-3),直接 return,跳过整页块预留。PAGE_ALLOC_COSTLY_ORDER 定义为 3,这是区分"低成本"和"高成本"分配的经典分界线。该检查放在函数最前,在计算 max_managed(1% zone 限制)等开销之前就快速返回。逻辑清晰直接:小阶原子分配不应捕获一个 2MB 级别的整页块,观察也确认所有原子分配均落在 order 0-3 范围内,因此限制不会拒绝任何真正需要大块的内核分配。
在约 60 台 Instagram 生产主机(64GB 内存)上的 A/B 测试:
| 降低 300 倍 | |||
| 提升约 15 倍 | |||
| 提升约 20 倍 | |||
| 降低 57% | |||
系列:[PATCH] mm/compaction: cap compact_gap() at COMPACT_CLUSTER_MAX作者: JP Kobryn版本: v1(1个patch)
compact_gap() 函数返回 2 << order,该值在 __compaction_suitable() 中作为空闲页水位的"余量"(headroom),同时在 kswapd 中作为回收目标。该值随 order 指数级增长:对于 order-9 的透明大页(THP)分配,计算结果为 1024 个页(4MB)。然而,compaction 自由页面扫描器的实际工作集受限于 COMPACT_CLUSTER_MAX(32 页)——扫描器在隔离的自由页面数量匹配迁移批次大小后即停止。当前 gap 值过度预留了 32 倍。代码中的注释曾认为"更复杂化的公式不值得,更大的 gap 对高阶分配有益",但生产数据证明相反。
compact_gap() 返回 1024 页,但 compaction 自由扫描器最多只需 32 页,多预留了 32 倍__compaction_suitable())不必要地失败——zone 实际有足够自由页供扫描器运行,但不足以通过被放大的水位线该 patch 在 include/linux/compaction.h 中的 compact_gap() 函数末尾添加 min() 限制,将返回值上限设为 COMPACT_CLUSTER_MAX(32 页)。改动极简:将 return 2UL << order; 改为 return min(2UL << order, COMPACT_CLUSTER_MAX);,并添加 #include <linux/swap.h> 头文件。order 0-4 不受影响,因为它们的 gap 值原本就小于等于 32。关键洞察在于:compaction 自由扫描器的隔离上限本就是 COMPACT_CLUSTER_MAX,水位线余量超过此值毫无意义,反而导致 __compaction_suitable() 错误地判定 zone 不适合 compaction,进而阻止 THP 分配并触发不必要的 kswapd 回收。
在约 100 台 Instagram 生产主机(64GB 内存,60 秒采样)上的 A/B 测试:
| 降低 3.6 倍 | |||
| 提升 13 倍 | |||
| 提升 3.5 倍 | |||
系列:[PATCH v4 0/4] Introduce Per-CPU Work helpers (was QPW)作者: Leonardo Bras, Marcelo Tosatti版本: v4(4个patch)
内核中有相当多的子系统采用 "local_lock() 保护本地操作 + queue_work_on() 调度远程操作" 的并行编程模式,典型场景包括 LRU 缓释(lru_add_drain_all)、mlock 批处理、SLUB 每CPU缓存的冲刷。在非 RT 内核中,这种方式通过本地锁避免了锁竞争开销,少量的远程操作虽昂贵但可接受。然而在 PREEMPT_RT 内核中,local_lock() 会蜕变为 per-CPU 自旋锁(spinlock),此时仍然通过 schedule_work_on() 向远程 CPU 调度工作会产生不必要的上下文切换,对于运行低延迟实时任务的 CPU,"getting an important workload scheduled out to deal with remote requests is sure to introduce unexpected deadline misses"。
local_lock() 已经变成了 spinlock(支付了加锁成本),仍然要承担 schedule_work_on() 的调度开销该系列引入了一个新的 Per-CPU Work(PW)抽象层,核心文件为 include/linux/pwlocks.h 和 kernel/pwlocks.c。其设计精髓在于利用内核配置和启动参数的组合来控制行为:
CONFIG_PWLOCKS=n**(默认):所有 PW 接口直接退化为当前的实现,pw_lock() 等同于 local_lock(),pw_queue_on() 等同于 queue_work_on(),pw_flush() 等同于 flush_work(),零性能影响。CONFIG_PWLOCKS=y + pwlocks=1**:pw_lock(lock, cpu) 使用远程 CPU 的 per-CPU spinlock,pw_queue_on() 变为在当前 CPU 上直接调用工作函数(操作远程 per-CPU 数据结构),pw_flush() 变为空操作。关键数据结构包括 pw_lock_t(联合体,封装 spinlock_t 和 local_lock_t)、pw_struct(包含 work_struct 和目标 CPU 信息)。该系列将 LRU 缓释(mm/swap.c)、mlock 批处理(mm/mlock.c)和 SLUB 每CPU sheaf 冲刷(mm/slub.c)转换为 PW 接口。
作者提供了 kmalloc 基准测试数据(9,000,000 次迭代,64 字节分配):
| PREEMPT_DYNAMIC=y | ||
| PREEMPT_RT |
PREEMPT_DYNAMIC 下 pwlocks=1 从 60 增加到 75 cycles(+25%),因为 spinlock 的原子操作开销高于 preempt_disable。但 PREEMPT_RT 下 pwlocks=1 与未打补丁的 95 cycles 基本持平(97 cycles)。关键在于:pwlocks=1 消除了对远程 CPU 上低延迟实时任务的调度干扰——"This will avoid schedule_work_on(), and thus avoid scheduling-out an RT workload"。
系列:[RFC PATCH 0/2] mm: swap: allow per-device skipping of zero-filled page check作者: Youngjun Park版本: v1(2个patch)
当前 swap 层在将页面写出到交换设备之前,会检查页面是否全为零。若页面全零,则通过 swap_zeromap_folio_set() 在集群 zeromap 中标记,从而避免实际的 I/O 操作。然而,某些交换后端(如 zram 和自定义交换设备)在内部已经执行了自己的相同填充页面检测(same-filled page checking)。这导致内核 swap 层和交换后端重复进行相同的零页检测,浪费 CPU 周期。此外,在不支持 SWAP_TABLE_HAS_ZEROFLAG 的架构上,swap 层还会为每个集群分配独立的 zero_bitmap,若设备不需要零页检测,则这些 bitmap 内存分配也是浪费。
SWAP_TABLE_HAS_ZEROFLAG 的架构上,为不需要零页检测的设备分配了不必要的 zero_bitmap该方案通过两个 patch 实现按设备粒度的零页检测跳过。核心机制是新增 SWAP_FLAG_SKIP_ZERO_CHECK(值 0x80000)作为 swapon() 系统调用的新标志,内部对应 swap_info_struct->flags 的 SWP_SKIP_ZERO_CHECK 位。在 swap_writeout() 写路径中,若设备设置了该标志,直接跳过 swap_zeromap_folio_set() 调用。同时在读路径 swap_read_folio_zeromap() 中也做相应守卫。在 swap_cluster_alloc_table() 中,若设备跳过零检查且架构不支持 SWAP_TABLE_HAS_ZEROFLAG,则跳过 zero_bitmap 的 bitmap 内存分配。
作者未提供性能数据。从代码逻辑推断的预期收益:
SWAP_TABLE_HAS_ZEROFLAG 的架构上,每个 swap 集群省去 zero_bitmap 内存分配和初始化开销系列:[PATCH v14 0/5] mm/vmalloc: free unused pages on vrealloc() shrink作者: Shivam Kalra版本: v14(5个patch)
内核 vrealloc() 函数可以对已有的 vmalloc 分配进行原地调整大小。当分配扩大时,如果虚拟地址空间中有足够的连续空间,会原地增长;但当分配缩小时,虽然会更新记账信息(requested_size、KASAN shadow),却从不释放底层物理页。这个机制缺陷导致收缩后的分配在整个生命周期内持续浪费物理内存。代码中一直保留着一条 TODO 注释:"Shrink the vm_area, i.e. unmap and free unused pages"。该系列填补了这一功能空白。
get_vm_area_size() 与实际的已映射物理页数量不一致,导致 vread_iter()(/proc/vmallocinfo 读取路径)可能访问未映射区域核心思路是利用已有的虚拟地址保留机制(不缩小 vm->size 和 vmap_area),仅在收缩时释放多余的物理页。先从 vfree() 中提取出 vm_area_free_pages() 辅助函数,使用 free_pages_bulk() 批量释放页面并完成 memcg 统计更新。将 grow-in-place 检查改为基于物理页计数(vm->nr_pages),让 vread_iter() 对 VM_ALLOC 类型使用 vm->nr_pages 推导区域大小。核心收缩逻辑:当 new_nr_pages < vm->nr_pages 且跨页边界时,在持有 vn->busy.lock 的情况下更新 vm->nr_pages,然后调用 vunmap_range() 解除尾页映射,最后调用 vm_area_free_pages() 释放物理页。该路径跳过大页分配、VM_FLUSH_RESET_PERMS、VM_USERMAP 及 GFP_NOFS/GFP_NOIO 场景。
作者未提供性能数据。从代码逻辑推断的预期收益:
KVVec::shrink_to 接口可从中受益系列:[PATCH v14 0/8] arm64: add ARCH_HAS_COPY_MC support作者: Ruidong Tian版本: v14(8个patch)
随着内存容量和密度的增加,内存错误的概率也在上升。x86 和 powerpc 早已支持 ARCH_HAS_COPY_MC(Machine Check safe copy),在 COW(写时复制)、KSM 复制、coredump、khugepaged 等场景中,当从源页拷贝时遇到内存错误,可以优雅地隔离错误页并让调用者返回错误,而不是触发内核 panic。arm64 此前缺乏这一能力——"In arm64, memory error handling in do_sea()... if the kernel state consumed the memory errors, the solution is to panic"。对于装备大内存的 ARM 服务器(如 Grace、Vera),这一问题愈加严重。
copy_mc_to_user()、copy_mc_to_kernel()、copy_mc_highpage() 等 MC-safe 复制接口的架构支持MF_ACTION_REQUIRED 并发送 SIGBUS,但内核代用户空间执行 uaccess 时消费的内存错误应让系统调用返回 -EFAULT 而非杀进程该系列建立了一个完整的 arm64 硬件内存错误恢复框架,分三个层次:
异常表扩展:在 arm64 的 extable 机制中新增 EX_TYPE_KACCESS_ERR_ZERO_MEM_ERR fixup 类型,并定义 KERNEL_MEM_ERR 汇编宏,用于标记可从内核内存访问错误中恢复的指令。__arch_copy_to_user() 中所有的加载指令都使用该宏包装。
SEA 处理框架改造:引入 enum context { IN_KERNEL, IN_USER },在 SEA 入口捕获 user_mode(regs) 信息。修改 SIGBUS 触发条件——只有当错误在用户态消费时才发送 SIGBUS 杀进程;内核态消费时走 memory_failure_queue(flags=0) 异步隔离,让 fixup 返回 -EFAULT。
MC-safe 页面拷贝实现:通过将 copy_page() 的汇编逻辑提取为共享模板 copy_page_template.S,重构了 copy_page() 并新增 copy_mc_page()。同步新增 mte_copy_mc_page_tags() 支持 MTE 标签的 MC-safe 拷贝。将 copy_mc_[user_]highpage() 的返回值语义统一为:成功返回 0,失败返回 -EFAULT。
| 全部消除 | |
系列:[PATCH 00/28] mm/damon: introduce data attributes monitoring作者: SeongJae Park版本: v1(28个patch)
DAMON(Data Access MONitor)最初设计为数据访问监控器,后续扩展了 DAMOS(数据访问感知的系统操作)。但监控部分始终仅限于数据访问模式。用户需要更全面的视图——例如,运维巨页效率的工程师想知道 DAMON 发现的热/冷区域中有多少是大页支持的,运行多 cgroup 工作负载的用户想知道热/冷区域中各 cgroup 的占比。6.14 内核已合入了基于 DAMOS 的页级别属性监控,但它以页为粒度遍历整个 DAMON 区域,"the overhead is proportional to the memory size","obviously too heavy to be enabled always on all machines running the real user workloads"。
核心设计是在 DAMON 访问采样点复用监控能力,以极低额外开销引入"数据探针"(data probe)机制。每个探针(struct damon_probe)携带一组过滤器(struct damon_filter,与 DAMOS filter 同构),在 DAMON 检查每个采样地址是否被访问的同时,遍历已注册的数据探针并判断该地址是否满足探针的过滤条件。匹配结果累加到 damon_region->probe_hits[],与 nr_accesses 在同一聚合间隔中维护。在物理地址监控层(paddr ops),damon_pa_apply_probes() 根据探针的过滤器检查采样地址对应 folio 的属性。用户接口通过 DAMON sysfs 暴露探针配置路径和结果读取路径。新增 damon_region_aggregated tracepoint 用于通过 tracefs 获取数据。
作者未提供性能数据。从代码逻辑推断的预期收益:
系列:[PATCH v3 0/6] Open HugeTLB allocation routine for more generic use作者: Ackerley Tng版本: v3(6个patch)
当前 Linux 内核的 HugeTLB 分配核心函数 alloc_hugetlb_folio() 强依赖于 struct vm_area_struct(VMA),因为需要通过 VMA 来查找预留(reservation)信息和获取内存策略(mempolicy)。作者指出:"alloc_hugetlb_folio() was so coupled to a VMA that even HugeTLBfs allocates HugeTLB folios using a pseudo-VMA." 这一限制阻碍了 guest_memfd 等新子系统使用 HugeTLB 作为通用大页来源。
dequeue_hugetlb_folio_vma() 中系列的核心思路是将 alloc_hugetlb_folio() 拆分为两层:底层是通用的 hugetlb_alloc_folio(),只关注分配本身和 cgroup 计费;上层 alloc_hugetlb_folio() 保留 VMA 相关的预留逻辑。引入 struct mempolicy_interpreted 承载"已解析的内存策略"。通过六步重构:将参数解析上移、重命名函数移除 VMA 参数、提取 hugetlb_alloc_folio() 作为公开接口。新接口接收 hstate、subpool、gfp、mempolicy_interpreted 和 alloc_flags 参数,其中 alloc_flags 使用位掩码,包含 HUGETLB_ALLOC_CHARG_CGROUP_RSVD 和 HUGETLB_ALLOC_USE_GLOBAL_RESERVATIONS。
作者未提供性能数据。从代码逻辑推断的预期收益:
alloc_flags 位掩码机制使未来增加分配控制选项无需扩展函数签名系列:[PATCH v2 0/2] cgroup/dmem: allow double-charging dmem allocations to memcg作者: Eric Chanudet版本: v2(2个patch)
设备内存(dmem,device memory)cgroup 子系统用于控制设备端内存(如 GPU VRAM)的分配配额。当前,dmem cgroup 的计费与内存 cgroup(memcg)完全分离。这意味着一个 cgroup 中的 dmem 分配虽然限制了 GPU 内存使用,但不计入内存控制器限额,管理员无法通过统一的 memory.max 限制来约束进程的总内存消耗(系统 RAM + 设备内存)。
该系列从 memcg 侧和 cgroup 侧分别实现。在 mm/memcontrol.c 中新增 mem_cgroup_dmem_charge/uncharge() 函数,通过 cgroup_get_e_css() 解析出有效的 memory css 并执行计费,同时引入新的统计计数器 MEMCG_DMEM。cgroup 侧设计了精巧的 4 状态机(OFF/ON/LOCKED_OFF/LOCKED_ON)管理计费开关,状态转换通过 atomic_cmpxchg() 实现无锁原子操作,首次计费后锁定配置防止 TOCTOU 竞争条件。dmem 子系统显式声明了对 memory cgroup 的依赖。
作者未提供性能数据。从代码逻辑推断的预期收益:
memory.max 同时约束系统 RAM 和 GPU 等设备内存的总消耗memory.stat 中新增的 dmem 条目使设备内存消耗可精确观测系列:[PATCH v5 00/13] arm64: Unmap linear alias of kernel data/bss作者: Ard Biesheuvel版本: v5(13个patch)
在 arm64 架构上,内核线性映射(linear/direct map)缺乏随机化,这使得线性映射中内核 data/bss 的可写别名成为一个严重的安全隐患。问题尤为突出的是,遵循原始 arm64 启动协议的引导加载程序——这涵盖了"大量 Android 手机"——会将内核放置在 DRAM 基地址处,导致内核在线性映射中的别名位于完全可预测的位置。如作者所言:"This puts a writable alias of the kernel's data and bss regions at a predictable location, removing the need for an attacker to guess where KASLR mapped the kernel."
系列通过 13 个 patch 逐步构建基础设施并实现 data/bss 线性别名取消映射,分为三个阶段:
基础设施清理:将线性别名 text/rodata 映射为 tagged 类型确保 MTE 兼容;将 empty_zero_page[] 声明为 const 放入 .rodata;修改 init_pmd()/alloc_init_pud() 在映射 DRAM 时保留已存在的表映射和离散描述符。
分离 fixmap 页表:通过链接脚本修改,将 fixmap 页表从 BSS 段移出到新的 .fixmap_pgdir 段。如作者所述:"These page tables are currently the only data objects in vmlinux that are meant to be accessed via the kernel image's linear alias."
取消映射 data/bss:核心函数 remap_linear_data_alias() 使用 set_memory_valid() API 取消映射 data/bss 线性别名(范围从 __init_end 到 __fixmap_pgdir_start)。对于休眠场景,注册 PM notifier 回调自动管理映射/取消映射。
作者未提供性能数据。从代码逻辑推断的预期收益:
empty_zero_page 设为 const,所有架构获得零页线性别名只读的连带安全收益系列:[PATCH] kho: always print scratch sizes作者: Pratyush Yadav版本: v1(1个patch)
KHO(Kexec HandOver)是内核的活跃更新(live update)机制,它在 kexec 过程中保留 scratch 缓冲区以跨越内核版本传递状态。当前,scratch 区域的大小打印仅在用户通过内核引导参数显式指定大小时触发。当大小由运行时根据 RSRV_KERN 类型预留区域自动计算时,这些关键信息完全不输出。作者指出:"These prints are completely useless. When the user specified the size on the command line, they already know how large the scratch areas are."
一个简洁的单 patch 修改:从 kho_parse_scratch_size() 中移除旧的 pr_notice() 调用;在 kho_reserve_scratch() 中实际分配前加入 pr_notice() 打印最终大小(无论来自命令行还是运行时计算);遍历 NUMA 节点时新增每个节点的 scratch 大小打印。打印时机安排在分配操作之前,确保分配失败时大小信息已可用。
作者未提供性能数据。从代码逻辑推断的预期收益:
| vrealloc() shrink | ||
| arm64 COPY_MC | ||
| DAMON data attributes | ||
| HugeTLB generic alloc | ||
| cgroup/dmem memcg | ||
| arm64 unmap data/bss | ||
| kho scratch print |
| KSM rmap optimization | ||
| skip high atomic reserve | ||
| cap compact_gap() | ||
| Per-CPU Work helpers | ||
| swap skip zero check |
| hugetlb reservation leak | ||
| kho order calculation | ||
| page_isolation underflow | ||
| x86 vmemmap leak | ||
| kho scratch alignment | ||
| slub refcount leak | ||
| userfaultfd VMA snapshot | ||
| rmap nr_pages init | ||
| slabinfo utility fix | ||
| damon tried region leak | ||
| memcg NMI slab stats | ||
| memcg obj_stock cache |