preallocate_vmalloc_pages是 x86-64 内核在启动阶段用于预分配 vmalloc 区域页表页的初始化函数,位于arch/x86/mm/init_64.c。它的核心目的是避免运行时昂贵的页表同步操作。
核心问题:为什么需要预分配?
在 x86-64 上,vmalloc 区域的页表项被修改时,内核必须将这些变更同步到系统中所有进程的页表。这个同步操作会获取pgd_lock并遍历系统中所有页表,开销很大。
解决方案是在启动时一次性预分配好 vmalloc 区域所需的页表页。这样,所有进程的页表在创建时就能直接继承这些预分配的页表页,运行时不再需要同步。
预分配的层级:取决于页表模式
预分配的目标是最顶层的、需要同步的页表页:
5 级页表:需要预分配P4D层级。
4 级页表:需要预分配PUD层级(因为 P4D 层被折叠,PUD 是实际存在的第二硬件层级)。
代码逻辑(修正后)
该函数在 2020 年经历了一次重要修复。早期版本假设p4d_offset()/pud_offset()会进行存在性检查,但实际上它们不会,导致在 PGD 条目不存在时错误地解引用垃圾数据。
修正后的逻辑变得简洁而健壮:
static void __init preallocate_vmalloc_pages(void) { unsigned long addr; const char *lvl; for (addr = VMALLOC_START; addr <= VMALLOC_END; addr = ALIGN(addr + 1, PGDIR_SIZE)) { pgd_t *pgd = pgd_offset_k(addr); p4d_t *p4d; pud_t *pud; lvl = "p4d"; p4d = p4d_alloc(&init_mm, pgd, addr); if (!p4d) goto failed; if (pgtable_l5_enabled()) continue; lvl = "pud"; pud = pud_alloc(&init_mm, p4d, addr); if (!pud) goto failed; } return; failed: panic("Failed to pre-allocate %s pages for vmalloc area\n", lvl); }关键点:
直接调用
p4d_alloc()和pud_alloc(),由这些函数内部处理 PGD 条目不存在的情况,避免了手动检查的疏漏。5 级页表下,预分配到 P4D 即可(因为 P4D 是实际存在的层级);4 级页表下,需要继续预分配到 PUD(因为 P4D 被折叠,PUD 是实际的第二硬件层级)。
分配失败时直接
panic(),因为这些页表页必须在启动阶段就位,否则后续进程页表会缺失这些映射。
调用时机与效果
该函数在mem_init()中被调用。执行后,init_mm.pgd中 vmalloc 区域的顶层页表页就被预先创建好了。当新进程创建页表时,内核会复制init_mm的 PGD 条目,这些预分配的页表页因此被所有进程共享,彻底消除了运行时对 vmalloc 区域的页表同步需求。
这个优化也使得 x86-64 上的vmalloc_fault()路径变得不再必要(与 x86-32 不同),进一步简化了缺页异常处理。