目录
- 收紧 mmap_miss 命中统计以防止错误的预读抑制
- 批量页面复制优化 folio_copy() 和 folio_mc_copy()
- 限制 filemap_fault 预读到 VMA 边界
1. 收紧 mmap_miss 命中统计以防止错误的预读抑制
系列:[PATCH v3 0/2] mm/filemap: tighten mmap_miss hit accounting作者: fujunjie版本: v3(2 patch)
背景
Linux 内核的文件预读(readahead)子系统维护了一个 per-file 的 file->f_ra.mmap_miss 计数器,用于 mmap 场景下的预读决策。当同步 mmap 预读被触发时(即发生 page cache miss),mmap_miss 递增;当 filemap_map_pages() 在 page cache 中找到已经存在的 folio(命中,hit)时,mmap_miss 递减。这个命中统计直接影响内核是否继续执行 mmap 预读——如果 mmap_miss 过高,预读会被抑制,因为内核认为频繁的 miss 意味着预读只是在浪费 IO。
解决的问题
- fault-around 过度计入命中:当缺页异常触发
filemap_map_pages() 时,fault-around 机制会尝试安装缺页地址附近最多 64 个 PTE(page table entry)。即使只有缺页地址本身被证明是热访问,周围的页面也被计为命中,导致 mmap_miss 被过度递减,掩盖了真实的访问稀疏性。 - VM_FAULT_RETRY 重试路径错误地抵消 miss:同步 mmap 预读因需要等待 IO 而返回
VM_FAULT_RETRY 并释放 mmap_lock。当重试进入 filemap_map_pages() 时,如果发现该 folio 已被先前同一次 miss 引入 page cache,会将其计为命中并递减 mmap_miss,从而"立即取消刚才发生的 miss",形成自欺欺人的统计。
如何做
方案的技术核心是两点精确的过滤条件,全部位于 filemap_map_pages() 中。
首先,patch 1 将 mmap_miss 的更新从 filemap_map_folio_range() 和 filemap_map_order0_folio() 这两个 helper 函数中移除,收回到 filemap_map_pages() 调用侧。Helper 函数不再接收 mmap_miss 参数,而是返回 map_ret(其中包含 VM_FAULT_NOPAGE 标志位)。在主循环中,仅当 map_ret & VM_FAULT_NOPAGE 成立时——即 helper 成功映射了实际缺页地址——才递减 mmap_miss:
if ((map_ret & VM_FAULT_NOPAGE) &&
!folio_test_workingset(folio)) {
unsigned short mmap_miss;
mmap_miss = READ_ONCE(file->f_ra.mmap_miss);
if (mmap_miss)
WRITE_ONCE(file->f_ra.mmap_miss, mmap_miss - 1);
}
这解决了第一个问题:fault-around 安装的周边 PTE 不再计入命中,只有实际缺页地址所在 folio 被成功映射时才被计为命中。
其次,patch 2 在上述条件中加入 !(vmf->flags & FAULT_FLAG_TRIED) 检查。这个标志位表示当前缺页处理是 VM_FAULT_RETRY 之后的重试。作者明确指出,重试路径映射的 folio 正是由同一次 miss 触发的预读带入的,不应计为"有用的 mmap 预读命中"("does not count as a useful mmap readahead hit")。加入此过滤后,重试路径的伪命中不再递减 mmap_miss。
收益
测试环境:8 GiB KVM 虚拟机,2 vCPU,read_ahead_kb=8192(ra_pages=2048),每次运行前清空 page cache,访问文件 1% 的数据,中位数取自 3 次运行。
20 GiB 文件(大于内存)大文件场景:
4 GiB 文件(可完全放入内存)场景:
消融实验(20 GiB random 场景):
random 和 stride2053/stride4099 模式取得了 20 倍到 200 倍的块 IO 减少,wall-clock 时间减少 96% 以上。消融实验揭示:P1 和 P2 必须同时存在才能生效——两个漏洞各自只会造成一部分错误统计,单独修任何一个都不足以恢复正确的预读行为。
2. 通过批量复制和 DMA 硬件卸载加速页面迁移
系列:[RFC v5 0/7] Accelerate page migration with batch copying and hardware offload作者: Shivank Garg,Mike Day, Zi Yan版本: v5 RFC(7 patch)
背景
页面迁移(page migration)是 Linux 内存管理的关键操作,用于 NUMA 平衡、内存压缩(compaction)、内存热移除、分层内存(tiered memory,如 CXL.mem)的数据放置等场景。现有的迁移路径对每个 folio 逐个进行处理:unmap → TLB flush → copy → move。其中 copy 阶段是 CPU 单线程逐 folio 执行的,对于大 folio(如 2 MB 透明大页,THP),数据复制开销占据主导地位。"在现代具有深层内存层次的系统中,单线程逐 folio 复制成为页面迁移的瓶颈"("Single-threaded, folio-by-folio copying bottlenecks page migration in modern systems with deep memory hierarchies")。
解决的问题
- 大 folio 迁移时,CPU 逐个复制成为吞吐量瓶颈
- DMA 引擎(如 AMD PTDMA)等硬件加速能力无法在迁移路径中被利用
- 迁移路径缺乏可插拔的加速器框架,无法适配不同的硬件卸载方案(DMA、SDXI、多线程 CPU copy 等)
如何做
本系列构建了一个完整的批量复制框架,分为三个层次:
第一层:基础设施清理(patch 1-3)。 Patch 1 将 PAGE_WAS_* 迁移状态标志重命名为 FOLIO_WAS_*。Patch 2 在 struct folio 的 union 中新增 unsigned long migrate_info 字段,替代原来使用 folio->private 存储迁移状态的做法。Patch 3 新增 FOLIO_ALREADY_COPIED 标志位:当该标志被设置时,__migrate_folio() 跳过 folio_mc_copy() 数据复制,仅执行元数据更新。
第二层:批量复制路径(patch 4)。 在 migrate_pages_batch() 中将原来单一的 unmap_folios/dst_folios 链表拆分为 unmap_batch/dst_batch(批量复制路径)和 unmap_single/dst_single(标准逐 folio 路径)。分配逻辑由一个默认关闭的 static branch migrate_offload_enabled 控制。TLB flush 之后,通过 folios_mc_copy() 批量复制,然后传 already_copied=true 进入 migrate_folios_move() 完成 PTE 更新。如果批量复制失败,自动回退到独立的 CPU 复制。
第三层:可插拔加速器框架(patch 5-7)。 Patch 5 新增 struct migrator,包含 name、offload_copy 回调、owner 模块指针。注册接口 migrate_offload_register() 通过 static_call 机制修补调用目标并翻转 migrate_offload_enabled static branch。Patch 6-7 实现了 DCBM(DMA Core Batch Migrator)参考驱动,通过 dma_request_chan_by_mask(DMA_MEMCPY) 获取 DMA 通道,将批量中的 src/dst folio 组装为 scatterlist,提交 dmaengine_prep_dma_memcpy() 异步执行。通过 sysfs 属性可动态控制和观测 DMA 卸载行为。
收益
2 MB folio,16 DMA 通道,AMD EPYC:
除了吞吐量提升,DMA 卸载还释放了原本消耗在 memcpy 上的 CPU 周期,对多租户场景尤为有利。
3. 批量页面复制优化 folio_copy() 和 folio_mc_copy()
系列:[RFC 0/1] batch page copies in folio_copy() and folio_mc_copy()作者: Shivank Garg版本: RFC(1 patch)
背景
folio_copy() 和 folio_mc_copy() 是内核中复制 folio 内容的两个核心函数,广泛用于页面迁移、KSM(Kernel Same-page Merging)、写时复制(COW)等路径。现有实现以 4 KiB 页面为单位循环:对每个子页面调用 copy_highpage()(或 copy_mc_highpage()),并在迭代间调用 cond_resched()。对于一个 2 MB folio,这个循环执行 512 次,每次迭代承担 kmap_local_page()/kunmap_local()、cond_resched() 以及体系结构 memcpy 原语的调用开销。
解决的问题
- 逐页循环产生 512 次函数调用开销(对 2 MB folio),包括 kmap/kunmap、cond_resched 和 per-page 原语调用
- 每次
copy_page()/copy_mc_to_kernel() 调用需要独立的 setup 开销,无法摊销 - 在 cache-hot 场景下,
cond_resched() 开销占比尤为显著
如何做
Patch 新增了 copy_highpages() 和 copy_mc_highpages() 两个批量 helper。对现代 64 位路径(!__HAVE_ARCH_COPY_HIGHPAGE && !CONFIG_HIGHMEM),直接对整个连续物理范围做一次 memcpy() 或 copy_mc_to_kernel():
staticinlinevoidcopy_highpages(struct page *to, struct page *from,
unsignedlong nr_pages)
{
#ifndef __HAVE_ARCH_COPY_HIGHPAGE
if (!IS_ENABLED(CONFIG_HIGHMEM)) {
memcpy(page_address(to), page_address(from),
nr_pages << PAGE_SHIFT);
for (unsignedlong i = 0; i < nr_pages; i++)
kmsan_copy_page_meta(to + i, from + i);
return;
}
#endif
for (unsignedlong i = 0; i < nr_pages; i++)
copy_highpage(to + i, from + i);
}
copy_mc_highpages() 同理,成功时批量调用 kmsan_copy_page_meta(),失败时定位出错的第一个页面并调用 memory_failure_queue() 返回 -EHWPOISON。folio_copy() 和 folio_mc_copy() 被改造成这两个 helper 的薄封装,各自从 ~20 行的循环缩减为一行调用。
收益
测试环境:双路 AMD EPYC 9655(Zen 5),三 NUMA 节点(DRAM node 0/1,CXL.mem node 2)。
Cache-cold microbenchmark(folio_mc_copy,2 MB folio):
Cache-hot microbenchmark(folio_copy,2 MB):
端到端 move_pages(2)(匿名 mTHP,1 GiB/run):
端到端加速比低于微基准测试,因为 move_pages(2) 系统调用还包含 rmap walk、TLB shootdown、目标 folio 分配、PTE 重写等开销,"系统调用的固定开销限制了加速比"("the syscall floor work caps the speedup")。
Zen 3 上的性能回归(AMD EPYC 7713): cache-hot 场景 0.60x 回归,因 copy_page() 使用微码 rep movsq 而批量 memcpy() 在无 FSRM/ERMS 的 Zen 3 上回退到较慢的 memcpy_orig。作者提出需引入 copy_pages() x86 辅助函数来统一原语选择。copy_mc_highpages() 无此问题。
4. 限制 filemap_fault 预读到 VMA 边界
系列:[PATCH v2] mm: limit filemap_fault readahead to VMA boundaries作者: Frederick Mayle版本: v2(1 patch)
背景
Linux 文件映射(file mapping)的预读机制在缺页中断(page fault)路径中会预测性地读取页面到页缓存(page cache)。然而,当一个文件映射只覆盖文件的严格子集时(例如 ELF 可执行文件的只读段),对映射末尾的访问触发的预读可能会读取到映射区域之外的文件页面。作者指出:"An access to the end of a program's read-only segment isn't a sign that nearby file contents will be accessed next (they are likely to be mapped discontiguously, or not at all)." 这些被错误预读进来的页面不仅不会被用到,还会挤占页缓存空间。
解决的问题
- 文件映射的 VMA 子集场景(如 ELF 加载)中,预读将 VMA 之外的文件页面拉入页缓存,造成内存浪费
- 在 Android 环境下,某些 ELF 文件因此多缓存了约 20% 的页面
如何做
该 patch 在 struct readahead_control 中新增 _max_index 字段作为预读硬上限。与已有的 file_ra_state->size 不同(后者是"提示"值,可被启发式算法增大),_max_index 提供不可逾越的边界。
具体实现包括三处改动:
在 DEFINE_READAHEAD 宏中默认初始化为 ULONG_MAX(无限制),保持其他路径语义不变。
在 do_sync_mmap_readahead() 和 do_async_mmap_readahead() 中将 ractl._max_index 设置为 VMA 结束位置(vmf->vma->vm_pgoff + vma_pages(vmf->vma) - 1),同时在 "read-around" 路径中将预读起始位置限制为不低于 VMA 起始页。
在 do_page_cache_ra() 和 page_cache_ra_order() 中以 min(limit, ractl->_max_index) 截断越界预读请求。
此限制仅影响缺页中断触发的预读,不影响 read(2) 系统调用触发的预读。
收益
fio 基准测试(Android 设备):
在 Android 平台上观测到某些 ELF 文件加载时减少了约 20% 的页缓存填充。用 cachestat 系统调用验证了映射区域外的页面不再被预读。
5. 改进 MGLRU 回收循环与脏页处理
系列:[PATCH v7 00/15] mm/mglru: improve reclaim loop and dirty folio handling作者: Kairui Song,Barry Song版本: v7(15 patch)
背景
多代 LRU(Multi-Gen LRU,MGLRU)是 Linux 5.15 引入的页面回收算法,通过逐代老化(aging)和回收(eviction)机制替代经典 LRU 的单链表扫描。然而,随着在生产环境中的深入使用,其回收循环中的若干设计问题逐渐暴露:扫描数量计算与老化逻辑耦合过紧、脏页处理方式与经典 LRU 截然不同导致回写唤醒不及时、老化后立即终止扫描浪费回收周期,以及缺少写回 throttling 机制触发提前 OOM。
解决的问题
- 回收循环中扫描数量每次迭代重新计算,且与老化判断耦合,逻辑难以跟踪
- 脏页/回写页在隔离前就被特殊处理移到次老代,绕过了
shrink_folio_list 的精准判断,回收效率低下 - flusher 唤醒发生在整个回收循环结束后,响应迟钝,高回写压力场景下文件 refault 严重
- 老化触发后立即终止扫描,浪费回收周期,并发回收场景下最坏情况触发提前 OOM
- MGLRU 完全不参与写回 throttling,在慢速存储设备上直接 OOM
如何做
该系列从三个核心维度重构 MGLRU 回收循环:
1. 扫描预算(scan budget)解耦老化。 新增 get_nr_to_scan() 一次性计算总体扫描预算,扫描数量始终按 sc->priority 右移。should_run_aging() 不再承担计算 nr_to_scan 的副作用,只返回布尔判断。
2. 老化后不再立即终止扫描。 之前 try_to_inc_max_seq() 返回失败时循环直接 break;现在改为设置 should_age 标志后继续扫描。对于 cgroup 级回收完全不停;对于全局回收仅在一个 batch 后终止以保证公平性。
3. 统一脏页/回写处理与 throttling(最核心改动)。 删除 sort_folio() 中对脏页/回写页的特殊处理(移入次老代并跳过隔离),让它们正常进入 shrink_folio_list(),与经典 LRU 走相同的 reactivation 路径。好处包括:dirty/writeback 检查更精准、保留了 folio 的 referenced bits、消除了脏页频繁出现在 LRU 尾部的问题。
通过引入统一的 handle_reclaim_writeback() 辅助函数,MGLRU 与经典 LRU 共享相同的 dirty throttling 行为。dd 到慢速 dm-delay 设备的测试中,之前 MGLRU 立即 OOM,现在 throttling 正常工作。
其他改进: 每批扫描大小从 MAX_LRU_BATCH 降为 MIN_LRU_BATCH 减小锁持有时间;隔离无进展时不立即回退到另一种回收类型;scan_folios() 返回精确的扫描数和隔离数;移除不再使用的 sc->file_taken 和 sc->unqueued_dirty 字段。
收益
测试环境:48c96t NUMA 机器,2 节点,128G 内存,NVMe 存储。
MongoDB YCSB workloadb(10G cgroup,95% 读 5% 更新,无 swap):
Android 应用冷启动延迟:
慢速 IO 设备上某些工作负载性能提升超过 100%。"file-anon-mix-pressure" 压力测试中,之前 MGLRU 在 10-20 轮迭代后触发 OOM,应用此系列后完成全部 128 轮——OOM 不再发生。MySQL sysbench、FIO mmap randread、内核编译、Chrome & Node.js 等测试均为中性变化,无性能衰退。
总结
新机制 / 新接口
| | |
|---|
| DMA 批量卸载页面迁移 | 新增 struct migrator 可插拔加速器框架,DCBM DMA 驱动实现 6 倍迁移吞吐量提升 |
| folio_copy/mc_copy 批量优化 | 新增 copy_highpages()/copy_mc_highpages() helper,Zen 5 上 2.04x cache-cold 加速 |
性能优化
| | |
|---|
| 收紧 mmap_miss 命中统计 | random/stride 场景块 IO 从 ~200 GiB 降至 ~1 GiB,wall-clock 时间减少 96%+ |
| 限制预读到 VMA 边界 | Android ELF 页缓存填充减少 ~20%,fio mmap 顺序读 +1.7% |
| MGLRU 回收循环改进 | MongoDB +27.6% 吞吐量、-21.9% 延迟、-43.3% refault;Android P95 冷启动 -12.2%;修复慢速 IO 过早 OOM |
Bug Fix(本期未深入分析)
| | |
|---|
| mm/hugetlb: fix subpool accounting after cgroup charge failure [v2] | |
| mm: memcontrol: fix rcu unbalance [v2] | |
| mm/hugetlb: min_hpages unwind corrupts reservation [RFC] | |
| mm/hugetlb: fix max-only subpool accounting [v3] | |
| x86/mm: fix freeing of PMD-sized vmemmap pages | |
| mm: Fix memory block leaks and locking [v2, 3p] | |
| mm: Fix vmemmap optimization accounting [v8, 6p] | |
| mm/damon/core: make charge_addr_from aware of end-address exclusivity | |
| POE sigreturn fix and extra tests [v2, 5p] | |
| mm/madvise: reject invalid process_madvise() for zero-length [v2] | |
| Fix halted scanning of swap-cache folios [RFC] | |
| mm/migrate_device: fix pgtable leak [v2] | |
| mm/page_alloc,slab: return NULL early in NMI on UP [v2, 2p] | |
内部优化 / 清理(本期未深入分析)
| | |
|---|
| process_vm_access: pidfd and nowait support [v3, 2p] | |
| page_owner: filter infrastructure [v3, 4p] | |
| dma-contiguous: setup default numa cma [v3] | |
| DAMON: node_eligible_mem_bp goal metric [v9] | |
| mm: remove page_mapped() [3p] | |
| DAMON: pause and resume [v2, 10p] | |
| mempolicy: track user-defined allocations [v4] | |
| userfaultfd: working set tracking [14p] | |
| slab: optimistic __slab_try_return_freelist [RFC v2] | |
| page_alloc: trace PCP refills [v2] | |
| vmalloc: free unused pages on vrealloc() shrink [v12, 5p] | |