do_anonymous_page是匿名页缺页异常的核心处理函数。当进程访问一个尚未分配物理内存的匿名内存区域(如堆、栈、私有映射)时,该函数负责为其分配物理页面并建立映射。
核心逻辑:读缺页与写缺页的分流
该函数最核心的设计是根据触发缺页的访问类型采取完全不同的策略:
1. 读操作触发的缺页(Read Fault)
进程只是读取一个尚未映射的地址。此时,内核不会立即分配新的物理页面,而是将一个全局的、内容全为零的零页(ZERO_PAGE)映射到该地址。
这个零页在系统启动时由
paging_init()分配,是只读的,被标记为special mapping。通过
pte_mkspecial()设置 PTE 的特殊位,告诉内核内存管理子系统:这个 PTE 指向的不是一个普通的、可回收的匿名页。优势:读取零页的内容是安全的(全零),且多个进程可以共享同一个零页,极大地节省了物理内存。只有当进程尝试写入这个地址时,才会触发真正的物理页分配。
2. 写操作触发的缺页(Write Fault)
进程尝试写入一个尚未映射的地址。此时,零页的“延迟分配”策略结束,内核必须为进程分配一块全新的、归其所有的物理内存。
调用
alloc_anon_folio()分配物理页面(在较新内核中,会尝试分配可变 order 的大 folio,即 mTHP,以提升性能)。新分配的页面内容被清零(使用
vma_alloc_zeroed_movable_folio或类似接口)。建立 PTE 映射时,会设置
pte_mkwrite和pte_mkdirty,赋予页面可写和脏的属性。同时增加进程的 RSS(常驻内存集)计数,表示该进程实际占用了新的物理内存。
关键操作与并发保护
函数在执行时,会获取 PTE 锁(pte_offset_map_lock),然后检查 PTE 是否确实为空(pte_none),以防止与其他缺页处理线程发生竞态。确认后,才调用set_pte_at将新的 PTE 写入页表。
现代内核中,do_anonymous_page进入时 PTE 是未映射且未锁定的状态,函数内部会自行获取锁并处理。
历史演进:从单页到可变大页
该函数经历了显著的性能优化演进:
早期:只处理单个 4KB 页面的分配。
Page Fault Scalability 补丁(2005年):移除了函数中对全局
page_table_lock的依赖,改用更细粒度的原子操作和 PTE 锁,显著提升了 SMP 系统下的缺页处理并行度。FLEXIBLE_THP / mTHP(2023年+):引入
alloc_anon_folio支持分配大于 4KB 但小于 PMD 大小(2MB)的可变 order 大 folio。这减少了缺页次数和每页的元数据开销,是当前内核性能优化的重要方向。