news 2026/6/14 12:25:08

Linux pagefault吞度量测量与major fault消除

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux pagefault吞度量测量与major fault消除

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一致性而非缺页路径本身。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/14 12:24:30

终极指南:让Xbox手柄在macOS上完美工作的免费开源方案

终极指南&#xff1a;让Xbox手柄在macOS上完美工作的免费开源方案 【免费下载链接】360Controller TattieBogle Xbox 360 Driver (with improvements) 项目地址: https://gitcode.com/gh_mirrors/36/360Controller 你是否曾经满怀期待地在Mac上连接Xbox手柄准备畅玩游戏…

作者头像 李华
网站建设 2026/6/14 12:24:23

MPC8540 DUART FIFO与LBC控制器配置实战与调试指南

1. 项目概述在嵌入式系统开发&#xff0c;尤其是通信和工业控制领域&#xff0c;处理器与外设之间的数据交换效率直接决定了系统的整体性能。MPC8540 PowerQUICC III作为一款经典的集成式通信处理器&#xff0c;其内部集成的DUART&#xff08;双通用异步收发器&#xff09;和LB…

作者头像 李华