- MGLRU: 用 folio_mark_accessed 替代 folio_set_active 优化预读页面处理
- vmscan: 批量 TLB 刷新以减少脏页回收中的 IPI 开销
- page_owner per-fd filter:print_mode 与 NUMA 过滤
- SLUB: 批量分离 node partial slabs 减少锁内链表操作开销
- page_counter stock 重构:将 stock 从 mem_cgroup 下沉至 page_counter
- HMM 与 device page 迁移流程统一:在缺页处理中完成迁移
- SLUB sheave 分配本地 NUMA 节点优化
- userfaultfd RWP:VM 客户机内存工作集追踪
- 通用 pagewalk API:pt_range_walk
1. MGLRU: 用 folio_mark_accessed 替代 folio_set_active 优化预读页面处理
系列:[PATCH v2] mm/mglru: use folio_mark_accessed to replace folio_set_active作者: Barry Song版本: v2(1 个 patch)
背景
MGLRU(Multi-Gen LRU)对映射在页表中的 folio 给予较高优先级的回收保护。在 folio_add_lru() 中,当 MGLRU 启用且处于 page fault 路径时,会无条件调用 folio_set_active() 设置 PG_active 标志。预读(readahead)机制会一次性读入大量 folio,其中许多永远不会通过页表被访问,但 MGLRU 将它们全部标记为 active,放置在较高优先级的 generation 中。这些预读页面占据了"热门"位置,不断将真正的工作集页面挤出,导致错误回收和严重性能退化。
解决的问题
- 预读页面污染工作集:readahead 引入的未被访问页面挤占了本应属于真正工作集的高优先级 generation 位置,导致连续 refault
- anon 页面无条件保护:
lru_gen_folio_seq() 中对非文件页(anon、非 swapcache)无条件给予较低 generation 序号(即更高优先级),不区分是否确实被访问 - working set refault 时的统计问题:在
lru_gen_refault() 中,无论 folio 是否确实属于 workingset,都会无条件累加 WORKINGSET_ACTIVATE 统计
如何做
核心思路是将"无条件 active"改为"按需标记、惰性激活"。涉及 4 个文件的 5 处关键修改:
**folio_add_lru() (mm/swap.c)**:将无条件 folio_set_active(folio) 改为条件逻辑——workingset folio 保持 folio_set_active(),否则使用 folio_mark_accessed() 设置 referenced 标志。这意味着预读但未被访问的 folio 会进入较低优先级的 generation,仅当后续 folio_check_references() 检测到 PTE 确实有 referenced 标志时才会被提升。
**lru_gen_folio_seq() (include/linux/mm_inline.h)**:删除对 anon folio 的无条件保护逻辑 (!folio_is_file_lru(folio) && !folio_test_swapcache(folio))。将 else 分支的 generation 计算改为 MAX_NR_GENS - (folio_test_workingset(folio) || folio_test_referenced(folio)),仅当 folio 被标记为 workingset 或 referenced 时才能获得更年轻的 generation。
**lru_gen_set_refs() (mm/vmscan.c)**:在设置 PG_workingset 标志前增加 folio_test_active(folio) 的前提检查。只有已被激活的 folio(通过 page table walk 或 refault 被明确标识为应该保护的)才会被进一步标记为 workingset。
**lru_gen_refault() (mm/workingset.c)**:将 folio_set_active() 和 WORKINGSET_ACTIVATE 统计移动到 if (workingset) 条件内。只有确实检测到该 folio 属于 workingset 时才设置 active 并计数。
**folio_check_references() (mm/vmscan.c)**:对 workingset 判断逻辑进行相应调整,与 lru_gen_folio_seq() 中的改动保持一致。
收益
ARM64 平台 fio strided read 测试(4KB 对齐、64KB 间隔的跨步读取模式,memcg 400M 限制,600M 文件):
x86 平台 fio strided read 测试(相同条件):
x86 平台内核构建测试(20 线程 memcg 1GB 限制):
| | | |
|---|
| | 1m48.915s | |
| | 3m43.685s | |
| | 915,629 | |
| | 3,207,173 | |
| | 257,271 | |
| | 931,259 | |
2. vmscan: 批量 TLB 刷新以减少脏页回收中的 IPI 开销
系列:[PATCH v4 0/5] mm: batch TLB flushing for dirty folios in vmscan作者: Zhang Peng版本:v4(5 个 patch)
背景
在内存回收路径中,shrink_folio_list() 是核心函数,负责逐个处理待回收的 folio。当遇到脏 folio 需要进行 pageout(写入磁盘或 swap)时,内核需要在写入开始之前刷新 TLB,以清除可能存在的 writable PTE entry,防止 CPU 在 IO 开始后仍然执行写入操作。这一 TLB 刷新通过 try_to_unmap_flush_dirty() 实现,对于多核系统,TLB shootdown 需要跨所有核心发送 IPI(inter-processor interrupt),开销与 CPU 核数成正比。
当前实现的瓶颈在于:每个脏 folio 都会独立调用一次 try_to_unmap_flush_dirty(),意味着在有大量脏 folio 需要回收的场景下,会产生海量的 IPI,严重影响回收效率。
解决的问题
- IPI 风暴问题:遍历回收列表时,每个脏 folio 触发一次 TLB shootdown,导致 IPI 数量和 CPU 核数成正比,在 16 核系统上基准测试产生了 5540 万次 TLB shootdown
- 回收吞吐受限:IPI 开销占据了大量 CPU 时间,导致实际的回收吞吐量(bogo ops/s)受限
如何做
该系列前 4 个 patch 是重构准备,将 shrink_folio_list() 中的内联逻辑提取为独立函数:folio_activate_locked()(激活逻辑)、folio_free()(释放路径)、pageout_one()(pageout 状态机)、folio_try_unmap()(TTU 标志及 unmap 调用)。
Patch 5 实现了核心的批量优化。引入第二个 folio_batch(容量 31 个 folio)用于累积待刷新的脏 folio,以及新函数 pageout_batch()。核心流程为:
累积阶段(在 shrink_folio_list() 中):遇到脏 folio 时不再立即调用 try_to_unmap_flush_dirty(),而是 folio_unlock() 后将其加入 flush_folios 批次。解锁的注释说明:如果持续持有锁等待批次填满,会阻塞通过 swap cache 查找该 folio 的 swap fault。当批次满 31 个时调用 pageout_batch()。
批量处理阶段(pageout_batch() 函数):采用"复用原地数组"技巧——先用 folio_batch_reinit() 清空计数器(底层 folios 数组仍可访问),然后遍历保存的数量:对每个 folio 重新加锁(folio_trylock()),检查可驱逐条件(folio_test_writeback()、folio_mapped()、folio_maybe_dma_pinned()),对整个 batch 执行一次try_to_unmap_flush_dirty(),然后对 batch 中每个 folio 调用 pageout_one() 完成回写。
遍历结束后,若有剩余未满批次的 folio,在 shrink_folio_list() 末尾再调用一次 pageout_batch() 处理。
收益
测试环境:stress-ng --vm 16 --vm-bytes 2G --vm-keep --timeout 60s,16 CPU,约 32G 内存,8G swap,512M memcg 限制。
3. page_owner per-fd filter:print_mode 与 NUMA 过滤
系列:[PATCH v9 0/4] mm/page_owner: add per-fd filter infrastructure for print_mode and NUMA filtering作者: Zhen Ni版本:v9(4 个 patch)
背景
page_owner 是内核内存分配追踪的核心调试工具,通过 page_ext 框架为每个物理页面记录分配调用栈、时间戳、PID/TGID 等信息。在大内存配置(250GB+)下收集 page_owner 信息会产生数 GB 的输出,带来三方面挑战:(1) 存储压力;(2) 从生产环境传输大文件的困难;(3) 用户态后处理工具的 CPU 开销。
文件体积膨胀的根本原因在于冗余的栈追踪信息:虽然内核已通过 stackdepot 对调用栈进行了去重(返回 32 位 handle),但 page_owner 在输出时为每个页面都展开并打印完整的调用栈文本。此外,在 NUMA 感知的生产部署中,OOM 事件通常是节点级别的,但此前 page_owner 无法按 NUMA 节点过滤。
解决的问题
- 输出体积过大:250GB+ 内存系统中,page_owner 输出可达 10GB+
- NUMA 过滤能力缺失:无法按 NUMA 节点选择性输出页面信息
- 多用户并发读取冲突:之前的 debugfs 文件不支持独立过滤状态
如何做
整个方案围绕 per-file-descriptor 过滤状态 的设计展开,核心思路是为每次 open() 调用分配独立的过滤状态。
状态结构(struct page_owner_filter_state)包含四个字段:print_mode(枚举值 PAGE_OWNER_PRINT_STACK / HANDLE / STACK_HANDLE)、nid_filter(nodemask_t 类型)、nid_filter_enabled(布尔标志)、lock(自旋锁,保护读写路径并发)。
文件操作改造:为 page_owner_fops 新增 .open、.release、.write 三个成员,权限从 0400 改为 0600。.write 解析类似 shell 的命令字符串(如 mode=handle nid=0,2-3),使用 strsep() 分割,sysfs_match_string() 匹配 print_mode,nodelist_parse() 解析 NUMA 节点列表(支持单节点、范围、混合格式),最后在 spinlock 保护下原子提交。
print_mode 过滤:handle 模式跳过调用栈展开,输出格式为 "handle: 1048577",文件体积缩减约 66%。handle 到实际调用栈的映射可通过现有 show_stacks_handles 接口离线查询。
NUMA 过滤:使用 memdesc_nid() 提取页面 node ID(特意替代 page_to_nid() 以绕过 PF_POISONED_CHECK 断言),与 nid_filter 做 node_isset() 检查。附带 tools/mm/page_owner_filter.c 用户态工具,提供完整的输入校验。
收益
测试环境:250GB+ 内存、4 节点 NUMA 的生产环境服务器,基于 v6.18-rc1 构建,启用 CONFIG_PAGE_OWNER
4. SLUB: 批量分离 node partial slabs 减少锁内链表操作开销
系列:[PATCH] mm/slub: batch-detach node partial slabs作者: Hao Li版本:1 个 patch
背景
SLUB 分配器的 get_partial_node_bulk() 函数负责从 node 级别的 partial list 中获取一组 partial slab 到 per-CPU 的本地列表中。在持有 n->list_lock 自旋锁的情况下,原有实现对每个符合条件的 slab 执行 remove_partial(n, slab) + list_add() 操作——即单独从 node partial list 中删除、再单独添加到本地 pc->slabs 列表。
在实际运行中,该循环常常会连续分离出多个相邻的 slab,这意味着在锁临界区内反复操作链表指针(修改前后节点的 next/prev 指针),造成不必要的链表指针抖动和缓存行颠簸(cache line bouncing)。
解决的问题
- 锁临界区内的重复链表操作:每个 slab 的
remove_partial() + list_add() 都涉及多次指针修改,当连续有多个匹配的 slab 时这些操作是冗余的 - 链表指针抖动:反复修改链表节点指针,在多核并发争抢
n->list_lock 的场景下增加了缓存一致性协议开销
如何做
引入 first 和 last 两个 struct slab * 指针追踪连续匹配段。遍历 n->partial 列表时不再对每个 slab 执行 remove_partial() 和 list_add(),而是调用 slab_clear_node_partial(slab) 清除 partial 状态并递减 n->nr_partial 计数。当遇到 pfmemalloc_match() 不匹配的 slab 时(连续段中断),调用 list_bulk_move_tail(&pc->slabs, &first->slab_list, &last->slab_list) 一次性将整段移动到 pc->slabs 列表。
关键之处在于:list_bulk_move_tail() 仅修改该段首尾节点的指针及目标链表的相关指针,中间所有节点的指针无需逐个修改。这显著减少了锁临界区内的指针操作次数(从 2N 次降至常数级别)。
收益
| | |
|---|
| will-it-scale 基准测试套件,mmap2 分配压力场景 | |
5. page_counter stock 重构:将 stock 从 mem_cgroup 下沉至 page_counter
系列:[PATCH v3 0/7] mm/memcontrol, page_counter: move stock from mem_cgroup to page_counter作者: Joshua Hahn版本:v3(7 个 patch)
背景
memcg 当前在每个 CPU 上维护一个 "stock"(预充电缓存),缓存最多 MEMCG_CHARGE_BATCH(64)个已充电页面,使小分配可以避免每次充电都遍历昂贵的 mem_cgroup 层级并执行原子操作。这个机制在 try_charge_memcg() 的快速路径中被使用,是 memcg 性能的关键优化。
然而每个 CPU 的 memcg_stock_pcp 只能追踪最多 NR_MEMCG_STOCK(7)个 memcg。当单个 CPU 上活跃充电的 memcg 超过 7 个时,会随机选择一个受害者驱逐并排空其 stock,导致不必要的延迟和"颠簸"。此外,stock 管理与 struct mem_cgroup 紧密耦合,使得为 memcg 添加新的 page_counter 并拥有独立的 stock 变得极其困难——这正是作者的分层内存限制(tiered memory limits)系列所需的基础设施。
解决的问题
- 7 槽位限制导致随机驱逐:超过 7 个活跃 memcg 时随机驱逐受害者,造成 stock 被频繁排空和重新填充,cache 命中率下降
- stock 与 memcg 紧耦合:所有 page_counter 共享同一个 stock 资源,无法为新的 page_counter 单独启用 stock 加速路径
- 充电路径逻辑复杂:
try_charge_memcg() 中的 stock 消费、贪婪充电、失败重试、refill 逻辑与 limit check 交织在一起
如何做
在 struct page_counter 中引入 per-CPU 的 struct page_counter_stock(包含 local_trylock_t lock 和 unsigned long nr_pages)。每个 page_counter 可以独立地通过 page_counter_enable/disable/free_stock() 管理自己的 stock。
充电路径简化:page_counter_try_charge() 现在直接感知 stock——如果 stock 中有足够页面,直接从 stock 消费(无层级遍历);否则贪婪地请求 max(batch, nr_pages) 个页面,成功后将多余页面尝试存入 stock(使用 trylock,失败则 uncharge 回去)。try_charge_memcg() 中的 consume_stock()、refill_stock()、MEMCG_CHARGE_BATCH 批量重试逻辑全部被移除,简化为直接调用 page_counter_try_charge()。
drain 路径重构:drain_all_stock() 重构为以 CPU 为外循环遍历(而非以 memcg 为外循环),通过 work_on_cpu() 在每个 CPU 上调用 drain_stock_on_cpu(),复杂度从 O(memcgs * CPUs) 降至 O(CPUs)。cgroup v2 优化了跳过 root memcg 的路径。memcg 原有的 struct memcg_stock_pcp 被移除。
收益
40 CPU、50G 内存系统,反复 fault 和 madvise MADV_DONTNEED 释放匿名页测试(40 次试验各 50 次迭代):
6. HMM 与 device page 迁移流程统一:在缺页处理中完成迁移
系列:[PATCH v12 0/5] Migrate on fault for device pages作者: Mika Penttila版本:v12(5 个 patch)
背景
GPU 等设备驱动使用 HMM(Heterogeneous Memory Management)框架实现 CPU 与设备内存之间的页面迁移。现有流程将"缺页处理"(fault handling)和"页面迁移"(migration)作为两个独立的 page table walk 操作,由调用方自行编排顺序。
方案一(先 fault 再迁移):hmm_range_fault() fault in 页面,然后 migrate_vma_*() 迁移。即使大多数时候页面已 present 或为零页映射,仍需要 2 次 page table walk。方案二(先迁移再 fault):先 migrate_vma_*() 迁移,如检测到未 present 页则调用 hmm_range_fault() 再重新走迁移。最坏需要 3 次 page table walk。在 x86-64 平台上,单次 page table walk 的开销可超过 1000 个 CPU cycle。
解决的问题
- 最坏情况下 3 次 page table walk 的严重性能损失
- 常见路径(页面已 present)仍需要 2 次 walk 的低效设计
- 调用方需要自行处理两种流程的编排和错误恢复,编码复杂且容易出错
如何做
核心思路是将迁移的"收集"(collect)阶段整合进 HMM 的 page table walk 中,使得一次 walk 同时完成 fault in 和 migration entry 的设置。
标志位与数据结构扩展:在 enum hmm_pfn_flags 中新增 HMM_PFN_REQ_MIGRATE 输入请求标志和 HMM_PFN_MIGRATE、HMM_PFN_COMPOUND 输出标志。struct hmm_range 新增 struct migrate_vma *migrate 字段。enum migrate_vma_info 新增 MIGRATE_VMA_FAULT 标志。
HMM page walk 的锁管理重构:struct hmm_vma_walk 扩展了 mmu_notifier_range、vma、start/end 以及锁管理字段。关键变化在于 hmm_vma_walk_pmd() 通过 pmd_lock() 持有 PMD 锁后进行迁移准备。新增 HMM_ASSERT_PTE_LOCKED、HMM_ASSERT_PMD_LOCKED、HMM_ASSERT_UNLOCKED 宏用于调试断言。
迁移准备逻辑移入 HMM walk:hmm_vma_handle_migrate_prepare()(PTE 级别)和 hmm_vma_handle_migrate_prepare_pmd()(PMD 级别)实现迁移 entry 安装逻辑,包括处理 anonymous 零页、device private/coherent 页、拆分 large folio、安装 migration entry(通过 set_pte_at() 写入 swp entry,保留 dirty/accessed/soft-dirty/uffd-wp 属性位)。
migrate_vma_setup() 统一入口:原有的 migrate_vma_collect() 及相关 page walk ops 被删除(约 500 行)。migrate_vma_setup() 现在构造 struct hmm_range,设置 default_flags = HMM_PFN_REQ_MIGRATE,调用 hmm_range_fault() 完成收集。新增 migrate_hmm_range_setup() 接口将 HMM pfn 数组转换为 migrate pfn 数组。
收益
作者未提供独立的量化性能数据(cover letter 声明纯 migrate 路径吞吐与基线持平,"within error margin")。从代码逻辑推断的预期收益:
- 将最坏情况下 3 次 page table walk 减少为 1 次,每次 walk 在 x86-64 上约 1000+ CPU cycles
- 常见路径(页面已 present)从 2 次 walk 减少为 1 次
- 从
migrate_device.c 删除约 500 行重复代码(8 files changed, 1053 insertions(+), 578 deletions(-)) migrate_vma.vma/start/end 在 fault 路径自动由 HMM pagewalker 填充,驱动代码更简洁- 统一的
MMU_NOTIFY_MIGRATE 通知范围由 walker 侧精确管理
7. SLUB sheave 分配本地 NUMA 节点优化
系列:[PATCH] mm/slub: allocate sheaves on local memory nodes作者: Hao Li版本:v1(1 个 patch)
背景
Sheave(struct slab_sheaf)是 SLUB 分配器的 per-CPU 元数据结构,包含对象数组(objects[]),在本地快速路径中被频繁访问。当前使用 kzalloc() 分配 sheave,未指定 NUMA 节点提示,可能导致 sheave 被分配到远程 NUMA 节点。虽然功能正确,但从 NUMA 局部性角度并非最优。此外在启动阶段(bootstrap),需要为所有 possible CPU 分配 sheave,但此时只有 online CPU 的 cpu_to_mem() 已完成初始化,直接使用会出错。
解决的问题
kzalloc() 未指定 NUMA 节点,sheave 可能分配在远程节点上,per-CPU 快速路径面临跨 NUMA 节点内存访问- 启动阶段
cpu_to_mem() 仅为 online CPU 初始化,直接使用会出错
如何做
两个改动点(仅修改 mm/slub.c,+11/-4 行):
运行时分配:__alloc_empty_sheaf() 新增 int node 参数,内部将 kzalloc() 替换为 kzalloc_node()。alloc_empty_sheaf() 调用时传入 numa_mem_id()。
启动阶段分配:bootstrap_cache_sheaves() 中参考 __build_all_zonelists 的做法,使用 local_memory_node(cpu_to_node(cpu)) 计算内存节点号,而非调用尚未初始化的 cpu_to_mem()。
收益
作者声明:"no measurable performance improvement was observed, but this approach is theoretically correct." 作者未提供性能数据。从代码逻辑推断的预期收益:
- 在理论上减少跨 NUMA 节点访问延迟,sheaf 中所有对象均来自 local node,保证访问时不会触发远端内存读取
- 对于 NUMA 亲和性敏感的 workload(如 per-node memory binding 场景),对象分配路径的 NUMA 一致性得到加强
- 为未来在 NUMA 拓扑更复杂的系统(如 CXL memory 扩展)上的优化奠定正确性基础
- 该路径处于非热路径(refill spilling 为低频事件),添加节点检查的额外开销极低
8. zswap 按 cgroup 主动写回机制
系列:[PATCH v2 0/4] mm/zswap: Implement per-cgroup proactive writeback作者: Hao Jia版本:v2(4 个 patch)
背景
zswap 当前的写回(writeback)完全是反应式的:要么由 shrinker 在内存回收时触发,要么在 zswap pool 达到大小上限时触发。缺乏针对特定 cgroup 的主动写回手段。正如作者在 cover letter 中所述:"users may want to prepare for an upcoming memory-intensive workload by flushing cold memory to the backing storage when the system is relatively idle"。
解决的问题
- 缺乏针对特定 cgroup 的主动 zswap 写回手段,shrinker 无法按用户意图精确控制写回目标
- 系统处于稳态(无内存压力)时,zswap pool 中的冷页无法及时被刷出
- 无法区分被动和主动写回:现有的
zswpwb 计数器混合了所有来源
如何做
整体方案从三个维度构建 per-cgroup 主动写回能力:
核心数据结构:struct zswap_wb_iter 嵌入 struct mem_cgroup,作为 per-memcg 的回写游标,在指定 cgroup 子树的 zswap LRU 上持续推进写回。新增 zswpwb_proactive 计数器与被动 zswpwb 区分。
算法流程:用户通过 memory.reclaim 接口触发后,遍历目标 memcg 子树,对每个 memcg 调用 zswap_proactive_shrink_memcg() 遍历所有 N_NORMAL_MEMORY 节点上的 zswap LRU,通过 list_lru_walk_one() 配合 shrink_memcg_cb() 回调执行逐 entry 写回。扫描量由 ZSWAP_PROACTIVE_WB_SCAN_RATIO(16 倍剩余 budget)控制,批次大小为 ZSWAP_PROACTIVE_WB_BATCH(128 页)。双重限流保护防止扫描和写回对系统造成冲击。
用户接口:扩展已有 memory.reclaim 接口,新增 zswap_writeback_only token。当该 token 被指定时直接调用 zswap_proactive_writeback()。使用示例:echo "100M zswap_writeback_only" > memory.reclaim。该 key 与 swappiness 互斥,同时指定返回 -EINVAL。
收益
作者未提供性能数据。从代码逻辑推断的预期收益:
- 用户可在业务低峰期主动触发 cgroup zswap 写回,避免高峰期被动写回导致的性能抖动
- Per-cgroup 粒度使定向内存管理成为可能,仅影响目标 cgroup 的压缩缓存
zswpwb_proactive 计数器实现主动/被动写回统计隔离,便于运维观测- 双重限流(扫描比例 16x + 批量上限 128 页)防止单次操作消耗过多 CPU
- 复用已有
memory.reclaim 接口(基于 Yosry Ahmed 和 Nhat Pham 的 review 建议),避免引入新的 cgroup 控制文件
9. userfaultfd RWP:VM 客户机内存工作集追踪
系列:[PATCH v4 00/14] userfaultfd: working set tracking for VM guest memory作者: Kiryl Shutsemau (Meta)版本:v4(14 个 patch)
背景
在现代虚拟化场景中,VMM(Virtual Machine Monitor)需要对客户机内存进行分层管理:将热页面保留在快速本地存储上,将冷页面驱逐到慢速分层或远端存储。这要求 VMM 能够 (1) 检测哪些页面正在被访问(工作集追踪),(2) 安全地驱逐冷页面,(3) 在页面被重新访问时按需取回。
现有的 userfaultfd 提供了 MODE_MISSING(缺页)、MODE_WP(写保护)、MODE_MINOR(minor fault),但没有一种模式能够高效地追踪读访问——MODE_WP 只拦截写操作,而 MODE_MISSING 需要实际解除页面映射,开销过大。
解决的问题
- 缺少读访问追踪能力:MODE_WP 仅能捕获写操作,无法检测读访问
- 现有方案开销大:使用 MODE_MISSING 需要将页面从页表中移除,每次检测周期都需要大量的 page fault 和重新映射
- 缺少异步工作集扫描路径:同步模式下每次访问都需要与 userspace handler 通信,延迟高,不适合高频的周期性工作集检测
如何做
本系列新增 UFFDIO_REGISTER_MODE_RWP(Read-Write Protect)模式,与现有的 MISSING/WP/MINOR 并列。核心技术架构分为三层:
第一层:基础设施解耦(Patch 1-3)
引入 CONFIG_ARCH_HAS_PTE_PROTNONE,将 pte_protnone()/pmd_protnone() 从 CONFIG_NUMA_BALANCING 解耦(RWP 也需要使用 PROT_NONE PTE 作为访问追踪标记)。六种架构(x86_64、arm64、powerpc、s390、riscv、loongarch)选择该选项。uffd PTE 位重命名为 _PAGE_BIT_UFFD/pte_uffd(),同时承载 WP 和 RWP 两种语义,由 VMA 标志区分。
第二层:内核机制(Patch 4-7)
新增 VM_UFFD_RWP VMA 标志(与 VM_UFFD_WP 互斥)。MM_CP_UFFD_RWP change_protection 原语:安装 PAGE_NONE 并设置 uffd 位;MM_CP_UFFD_RWP_RESOLVE 恢复 vma->vm_page_prot 并清除 uffd 位。通过 change_pte_range()、change_huge_pmd() 和 hugetlb_change_protection() 统一处理 anon、shmem、THP 和 hugetlb。在 swap、device-exclusive、migration、fork、mremap、mprotect 路径中保留 RWP 标记。khugepaged/rmap 中的 userfaultfd_wp() 检查扩展为 userfaultfd_protected()(覆盖 WP 和 RWP)。GUP 的 gup_can_follow_protnone() 对 VM_UFFD_RWP VMA 强制返回 false。
第三层:用户态接口(Patch 8-12)
新增 UFFDIO_RWPROTECT 保护/解除保护 ioctl。Fault 分发中 sync 模式通过 handle_userfault() 投递 UFFD_PAGEFAULT_FLAG_RWP 消息;async 模式内核原地解析——恢复原始 PTE 权限,清除 uffd 位,fault 线程不阻塞继续执行。UFFD_FEATURE_RWP_ASYNC 使能异步模式,UFFDIO_SET_MODE 运行时切换 sync/async。PAGEMAP_SCAN 集成新增 PAGE_IS_ACCESSED 标志,实现 O(1) 冷页发现。
典型 VMM 工作流:UFFDIO_RWPROTECT 安装保护 → sleep(interval) → PAGEMAP_SCAN(!PAGE_IS_ACCESSED) 发现冷页 → 切换 sync 模式 → 安全驱逐 → 恢复 async 模式。
收益
作者未提供性能数据。从代码逻辑推断的预期收益:
- 利用 CPU 的 PROT_NONE 机制在硬件层面捕获所有访问(读和写),无需逐页改变页表即可实现工作集检测
- 通过
PAGEMAP_SCAN 扫描 uffd 位(而非逐页触发 fault),检测周期内扫描开销与页面数量成正比但无需用户态-内核态来回切换 - async 模式实现零消息开销的被动检测,sync 模式提供精确的 race-free 驱逐,可在运行时动态切换
- anon、shmem、hugetlbfs 使用完全相同的 PROT_NONE + uffd 位机制,无需为不同内存类型编写不同逻辑
- 51 个文件的修改广度表明该功能深入触及了 PTE 操作的各个关键路径,设计成熟度高
10. 通用 pagewalk API:pt_range_walk
系列:[RFC PATCH v3 0/8] Implement a new generic pagewalk API作者: Oscar Salvador版本:RFC v3(8 个 patch)
背景
Linux 内核中存在多个需要遍历进程页表的子系统,如 /proc/pid/smaps、numa_maps、pagemap 和 clear_refs。当前代码使用基于回调函数的通用 pagewalk API(struct mm_walk + mm_walk_ops),但其存在多个痛点:(1) 各 /proc 接口之间存在大量重复代码,(2) hugetlb 被特殊化为 PTE 级别处理,无法按真实的大页级别(PMD/PUD)来遍历,(3) 锁管理和批处理逻辑分散在各调用者中。
在 LSFMM/BPF 2025 上,社区达成共识:需要统一 pagewalk API 来取代现有回调式 API,使 HugeTLB 能够像普通页面一样被处理而无需特殊 case。
解决的问题
- 代码重复:
fs/proc/task_mmu.c 中 smaps、numa_maps、pagemap、clear_refs 各自实现了大量相似的页表遍历逻辑 - hugetlb 的特殊处理:当前 API 使用
.hugetlb_entry 回调伪装成 PTE 级别条目 - 锁管理分散和批处理不一致:每个调用者需要自行管理 PTL 锁和连续 PTE 批处理
如何做
实现全新的 pt_range_walk API,核心设计理念是"让 API 管理锁和批处理,调用者只需消费结果"。
基础设施:新增 softleaf_from_pud() 解析 1GB 大页,pmd_huge_lock()/pud_huge_lock() 统一 PMD/PUD 锁获取,folio_pmd_batch() 实现 PMD 级别的 folio 批处理(类比 folio_pte_batch())。
核心 API:定义 8 种内部返回类型(PT_TYPE_NONE、FOLIO、MARKER、PFN、SWAP、MIGRATION、DEVICE、HWPOISON)。API 包含 pt_range_walk_start()、pt_range_walk_next()、pt_range_walk_done() 三个函数。调用者通过 pt_type_flags_t 位掩码指定感兴趣的类型,API 自动跳过不感兴趣的类型。返回的 struct pt_range_walk 包含了 entry 的完整信息:folio、page、大小、可写/young/dirty 状态、页表级别、批处理条目数等。
接口转换:将 fs/proc/task_mmu.c 中 /proc/pid/smaps、numa_maps、pagemap、clear_refs 全部转换为使用新 API。hugetlb VMA 与普通 VMA 在同一循环中处理,无需 is_vm_hugetlb_page() 分支。
收益
作者未提供性能数据。从代码逻辑推断的预期收益:
- 该文件代码从约 2295 行减少到约 1065 行(-1230 行,约 54%),消除了大量重复逻辑
- hugetlb 不再需要
.hugetlb_entry 特殊回调,PUD/PMD 级别的 hugetlb 页按真实大小返回 - 内置锁管理使调用者不再需要关心 PTL 获取/释放、VMA 遍历锁、pte_offset_map 配对等细节
- 类型过滤使调用者通过位掩码精确控制感兴趣的类型,API 自动跳过不需要的条目
page-types 工具测试显示新旧 API 输出完全一致,pagemap_ioctl 测试 113/117 通过- 注意该系列仍为 RFC,作者明确指出还需解决
make_uffd_wp_huge_pte 的 PTE_MARKER 问题和 i_mmap_lock 获取问题
总结
性能优化
| | |
|---|
| MGLRU: folio_mark_accessed | fio strided read: refault 从千万级降至 0,带宽提升 86-584x;内核构建 swap 相关指标 -26%~31% |
| vmscan: batch TLB flushing | TLB shootdowns -68.2%,IPI -57.4%,回收吞吐 +26.9% |
| page_owner per-fd filter | 输出缩减 66%(244MB→84MB),读取性能提升 4.4x |
| SLUB: batch-detach partial slabs | will-it-scale mmap2 测试 +5% |
| page_counter stock 重构 | 32 cgroup 场景改善 3.5%,消除 7 槽位限制 |
| HMM migrate on fault | 将 2-3 次 page table walk 减少为 1 次,删除约 500 行重复代码 |
| SLUB sheave local node | |
新机制 / 新接口
| | |
|---|
| zswap per-cgroup proactive writeback | 通过 memory.reclaim 新增 zswap_writeback_only key,支持 cgroup 级主动 zswap 写回 |
| userfaultfd RWP | 新增 UFFDIO_REGISTER_MODE_RWP 模式,利用 PROT_NONE PTE 实现 VM 客户机内存工作集追踪,支持 sync/async 双模,51 个文件 |
内部优化 / 清理
| | |
|---|
| pt_range_walk API | 新通用 pagewalk API,统一 hugetlb 处理,消除 /proc 接口中 ~54% 重复代码 |
Bug Fix
| | |
|---|
| alloc_tag: fix use-after-free in /proc/allocinfo | 模块卸载后 /proc/allocinfo 读取导致的 use-after-free,Fixes: 9f44df50fee4 |
| mm: free HIGHATOMIC/CMA frozen pages via buddy | HIGHATOMIC/CMA 页面被重映射为 MIGRATE_MOVABLE,修复后直接通过 free_one_page() 释放 |
| | blk_idx 泄漏 + 压缩后页面尾部残留旧数据未清零 |
| riscv: mm: mark_new_valid_map after hotplug vmemmap | RISC-V 缓存 non-present TLB 条目导致新映射不可见 |
| mm/hugetlb_vmemmap: fix vmemmap restore rollback | tail-page 模板覆盖 head-page 元数据,Fixes: c0b495b91a47, Cc: stable |
| mm/damon: fix stale TLB young-state on arm64 | ptep_test_and_clear_young() 不清除 TLB 导致 nr_accesses 始终为 0 |
| mm/damon/ops-common: folio_test_lru after folio_get | 投机性 lru 检查导致竞争中的 VM_BUG_ON_PGFLAGS,Fixes: 3f49584b262c, Cc: stable |
| mm: make mmap_miss accounting symmetric | VM_SEQ_READ 的 mmap_miss 计数不对称影响 readahead 行为 |