系列:[PATCH v5 00/12] mm, swap: swap table phase IV: unify allocation and reduce static metadata作者: Kairui Song版本: v5(12个patch)
Linux swap 子系统历史上维护了两套独立的静态元数据数组:swap_cgroup_ctrl(每个 swap 设备一个 vzalloc 分配的数组,记录每个 swap 槽位归属的 memcg ID)和 zeromap(kvmalloc 分配的 bitmap,记录零填充页)。这两者都是"全或无"式分配——无论 swap 设备是否被实际使用,挂载时即全额分配。对于 1TB 的 swap 设备,仅 swap_cgroup_ctrl 就需要约 512MB 内存(每个槽位 2 字节)。此外,匿名页(anon)和 shmem 的大页 swapin 走不同的分配路径,各有自己的 fallback 逻辑和 swap cache 检查;memcg 归属检查在缺页异常的页表遍历(page table walk)阶段进行,与 swap 层的 cluster 锁保护脱节,增加竞态窗口。
该系列是 swap 表重构的第四阶段,建立在前期工作(第一阶段引入 per-cluster swap_table 替换全局 swap_map 数组,第二阶段引入 swap_cluster_info 和 cluster 锁,第三阶段支持基于 folio 的 swap cache 操作)之上。
mm/memory.c 和 mm/shmem.c 中。swap_cache_alloc_folio 返回已存在的 folio 或新分配的 folio 或 NULL,调用者需要额外的 bool *alloced 输出参数来区分。swap_pte_batch() 在页表遍历时查询 cgroup ID,此时并未持有 cluster 锁,存在 TOCTOU 竞态。set_bit/clear_bit/test_bit 原子操作。该系列的核心设计思想是:将 swap 的所有元数据(swap table entry、zero flag、cgroup ID)全部折叠进 per-cluster 的动态分配结构中,彻底消除全局静态数组。
1. API 重构(patch 1-2)。swap_cache_alloc_folio 不再返回已有 folio 作为成功,而是返回 ERR_PTR。新增 swap_cache_read_folio 封装"读取或返回已有缓存"语义,内部在 -EEXIST 时自动重试。新增 __swap_cache_add_check 独立检查函数,在 cluster 锁下原子地验证整个范围的所有 slot 状态。这为后续统一 large folio 分配铺平道路。
2. Large folio 分配统一(patch 4-5)。 核心是引入 swap_cache_alloc_folio 的带 orders 位掩码版本和新的 swapin_sync 统一入口。swapin_sync(entry, gfp, orders, vmf, mpol, ilx) 替代了原先的 alloc_swap_folio(anon 端在 do_swap_page 中调用)和 shmem_swap_alloc_folio(shmem 端),在紧凑的循环内完成:检查 swap cache 是否已有 folio -> 尝试按最高 order 分配 -> 在 cluster 锁下验证整段 slot 可用 -> 添加 folio 到 swap cache -> 发起 IO。如果遇到 -EEXIST 竞态则重试,如果遇到 -EBUSY(slot 被不兼容的 zeromap 或 memcg 占用)则 fallback 到下一 order。
// 统一入口,anon和shmem均调用此函数
struct folio *swapin_sync(swp_entry_t entry, gfp_t gfp, unsigned long orders,
struct vm_fault *vmf, struct mempolicy *mpol, pgoff_t ilx)
3. memcg 数据迁移到 cluster(patch 8, 10, 11)。 在 swap_pte_batch() 中删除了 lookup_swap_cgroup_id() 调用,将 memcg 检查推迟到 __swap_cache_add_check() 中在 cluster 锁保护下执行。新增 per-cluster 的 swap_memcg_table 结构(unsigned short id[SWAPFILE_CLUSTER],大小 512/1024 字节,通过 kzalloc 分配),提供 __swap_cgroup_set、__swap_cgroup_get、__swap_cgroup_clear 三个 cluster 锁保护的访问函数。最后完全删除 mm/swap_cgroup.c 和 include/linux/swap_cgroup.h。
4. zeromap 内联(patch 12)。 在 64 位系统上,swap table entry 有足够的 bit 空间同时容纳 PFN 和 flag,因此将 zero flag 作为 SWP_TB_ZERO_FLAG 存入 entry 的高 bits 中。在 32 位系统上,PFN 可能占据过多 bit 导致没有剩余空间,此时回退到 per-cluster 的 zero_bitmap。zeromap 的设置和清除需要持有 folio 锁和 cluster 锁(由 swap_zeromap_folio_set/clear 内部获取),保证了与 swap cache 操作的一致性。
5. 批量 memcg 释放优化(patch 7)。__swap_cluster_free_entries 在释放 slot 时,对连续相同 memcg 的 slot 进行批处理,减少 mem_cgroup_uncharge_swap 调用次数。
free -m | -528 MB (65.6%) | |||
| -95.3% | ||||
| +1.42% | ||||
| -2.77% | ||||
作者指出:"挂载 1TB swap 设备可节省约 512MB 内存",且"现在每个槽位约 0.09375 字节(每个 cluster 48 字节 ci info,覆盖 512 个 slot)"。在高压测试中,"不再看到任何 'Huh VM_FAULT_OOM leaked out to the #PF handler' 错误"。
值得注意的是,该系列删除了整个 swap_cgroup.c 文件,将元数据管理从全局静态分配彻底转为按需动态分配。未使用(空闲)的 cluster 完全不分配元数据表,进一步压缩了实际开销。
系列:[PATCH 0/4] mm: speed up ZONE_DEVICE memmap initialization作者: Li Zhe版本: v1(4个patch)
memmap_init_zone_device() 负责初始化 ZONE_DEVICE 类型内存区域(如 PMEM、DAX 设备)的 struct page 元数据。对于大容量设备(例如 100GB 的 PMEM 命名空间),这意味着对数十万乃至上百万个 PFN 逐一调用 __init_zone_device_page(),每次调用重复设置几乎完全相同的 struct page 字段——包括 __init_single_page 的基本初始化、zone 和 node 设置、pgmap 指针赋值等。在热路径(如 nd_pmem 驱动的 bind/rebind)中,这个初始化延迟可达数百毫秒,影响设备的启动和重配置响应时间。
struct page 的绝大部分字段在同一个 ZONE_DEVICE 区域内是完全相同的,但现有代码对每个 PFN 都重新计算和写入一遍。pfns_per_compound > 1 时(如 devdax 的 2MB 对齐),每个 tail page 也独立调用完整初始化流程。该系列采用"模板复制 + 架构优化存储指令"策略,分四步实现加速。
1. 函数拆分(patch 1)。 将 __init_zone_device_page 拆分为三个独立部件:zone_device_page_init_refcount() 根据 pgmap->type 决定初始 refcount、generic_init_zone_device_page_slow() 执行通用 struct page 字段初始化(调用 __init_single_page、设置 zone/node/pgmap 等)、zone_device_page_init_pageblock() 处理 pageblock 的 migratetype 标记。拆分后的函数保持慢路径行为不变,但各个部件可被快路径复用。
2. 模板化头页快路径(patch 2)。 在 64 位系统上,先用慢路径构建一个"模板 page",然后在热路径循环中将模板直接复制到每个目标 page。复制后仅修复 PFN 相关的两个字段:SECTION_IN_PAGE_FLAGS 中的 section 编号和 WANT_PAGE_VIRTUAL 中的 page->virtual。构建时检查 sizeof(struct page) 是 u64 对齐的(BUILD_BUG_ON)。当 page_ref_set tracepoint 活跃时自动退回到慢路径,因为模板复制绕过了 set_page_count()。
3. 模板化 compound tail 快路径(patch 3)。 为 compound page 的 tail pages 构建独立的 tail 模板(通过 prep_compound_tail() 设置 compound 链接信息)。由于同一 compound 内所有 tail 共享相同的 compound 状态,一个 tail 模板可复用于整个 compound 的 tail 范围。每个 tail page 的初始化简化为模板复制 + PFN 字段修复。
4. 架构优化存储指令(patch 4)。 引入 arch_optimize_store_u64() 和 arch_optimize_store_drain() 接口及 x86-64 MOVNTI/SFENCE 实现。模板复制采用展开的固定偏移量 u64 存储序列(switch (sizeof(struct page)) fallthrough 展开),而非运行时循环。这确保编译器生成固定偏移的存储指令链而非 memcpy 调用。x86-64 的 MOVNTI(Move Non-Temporal)指令是流式写入模式,避免常规缓存存储对 CPU 缓存的污染——这对"写入一次、几乎不再读取"的 ZONE_DEVICE struct page 非常合适。SFENCE 确保在 compound head 初始化前和函数返回前所有 non-temporal 存储已排空。在 KASAN/KMSAN 构建下退回到通用版本(普通存储),因为 sanitizer 依赖编译器插桩的存储。
测试环境:Intel Ice Lake 服务器上的虚拟机,两个 PMEM 配置各 rebind 30 次取稳态平均值。
| -14.4% | ||||
| -61.8% | ||||
| -15.1% | ||||
| -62.7% |
作者在封面信中总结:"memmap_init_zone_device() 在初始化大容量 ZONE_DEVICE 范围时会花费大量时间,因为它对每个 PFN 都重复几乎完全相同的 struct page 设置。" 经过优化后,100GB 设备的稳态 rebind 延迟从约 300ms 降至约 110ms,加速比约 2.6 倍。
该系列的优化仅限于 64 位系统("有意限制在 64 位构建"),因为模板复制依赖 struct page 的固定布局和 u64 对齐存储。作者指出 arm64 等架构可以在后续工作中添加自己的优化后端。
系列:[RFC PATCH 0/4] mm/shmem: optimize read performance with folio batching作者: Chi Zhiling版本: RFC v1(4个patch)
shmem(tmpfs)是 Linux 内核中广泛应用的内存文件系统,其读路径 shmem_file_read_iter() 长期以来以逐页(per-page)方式获取 folio 并立即解锁后执行 copy 操作。由于 shmem 读取发生在 i_rwsem 保护之外,folio_lock 在获取 folio 后立刻锁-解锁的模式实际提供了极为有限的保护——folio_lock 虽然能在持锁期间防止 truncate 并发,但一旦解锁,truncate 同样可以发生。在这种情况下,"作者指出 folio 引用计数才是真正在访问期间阻止回收的机制,lock 是多余的"。此外,这一逐页获取的模式意味着每次迭代都需要一次 shmem_get_folio() 调用,涉及 page cache 查找和锁操作,在高吞吐场景下成为瓶颈。
shmem_file_read_iter()、shmem_file_splice_read() 和 shmem_get_link() 三个函数在获取 folio 后立即解锁然后再读数据,lock 保护无实际意义却引入了不必要的原子操作开销。SGP_READ/SGP_GET 在空洞(hole)时返回 0 和 NULL folio,而 SGP_NOALLOC 却返回 -ENOENT,调用方(khugepaged、userfaultfd)需要特殊处理。该系列的核心思路是两阶段优化:先消除不必要的锁操作,再在解耦后的读路径上实现批量 folio 获取。
阶段一:引入 SGP_GET 无锁模式(Patch 1-2)。新增 sgp_type SGP_GET,它与 SGP_READ 的区别在于跳过 folio_lock() 和 mapping 一致性检查,仅依赖 refcount 和 Uptodate 标志保障安全。关键代码路径在 shmem_get_folio_gfp() 中:当 folio 存在且 sgp == SGP_GET 时,检查 folio_test_uptodate() 后直接跳转到 out 标签返回,不执行锁操作。作者明确说明这一设计的约束:"调用方不得依赖 folio->mapping 的有效性,因为它在并发 truncate 下可能已经失效"。然后 Patch 2 将三个读函数中的 SGP_READ 替换为 SGP_GET,并移除后续的 folio_unlock() 调用。
阶段二:实现 folio 批量化读取(Patch 3)。引入两个新的辅助函数。shmem_get_read_batch() 利用 XA_STATE 和 xas_for_each() 在 RCU 读锁保护下遍历 page cache xarray,收集连续的已 uptodate folio 放入 struct folio_batch。关键细节:遇到 xa_is_value()(swap entry)时停止批量化;通过 folio_try_get() + xas_reload() 双重确认避免竞态;只收集已 uptodate 的 folio,保证批内的所有 folio 立即可用。shmem_get_folio_from_batch() 则作为统一接口:先尝试从已有的 fbatch 中获取 folio,若批已耗尽则调用 shmem_get_read_batch() 重新填充,只有当批填充也失败时才回退到单次 shmem_get_folio()。在 shmem_file_read_iter() 中,folio_put() 被推迟——单个 folio 不再在每次迭代结束时释放,而是在整个读循环结束后对 fbatch 做统一释放。
阶段三:统一 hole 语义(Patch 4)。将 SGP_NOALLOC 改为在空洞时同样返回 0 和 NULL folio,与 SGP_READ/SGP_GET 一致。shmem_get_folio_gfp() 中的条件从 if (sgp == SGP_READ || sgp == SGP_GET) 简化为 if (sgp <= SGP_NOALLOC),利用枚举值顺序消除分支。
测试环境:fio,配置 --ioengine=sync --rw=read --size=1G --runtime=120。
| +12% | ||||
| +11% | ||||
| +22% | ||||
| +19% | ||||
| +43% | ||||
| +38% |
系列:[PATCH RFC 0/8] mm/slab: enable runtime sheaves tuning作者: Harry Yoo版本: RFC v1(8个patch)
Sheaf 机制于 v6.18 引入内核,自 v7.0 起对所有 slab cache(除 kmem_cache 和 kmem_cache_node 外)默认启用。Sheaf 取代了原先的 cpu_partial 机制,将 per-CPU 缓存对象组织为固定容量的 "束"(sheaf),通过 cpu 本地锁保护的无锁快速路径进行分配和释放,大幅提升了 slab 分配器的 per-CPU 缓存效率。然而,sheaf 的容量(sheaf_capacity)在缓存创建时确定后不可更改,且管理 barn(全局空/满 sheaf 池)的 MAX_FULL_SHEAVES 和 MAX_EMPTY_SHEAVES 两个阈值为硬编码常量 10,用户无法根据工作负载特征调优。作者的明确目标是"在下一个 LTS 之前使 sheaf 可以运行时调优"。
cpu_partial 参数可供调节每 CPU 缓存的对象数量,sheaf 没有等价机制,"sheaf 容量在内核代码中确定且之后无法更改"。struct kmem_cache *cache 指针(32字节占一个指针位),且容量使用 unsigned int(4字节),对高频分配释放的每个 sheaf 都存在空间浪费。该系列从数据结构精简入手,逐步构建运行时容量切换的安全机制,最终暴露 sysfs 接口。
数据结构精简(Patch 1-3)。首先从 struct slab_sheaf 中删除 cache 指针——它仅在 RCU 释放慢路径中使用,通过 virt_to_slab(sheaf->objects[0])->slab_cache 即可获取,无需在每个 sheaf 中存储。然后将 capacity 从 unsigned int 改为 unsigned short(2字节),因为单个 sheaf 容量不可能超过约 2^15 个对象(受 slab order 限制)。最后将 capacity 字段提升为 per-sheaf 通用字段(而非仅 prefilled sheaf 使用),并消除与 pfmemalloc 的 union 共享。最终 struct slab_sheaf 从 32 字节缩减到 24 字节(不含变长 objects[] 数组)。
运行时容量切换的安全框架(Patch 4-6)。在 struct slub_percpu_sheaves 中新增 capacity 字段,实现 per-CPU 容量副本。切换流程为:(1) 禁用 sheaf——对所有在线 CPU,在 local_lock 保护下将 pcs->main 替换为 bootstrap_sheaf;(2) 等待 RCU 回调完成——rcu_barrier() 确保所有飞行中的 RCU sheaf 已返回 barn;(3) 收缩缓存——__kmem_cache_do_shrink() 刷新并释放所有现有 sheaf;(4) 重新启用——为每个 CPU 预分配新容量的 sheaf 并通过 workqueue 安装。
解决并发竞态的核心机制是 per-CPU 容量副本 + local_lock 生涯规则。三条新规则:(a) 存取 barn 时必须持有 local_lock;(b) 分配新 sheaf 后在重新获取 local_lock 后检查 pcs->capacity == sheaf->capacity;(c) 若 local_trylock 失败则刷新并释放 sheaf。由此保证"过程中不留下任何具有过期容量的 sheaf"。
cache_has_sheaves() 重构(Patch 5)。拆分为 cache_supports_sheaves()(基于不可变属性)和 pcs_has_sheaves()(检查 CPU 是否实际启用了 sheaf),解决了两个不同问题的混淆。
sysfs 接口暴露(Patch 6 & 8)。sheaf_capacity 从只读改为可写,写入新值时执行上述四步切换流程,使用 slab_mutex 串行化并发写入。max_full_sheaves 和 max_empty_sheaves 作为新的 per-cache 字段和 sysfs 属性直接可写。默认值保持为 10,保持向后兼容。
作者未提供性能数据。封面信中明确说明:"这些可调参数的性能影响测量待完成"。从代码逻辑推断的预期收益:
sheaf_capacity 运行时可调允许管理员在吞吐量与内存占用之间取得平衡;max_{full,empty}_sheaves 可调允许控制全局 barn 池的大小和周转行为。pcs_capacity_match() 检查在快速路径中仅比较两个 unsigned short,开销可忽略。系列:[PATCHSET v2 sched_ext/for-7.2] bpf/arena: Direct kernel-side access作者: Tejun Heo, Kumar Kartikeya Dwivedi版本: v2(8个patch)
BPF arena 是一种允许 BPF 程序使用 __arena 指针直接解引用的大块内存映射区域,其页表是稀疏填充的——只有 BPF 程序通过 bpf_arena_alloc_pages() 分配后的地址才有真实物理页,未分配的 PTE 保持为空,访问会触发缺页异常。在此之前,内核代码无法安全地写入 arena 内存:内核侧的 arena 解引用如果命中空 PTE 会直接触发 Oops,导致内核崩溃。这迫使内核与 BPF 之间通过 arena 共享数据时采用笨拙的方案:内核将数据放在自己的内存中,以 "trusted pointer" 形式传递给 BPF 程序,BPF 程序通过 probe_read 逐字拷贝到 arena 内存中。sched_ext 的 ops.set_cmask() 正是这样一个典型案例——内核构造好 cpumask 后,BPF 侧必须通过 cmask_copy_from_kernel() 逐字节探测读取,效率低下且代码臃肿。
cmask_copy_from_kernel() 探测读取循环)该系列的核心思想是为每个 arena 分配一个 scratch page(单页,per-arena 共享),当内核代码访问 arena 内未分配的地址时,通过架构层面的缺页处理 hook 原子地为空 PTE 安装 scratch page,使访问指令能够重试执行,同时将违规访问通过 BPF stream 上报给程序。
具体实现分为三个层次:
第一层:无锁 PTE 安装基础设施。 Patch 1 引入 ptep_try_install(ptep, new_pte),原子地将一个空的 PTE 设置为新值当且仅当 PTE 当前为 pte_none()。x86 和 arm64 使用 try_cmpxchg 实现覆盖版本,其他架构返回 false(后续流程回退到正常 Oops)。这种无锁设计至关重要——缺页处理可能与缺页调用者已持有的锁冲突。
第二层:内核缺页恢复路径。 Patch 2 实现了核心函数 bpf_arena_handle_page_fault(),分别在 x86 的 page_fault_oops() 和 arm64 的 __do_kernel_fault() 中、KFENCE 处理之后被调用。该函数通过 bpf_prog_find_from_stack() 从内核栈回溯当前运行的 BPF 程序及其 arena,然后检查故障地址是否落在 arena 映射区 + 上半 guard(GUARD_SZ / 2)范围内。如果命中,则将 scratch page 安装到目标 PTE。违规访问通过 __bpf_prog_report_arena_violation() 上报给程序的 bpf_stream。用户侧对 scratched 地址的访问会触发 SIGSEGV。
第三层:sched_ext 集成。 Patches 4-5 添加了 bpf_struct_ops_for_each_prog() 和 bpf_prog_arena() 两个辅助函数,供 struct_ops 注册回调发现 BPF 程序引用的 arena map。Patch 6 强制要求 cid-form 调度器必须引用恰好一个 arena。Patch 7 构建了一个基于 gen_pool 的子分配器,让内核在 arena 内部管理自己的内存。Patch 8 将 set_cmask 转换为直接写入 arena 内存。
作者将这种 kfunc 合约总结为:"因此,被传递了 arena 指针的 kfunc 可以访问超出 arena 边界最多 GUARD_SZ / 2 的范围而无需进行边界检查。更大的访问必须显式验证范围。"
作者未提供性能数据。从代码逻辑推断的预期收益:
cmask_copy_from_kernel() 的逐字节探测读取开销系列:[PATCH v6 00/14] Remove CONFIG_READ_ONLY_THP_FOR_FS and enable file THP for writable files作者: Zi Yan版本: v6(14个patch)
CONFIG_READ_ONLY_THP_FOR_FS 是早期为不支持大页(large folio)的文件系统提供文件级透明大页支持的配置选项。它允许 khugepaged 和 MADV_COLLAPSE 在只读文件上创建 PMD 级别的文件映射大页。然而该机制与原生支持 large folio 的文件系统走的是两套不同路径:前者的文件 THP 通过 mapping->nr_thps 计数器和 inode->i_writecount 保护,在文件变为可写时截断所有只读 THP;后者由文件系统自身的 page fault 路径分配 large folio,但 khugepaged 和 MADV_COLLAPSE 却无法对这些大型页文件系统创建的文件 THP 进行collapse。这导致了能力分布不均的割裂局面:
READ_ONLY_THP_FOR_FS 能创建只读文件 THP,而本身支持 large folio 的文件系统反而不支持 khugepaged/MADV_COLLAPSE collapse,造成语义混乱nr_thps 跟踪 vs. mapping folio order 检查),代码复杂度高truncate_inode_partial_folio() 中必须通过 try_folio_split_to_order() 处理 "非统一 split" 的边界情况该系列采用渐进式策略:先启用功能保证不退化,再删除配置选项,最后扩展到可写文件。
统一 THP 判定逻辑。 Patch 1 用新引入的 mapping_pmd_folio_support(mapping) 替换 collapse_file() 中的 CONFIG_READ_ONLY_THP_FOR_FS 检查。新函数检查 mapping_min_folio_order() <= PMD_ORDER && mapping_max_folio_order() >= PMD_ORDER,确保该文件系统的 large folio 支持范围覆盖 PMD 阶。Patch 2 在 collapse_file() 中添加脏页检查——在 folio 锁定且已从所有 PTE 卸载后,再次检查 folio_test_dirty(),跳过脏 folio。
删除配置并清理。 Patch 5 正式删除 CONFIG_READ_ONLY_THP_FOR_FS Kconfig。Patches 6-7 删除 filemap_nr_thps*() 函数族和 struct address_space 中的 nr_thps 字段。Patch 9 将 truncate_inode_partial_folio() 中的 try_folio_split_to_order() 替换为直接调用 folio_split(),因为 READ_ONLY_THP_FOR_FS 删除后所有大 pagecache folio 必然出现在支持 large folio 的文件系统上。
扩展到可写文件。 Patch 13 是关键扩展:从 file_thp_enabled() 中删除 inode_is_open_for_write() 检查,使 khugepaged 和 MADV_COLLAPSE 可以对可写文件操作。collapse_file() 中的 filemap_flush() 调用被条件化为仅对只读文件执行——避免对可写文件的脏页进行系统级 writeback。对于可写文件,用户空间必须显式刷回脏数据,或使用 MADV_COLLAPSE,后者通过其 writeback-and-retry 路径自动处理 flush。
删除后的最终兼容矩阵:
*可写文件仅 clean folio 能被 khugepaged collapse。
作者未提供性能数据。从代码逻辑推断的预期收益:
nr_thps 机制和 try_folio_split_to_order() 等专用函数被移除MADV_COLLAPSE 和 khugepaged 的 large folio 文件系统现在获得了完整的文件 THP collapse支持,可有效减少文件映射的 TLB miss系列:[RFC PATCH v3 00/28] mm/damon: introduce data attributes monitoring作者: SeongJae Park版本: RFC v3(28个patch)
DAMON(Data Access MONitor)最初设计为只监控内存访问模式(冷/热分布),并被扩展到基于访问信息的数据访问感知系统操作(DAMOS)。然而实际用户需要更全面的内存视图,例如:想了解 DAMON 发现的热区域中有多少是由大页(huge page)后盾的,或者特定 cgroup 中有多少热/冷内存。为此,社区在 6.14 内核中合入了基于 DAMOS 的页级别属性监控——通过对 DAMON 区域内每个 folio 应用 DAMOS 过滤器来确定属性归属。但这种方法以页粒度遍历,开销与内存大小成正比,作者指出:"它显然太重了,无法在所有运行真实用户工作负载的机器上持续启用。" 用户被迫采用采样控制(如每小时仅在一台机器上运行一次),而这对于忙碌的运维团队来说实现成本太高。
核心设计理念是复用 DAMON 已有的采样机制来实现零额外采样开销的数据属性监控。DAMON 每次采样都是随机选取区域内某个物理地址检查其访问位,本系列在此采样点上同时应用用户注册的 "data probe"。
新的核心数据结构。 Patch 1 引入 struct damon_probe,初始只是一个链表节点,后续扩展为包含 damon_filter 过滤器链表的数据属性探针。Patch 2 在 damon_ctx 中嵌入 probes 链表头,提供 damon_new_probe()、damon_add_probe() 等管理 API。Patch 3 引入 struct damon_filter,其结构与 DAMOS filter 类似,支持按类型(匿名页、大页、memcg 等)和匹配函数过滤。
区域级别计数器。 Patch 5 在 damon_region 中新增 probe_hits 数组——每个区域维护每个 probe 的正采样计数器,与 nr_accesses 并行更新和重置。Patch 6 在 struct damon_operations 中新增 apply_probes 回调。Patch 7 是核心调度变更:在 kdamond_fn() 的每个采样周期中,当进行访问检查时同步调用 ops->apply_probes(ctx),对每个采样地址的内存应用所有已注册的 probes。Patch 8 在 paddr ops 中实现了完整的 probe 应用逻辑。
用户接口。 Patches 9-18 实现了完整的 sysfs 接口:在 /sys/kernel/mm/damon/admin/kdamonds/<N>/contexts/<N>/ 下新增 monitoring_attrs/probes/ 目录,每个 probe 目录下包含 filters/ 目录用于配置探测条件,并在 tried_regions/<R>/probes/ 下通过 hits 文件暴露每个区域的 probe 命中数。
Memcg 支持。 Patches 22-28 新增 DAMON_FILTER_TYPE_MEMCG:该 filter 匹配属于特定 memory cgroup 的 folio。用户通过 sysfs 的 path 文件设置目标 cgroup 路径,系统将其解析为 memcg_id。这使得用户可以了解热/冷内存在各 cgroup 间的分布比例。
在长期规划中,作者设想 DAMON 将演化为 "Data Attributes Monitoring and Operations eNgine"——用户可以将任何数据属性(page fault 确认的访问、PMU 事件确认的访问)设为 "primary" 属性,DAMON 据此拆分/合并区域,DAMOS 据此选择操作目标。
作者未提供性能数据(RFC 阶段)。从设计原则推断的预期收益:
系列:[RFC PATCH 0/7] mm/damon: hardware-sampled access reports + AMD IBS Op example作者: Ravi Jonnalagadda版本: RFC v1(7个patch)
DAMON 现有的访问检测原语(PTE Accessed-bit 扫描和缺页采样)全部通过软件路径观测内存访问:PTE A-bit 扫描依赖 TLB 缺失时硬件设置 Accessed 位后 DAMON 周期性清空并读取,对 TLB 驻留的翻译会产生"盲区";缺页采样则需要先解映射(unmap)页面才能触发缺页。这两种原语产生的热页视图需要在一个聚合间隔(aggregation interval)内逐渐收敛到真实分布。在大规模异构内存系统上,当闭环比率控制器(goal-driven schemes)需要快速收敛到目标分布时,这种收敛延迟会导致"ramp duration 和 oscillation amplitude"增大。如作者所言:"一个互补的低延迟访问视图可以收紧控制环路——减少 DAMON 的 nr_accesses 反映工作负载实际访问分布所需的时间"。
damon_report_access() 原有的 mutex 保护固定数组无法从 NMI 上下文中调用,无法接收硬件采样器的报告基础设施改造(Patch 1/3/4):
Patch 1 在 struct damon_operations 中加入 struct module *owner 字段,通过 try_module_get()/module_put() 确保模块卸载与 ctx 安全解耦。
Patch 3 将原来的 mutex 保护固定大小数组(DAMON_ACCESS_REPORTS_CAP=1000)替换为每 CPU 无锁环形缓冲区。设计要点:每个 CPU 的生产者通过 per-CPU damon_report_ring_busy 计数器检测 NMI-on-process 嵌套;head 由生产者通过 smp_wmb() 后推进;tail 由消费者(kdamond)读取 entries[] 后推进;head/tail/entries[] 分别置于独立 cache line(____cacheline_aligned_in_smp)。
Patch 4 将 drain 循环从 O(reports x regions) 线性扫描优化为 O(reports x log2(regions))。drain 开始时将每个 target 的 region 数组快照到扁平数组,region 天然按 r->ar.start 升序排列,使用二分查找定位 ring entry 对应的 region。
IBS 后端(Patch 2/5/6/7):
Patch 7 是核心的 AMD IBS 后端(mm/damon/damon_ibs.c)。利用 perf_event_create_kernel_counter() 创建 per-CPU IBS Op 采样事件,通过 cpuhp notifier 管理 CPU 热插拔。采样回调 damon_ibs_overflow_handler() 在 NMI 上下文运行:通过检测 data->sample_flags & PERF_SAMPLE_PHYS_ADDR 判断 IBS_OP_DATA3.dc_phy_addr_valid;从 data->data_src.mem_op 解码 load/store 方向;填充 struct damon_access_report 后调用 damon_report_access() 提交。模块参数 max_cnt 可写(默认值 262144,约 4K samples/sec/core)。
作者在 AMD EPYC 双路服务器、CXL.mem 独立 NUMA 节点、32GB 热工作集的配置上进行了闭环验证:
所有场景在 15-30 分钟持续运行中,实测 DRAM 占比稳定在目标值的 1.3 个百分点以内,标准差不超过 1.3 个百分点。配置参数:256-entry per-CPU report ring、512 MiB per-scheme quota、1 秒 reset interval。
系列:[PATCH] shmem: support xattr gnu.* namespace for the Hurd作者: Janneke Nieuwenhuizen版本: v1(1个patch)
GNU Hurd 正在将 translator 和 author 字段从 inode 内移出并放到 "gnu.*" xattr 命名空间中存储。Linux 内核为此预留了 xattr INDEX(XATTR_HURD_PREFIX,commit 3980bd3b406a),Hurd 自身已完成合规改造,ext4 也已支持(commit 88ee9d571b6d)。但 tmpfs(shmem)尚未实现该命名空间支持。GNU Guix 使用 tmpfs 创建 Hurd 初始文件系统镜像时需要用 setfattr/getfattr 设置管道 translator 等属性,作者描述:"现在可以做类似...的事情来设置管道 translator,这正被用于从 GNU Guix 创建 Hurd 的初始文件系统镜像"。
gnu.translator 等 gnu.* 扩展属性,阻碍了 Hurd 从 Linux tmpfs 上引导的用例gnu.* xattr 支持,而 tmpfs 是该生态系统中的缺口在 mm/shmem.c 中新增一个 shmem_hurd_xattr_handler,其 prefix 设为 XATTR_HURD_PREFIX(即 "gnu."),get 和 set 复用已有的通用 shmem xattr 处理函数 shmem_xattr_handler_get 和 shmem_xattr_handler_set。将该 handler 注册到 shmem 的 xattr handler 数组 shmem_xattr_handlers[] 中,排在 security、trusted、user 之后。这是对现有 shmem xattr 机制的纯增量扩展——shmem 本身已有一套基于 simple_xattrs 的完整 xattr 存储、读取、写入路径,新增 gnu.* 命名空间只需要注册一个 handler 告诉 VFS:"我可以处理此前缀的 xattr"。
作者未提供性能数据。这是一个纯功能启用(feature enablement)patch,不涉及性能路径变化。收益在于:Linux tmpfs 现在可以作为 Hurd 启动过程中的文件系统镜像创建工具,支持 setfattr --name=gnu.translator 等操作,消除了 Hurd/GNU Guix 从 Linux 交叉构建的生态系统障碍。
系列:[PATCH v4 0/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev作者: Alban Crequy版本: v4(2个patch,含 selftest)
process_vm_readv(2) / process_vm_writev(2) 有两类用户。第一类是调试器(如 GDB、strace),它们需要精确解析目标内存内容,page fault 必须被解决,且通常附着于单个进程。第二类是 profiler(如 OpenTelemetry eBPF Profiler),以 20Hz 频率从所有进程中采样栈回溯,对解释型语言(Ruby、Python 等)需要用 process_vm_readv 获取符号信息。在此场景下,"性能是最重要的。只要在统计上不显著,部分栈回溯无法解析是可以接受的。" profiler 不能忍受因某一个目标进程的后端文件系统慢速 IO 而阻塞所有其他目标进程的监控。此外,profiler 场景中 PID 重用可能导致读取到错误进程的内存。
process_vm_readv 无条件等待 page fault 解决,不适合 profiler 的低延迟需求flags != 0 直接返回 EINVAL)新增两个标志定义于 include/uapi/linux/process_vm_access.h:
PROCESS_VM_PIDFD (1UL << 0):使用 pidfd 替代 PID 引用目标进程PROCESS_VM_NOWAIT (1UL << 1):不阻塞等待 IO 完成 page fault核心实现位于 mm/process_vm_access.c:
Flag 校验与路由。 引入内核内部宏 PROCESS_VM_SUPPORTED_FLAGS,process_vm_rw() 中将 flags & ~PROCESS_VM_SUPPORTED_FLAGS 替换原来的 flags != 0 检查,既保持向后兼容又允许用户态探测 flag 支持。
PIDFD 路径。 在 process_vm_rw_core() 中,当 flags & PROCESS_VM_PIDFD 时,调用 pidfd_get_task(pid, &f_flags) 获取任务引用(与 process_madvise() 风格一致)。如果 pidfd 无效,返回 EBADF。
NOWAIT 路径。 在 process_vm_rw_single_vec() 中,当 pvm_flags & PROCESS_VM_NOWAIT 时设置 FOLL_NOWAIT 标志传递给 GUP。对于非驻留页面,返回短读(已成功读取的字节数)或 EFAULT(一个字节都没读到),语义与 O_NONBLOCK 惯例一致。
selftest。 新增 process_vm_readv 自测文件,覆盖:基本读、无效 flag、pidfd、NOWAIT、userfaultfd 阻塞/非阻塞、部分读等 10+ 场景。所有新 flag 测试在旧内核上通过 EINVAL 优雅 SKIP。
作者未提供性能数据。从代码逻辑推断的预期收益:
PROCESS_VM_NOWAIT 避免了因后端文件系统慢速 IO 导致的 page fault 阻塞,保证 profiler 对多个目标进程的采样延迟不会因某个进程的单次 IO 而被放大PROCESS_VM_PIDFD 消除了 PID 重用竞争process_vm_readv(pid, NULL, 1, NULL, 1, PROCESS_VM_PIDFD) 检测内核是否支持新 flag(返回 EINVAL = 不支持,返回 EFAULT = 支持)系列:[PATCH v7 0/4] mm/page_owner: add per-fd filter infrastructure for print_mode and NUMA filtering作者: Zhen Ni版本: v7(4个patch)
在生产环境中,大内存配置(250GB+)下收集 page_owner 信息经常产生数 GB 到 10GB+ 的输出文件。作者指出:"在大内存配置(例如 250GB+)的生产环境中,收集 page_owner 信息经常产生数 GB 到超过 10GB 的输出文件。" 主要问题有三个方面:(1)存储压力——生产系统上生成的大文件难以存放;(2)传输困难——从生产环境拷贝大文件代价高昂;(3)后处理开销——tools/mm/page_owner_sort.c 需要对已在内核中通过 stackdepot 做过栈去重的数据再次去重。此外,在 DPDK 等 NUMA 绑定的云部署中,OOM 事件往往是节点级别的,但 page_owner 无法按 NUMA 节点过滤,只能收集全量数据。
page_owner 输出文件过大(可超 10GB),导致存储、传输、后处理三项开销核心设计是 per-file-descriptor 过滤状态:每个 open() 持有独立的 struct page_owner_filter_state(存储于 file->private_data),通过 debugfs_create_file("page_owner", 0600, ...) 新增的 .write 文件操作接收过滤命令。
print_mode 过滤器(Patch 1)。 定义三种模式枚举 PAGE_OWNER_PRINT_{STACK,HANDLE,STACK_HANDLE},对应字符串 "stack"、"handle"、"stack_handle"。默认模式为 PAGE_OWNER_PRINT_STACK(向后兼容)。.write 解析 "mode=xxx" 命令(使用 sysfs_match_string() 匹配)。print_page_owner() 函数根据 state->print_mode 选择性地调用 stack_depot_snprint() 和 scnprintf("handle: %d\n", handle)。
NUMA 节点过滤器(Patch 2)。 在 struct page_owner_filter_state 中加入 nodemask_t nid_filter 和 bool nid_filter_enabled。.write 解析 "nid=..." 命令,使用 nodelist_parse() 支持灵活格式(单节点 0、多节点 0,2,3、范围 0-3、混合 0,2-4,7)。通过 nodes_subset(new_nid_filter, node_states[N_MEMORY]) 校验节点有效性。在 read_page_owner() 的 page 迭代循环中,通过 page_to_nid(page) 获取页面节点号后用 node_isset() 过滤。关键性能优化:nodemask_t(128 字节)在页面迭代循环外读取,避免每个 PFN 迭代都复制 128 字节结构体。
用户态工具(Patch 3)。 新增 tools/mm/page_owner_filter.c,提供命令行接口 ./page_owner_filter -m handle -n "0,2-3" -o output.txt。工具内建输入校验(模式合法性、NID 范围检查、start <= end),通过 write() 向 debugfs 文件发送过滤命令后 read() 输出到文件或 stdout。
文档(Patch 4)。 更新 Documentation/mm/page_owner.rst,加入过滤功能说明、使用示例和并发使用场景。
作者在 4 节点 NUMA 系统上的测试显示:
NUMA 过滤的正确性通过了 26 项测试验证,包括单节点、多节点、范围、混合格式、越界拒绝、范围起始大于结束拒绝、handle+NUMA 组合过滤等。per-fd 设计允许在不同终端中并发运行不同的过滤命令(例如终端 1 过滤 node 0,终端 2 过滤 node 1),无协调冲突。
| swap table phase IV | ||
| ZONE_DEVICE memmap 初始化加速 | ||
| shmem folio batching | ||
| slab runtime sheaves tuning |
| BPF Arena 内核侧访问 | ||
| 文件 THP 可写文件支持 | ||
| DAMON data attributes | ||
| DAMON 硬件采样 + AMD IBS | ||
| shmem gnu. xattr | ||
| process_vm_access pidfd+nowait | ||
| page_owner per-fd filter |