Linux pagefault吞吐量测量与major fault消除
pagefault吞吐的测量入口是perf事件子系统。`perf stat -e page-faults,minor-faults,major-faults` 最终落入 `kernel/events/core.c` 中 `PERF_COUNT_SW_PAGE_FAULTS` 的计数路径。perf_sw_ids枚举定义在 `include/uapi/linux/perf_event.h`:
```c
enum perf_sw_ids {
PERF_COUNT_SW_PAGE_FAULTS = 1,
PERF_COUNT_SW_PAGE_FAULTS_MIN = 8,
PERF_COUNT_SW_PAGE_FAULTS_MAJ = 9,
};
```
软事件通过 `perf_sw_event()` 在 `mm/memory.c` 的缺页路径中被触发。`mm_account_fault()` 是实际的记账函数,在 `handle_pte_fault()` 返回后被调用:
```c
static void mm_account_fault(struct pt_regs *regs,
unsigned long address, unsigned int flags,
vm_fault_t ret)
{
bool major;
major = (ret & VM_FAULT_MAJOR);
if (major)
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ,
1, regs, address);
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS,
1, regs, address);
}
```
`mm_account_fault()` 受 `CONFIG_PERF_EVENTS` 控制。在没有perf事件的嵌入式内核中,唯一的计数来源是 `/proc/vmstat` 中的 `pgfault` 和 `pgmajfault`,它们分别由 `count_vm_event(PGFAULT)` 在 `handle_mm_fault()` 入口处和 `count_vm_event(PGMAJFAULT)` 在 `__do_fault()` 路径中递增。吞吐量的瓶颈不在这些counter本身——它们只是cache line上的原子递增——而在于 `handle_mm_fault()` 内部的 `mmap_lock` 竞争。`down_read(&mm->mmap_lock)` 在 `handle_mm_fault()` 栈顶被持有,当高并发线程同时缺页时,这个读者锁本身会成为perf采样中的top entry。
tracepoint层面,`include/trace/events/filemap.h` 中 `DEFINE_EVENT(mm_filemap_fault)` 在 `filemap_fault()` 中通过 `trace_mm_filemap_fault` 发射,携带 `vmf->pgoff`、`vmf->flags` 和 `vmf->major`。配合 `perf record -e filemap:mm_filemap_fault` 可以精确测量每个major fault从进入 `filemap_fault()` 到返回的延迟分布。
major fault消除的第一目标是把 `filemap_fault()` 路径中的 `mapping->a_ops->readpage()` 调用消除。`filemap_fault()` 中 `find_get_page()` 的page cache miss触发 `page_cache_sync_readahead()`,其行为由 `vm_file->f_ra` 控制:
```c
vm_fault_t filemap_fault(struct vm_fault *vmf)
{
struct file *file = vmf->vma->vm_file;
struct address_space *mapping = file->f_mapping;
struct folio *folio;
pgoff_t index = vmf->pgoff;
unsigned long max_ra_pages;
folio = filemap_get_folio(mapping, index);
if (likely(!IS_ERR(folio)))
goto found;
max_ra_pages = min_t(unsigned long,
VM_READAHEAD_PAGES(vmf->vma),
ra->ra_pages);
page_cache_sync_readahead(mapping, ra, file, index,
max_ra_pages);
folio = filemap_get_folio(mapping, index);
if (IS_ERR(folio))
goto no_page;
...
}
```
`page_cache_sync_readahead()` 内部调用 `ondemand_readahead()`,后者根据 `ra->start` 和 `ra->size` 决定是顺序还是随机探测模式。一个被绝大部分开发者忽略的参数是 `ra->async_size`——它定义了触发点到窗口末尾的距离。对于顺序读负载,`async_size` 不应小于 `ra_pages >> 1`,否则readahead会被推迟到fault已经逼近窗口边界才触发,增加了major fault概率。调整入口是 `/sys/class/bdi//read_ahead_kb`,但真正的生效是在VMA层面:`VM_READAHEAD_PAGES(vmf->vma)` 决定单个VMA的readahead上限。
对已经处于page cache中的连续页面,`filemap_map_pages()` 比反复走 `filemap_fault()` 高效一个数量级。前者在 `do_fault_around()` 中被调用,通过 `filemap_get_read_batch()` 一次XArray walk获取多个folio并批量映射PTE:
```c
vm_fault_t filemap_map_pages(struct vm_fault *vmf,
pgoff_t start_pgoff, pgoff_t end_pgoff)
{
struct file *file = vmf->vma->vm_file;
struct address_space *mapping = file->f_mapping;
struct folio_batch fbatch;
unsigned int rpages;
pte_t *pte;
...
rcu_read_lock();
filemap_get_read_batch(mapping, start_pgoff, &fbatch);
for (i = 0; i < folio_batch_count(&fbatch); i++) {
struct folio *folio = fbatch.folios[i];
if (folio_test_readahead(folio))
page_cache_async_readahead(mapping, ra, file,
folio, folio->index,
ra->ra_pages);
if (filemap_map_folio_range(vmf, folio, rpages,
start_pgoff, end_pgoff))
break;
}
rcu_read_unlock();
}
```
`filemap_get_read_batch()` 持有 `rcu_read_lock` 并在 `xas_for_each()` 中遍历xarray。一个关键边界条件:folio在batch查询和 `set_pte_at()` 之间可能被 `shrink_active_list()` 逐出active list,导致刚映射的PTE指向一个即将被回收的folio。但这在语义上是安全的——page reclaim必须通过 `try_to_unmap_one()` 检查pte并做pte_clear,不会漏掉映射。
`do_fault_around()` 的range由 `fault_around_bytes` 控制,默认65536字节。增大该值(通过 `debugfs` 的 `fault_around_bytes`)可以提升顺序缺页的批量映射吞吐,但对于稀疏VMA,多余的PTE walk反而降低性能。`fault_around_bytes` 的单次最大值不能超过pmd_size,否则 `pmd_none()` 检查不会通过。
mlock是消除major fault的激进路径。`mlock_fixup()` 在 `mm/mlock.c` 中通过 `__mlock_vma_pages_range()` 对VMA范围内的每页做 `__get_user_pages()` 并标记 `PG_unevictable`:
```c
static int mlock_fixup(struct vm_area_struct *vma,
struct vm_area_struct **prev,
unsigned long start, unsigned long end,
unsigned int newflags)
{
...
if (newflags & VM_LOCKED) {
if (!(oldflags & VM_LOCKED))
ret = __mlock_vma_pages_range(vma, start,
end, &locked);
}
...
}
```
`__mlock_vma_pages_range()` 对每个页做gup,但大内存场景下需要注意 `mmap_lock` 的递归获取——`__get_user_pages_locked()` 内部通过 `faultin_page()` 触发 `handle_mm_fault()`,而后者已经在 `mlock_fixup()` 调用者处持有了 `mmap_lock` 的写锁。`MCL_ONFAULT` 标志避免了这种递归:它不预缺页,只在VMA上设置 `VM_LOCKEDONFAULT`,让首次缺页触发 `mlock_pte_range()` 自动将页上锁,但代价是首次缺页仍可能是major fault。
竞态条件在 `mm/memory.c` 的 `handle_pte_fault()` 中集中爆发。`do_anonymous_page()` 的 `pte_none()` 检查和 `set_pte_at()` 之间是一个经典的TOCTOU窗口:
```c
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct page *page;
pte_t entry;
if (pte_alloc(vmf->vma->vm_mm, vmf->pmd))
return VM_FAULT_OOM;
vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm,
vmf->pmd, vmf->address,
&vmf->ptl);
if (!pte_none(*vmf->pte)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
goto unlock;
}
page = alloc_zeroed_user_highpage(vmf->vma, vmf->address);
...
set_pte_at(vmf->vma->vm_mm, vmf->address,
vmf->pte, entry);
...
}
```
两个线程同时在缺同一地址的匿名页时,各自分配一个zero page,后到的 `set_pte_at()` overwrite先到的映射,造成一次多余的page allocation和一次page leak需要 `page_remove_rmap()` 回收。对于高频缺页的workload,这个重复分配在page allocator的 `rmqueue_bulk()` 上产生可以测量的开销。内核解决此问题的策略是用 `FAULT_FLAG_TRIED` 让第二次fault走 `do_fault()` 回退,但匿名页路径没有readahead,这个回退只减少了一次check。
`filemap_fault()` 内部 `xa_lock` 的获取顺序是另一个常见陷阱。`filemap_get_folio()` 在 `__filemap_get_folio()` 中通过 `xa_state` 做 `xas_load()`,持有 `rcu_read_lock` 但不持有xa_lock。如果 `xas_load()` 返回folio,才调用 `folio_try_get_rcu()` 增加refcount,并在成功时释放rcu锁。如果folio在这个窗口中被truncate,`folio_try_get_rcu()` 获得的folio在 `folio_test_uptodate()` 检查时失败,goto `page_not_uptodate`,触发完整的readpage:
```
线程A: filemap_get_folio() -> xas_load() got folio
线程B: truncate_inode_pages_range() -> remove_mapping() freeze refcount
线程A: folio_try_get_rcu() succeeds (refcount > 0)
线程A: folio_test_uptodate() -> false -> goto page_not_uptodate
线程A: folio_lock() -> blocked on folio being truncated
```
线程A在 `folio_lock()` 上的spin实际上是等待folio被truncate路径释放。`folio_lock()` 内部如果folio不属于当前线程又不处于 `PG_locked` 状态(truncate时folio被设置 `PG_locked`),`__folio_lock()` 会调用 `io_schedule()` 让出CPU,导致major fault的延迟从disk IO变为调度延迟,难以通过readahead手段解决。
`pte_alloc_map()` 的并发分配也有边界问题。两个线程在相同pmd entry上触发缺页,都看到 `pmd_none()` 为真并调用 `__pte_alloc()`。`pmd_lock` 保证只有一个 `pmd_populate()` 写入新的pte table,另一个线程获取锁后看到 `pmd_present(*pmd)` 为真,释放刚刚分配的pte table并返回。这个路径调用 `__pte_free_tlb()` 触发 `flush_tlb_mm()`,在多socket系统上是一个跨die的IPI广播,在perf采样中表现为 `native_flush_tlb_others` 热点。
Large folio是降低pagefault吞吐的有力手段。`filemap_fault()` 中的 `filemap_alloc_folio()` 通过 `order` 参数向buddy allocator申请2^order个连续物理页:
```c
static inline struct folio *filemap_alloc_folio(gfp_t gfp,
unsigned int order)
{
return folio_alloc(gfp, order);
}
```
当folio order大于0时,一次major fault映射2^order个PTE,后续访问不再触发任何minor fault。但alloc的代价是buddy allocator的 `compaction` 路径——`__alloc_pages_slowpath()` 在 `compact_zone()` 中做页面迁移时,可能因无法直接回收而进入direct reclaim,导致fault延迟增加。folio order的选择由 `mapping->a_ops->readahead` 中 `(index << PAGE_SHIFT) & (pmd_size - 1)` 的alignment决定,对齐到PMD边界才分配PMD-size folio。
bpftrace attach到 `tracepoint:exceptions:page_fault_user` 可以精确定位major fault的地址分布:
```bash
bpftrace -e '
tracepoint:exceptions:page_fault_user
{
@[kstack] = count();
}
'
```
stack trace中如果 `filemap_fault` 是top entry但 `readpage` 的跟踪点 `block:block_rq_issue` 不匹配,表明瓶颈不在磁盘IO而在page cache内部的 `folio_lock` contention。此时增大readahead反而加重cache污染,应针对 `xa_lock` 做 `perf c2c` 分析确认cacheline bouncing源。
`mincore()` 系统调用用于在生产环境诊断major fault来源。`mincore_pte_range()` 对每个PTE检查 `pte_present()`,如果页面不在内存中,返回0。结合 `addr2line` 将fault address反解为文件偏移,可以判断是mmap文件还是匿名页。对于mmap文件缺页,`/proc/` 下对应进程的 `smaps_rollup` 中的 `MajorPageFaults` 字段提供累计计数,但无法定位到具体代码路径——只能通过perf的采样实现。
实测pagefault吞吐上限的方法是用 `perf bench` 构造纯minor fault的测试:`perf bench sched messaging --pipe` 走的是pipe而非mmap。更精确的方式是 `mmap` 一个已读入page cache的大文件后 `madvise(MADV_RANDOM)` 再遍历,测纯minor fault的 `handle_mm_fault()` 吞吐上界,然后对比加上major fault后的退化比例。当minor fault吞吐(`pgfault - pgmajfault` 的每秒差值)低于200万次/core时,问题通常在 `mmap_lock` 或TLB一致性而非缺页路径本身。
Linux pagefault吞度量测量与major fault消除
张小明
前端开发工程师
终极HTML5视频播放速率控制:Video Speed Controller技术架构深度解析
终极HTML5视频播放速率控制:Video Speed Controller技术架构深度解析 【免费下载链接】videospeed HTML5 video speed controller (for Google Chrome) 项目地址: https://gitcode.com/gh_mirrors/vi/videospeed Video Speed Controller是一款专业级的Chrome…
Ryzen SMU调试工具完全指南:硬件级精准控制AMD处理器的终极方案
Ryzen SMU调试工具完全指南:硬件级精准控制AMD处理器的终极方案 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: h…
Mac与Linux服务器传文件,除了scp你还可以试试这几种更高效的工具(附对比)
Mac与Linux服务器高效传文件:5种专业工具深度评测与场景指南在跨平台开发运维中,文件传输如同程序员的血脉通道。当你在凌晨三点调试生产环境故障,突然需要将20GB的日志文件从CentOS服务器拉到MacBook Pro分析时,传统SCP的龟速传输…
终极指南:让Xbox手柄在macOS上完美工作的免费开源方案
终极指南:让Xbox手柄在macOS上完美工作的免费开源方案 【免费下载链接】360Controller TattieBogle Xbox 360 Driver (with improvements) 项目地址: https://gitcode.com/gh_mirrors/36/360Controller 你是否曾经满怀期待地在Mac上连接Xbox手柄准备畅玩游戏…
MPC8540 DUART FIFO与LBC控制器配置实战与调试指南
1. 项目概述在嵌入式系统开发,尤其是通信和工业控制领域,处理器与外设之间的数据交换效率直接决定了系统的整体性能。MPC8540 PowerQUICC III作为一款经典的集成式通信处理器,其内部集成的DUART(双通用异步收发器)和LB…
深度解析Mac Mouse Fix:革命性macOS鼠标优化技术架构与实战配置指南
深度解析Mac Mouse Fix:革命性macOS鼠标优化技术架构与实战配置指南 【免费下载链接】mac-mouse-fix Mac Mouse Fix - Make Your $10 Mouse Better Than an Apple Trackpad! 项目地址: https://gitcode.com/GitHub_Trending/ma/mac-mouse-fix Mac Mouse Fix是…