early_alloc_pgt_buf()是 Linux 内核 x86 架构启动早期,用于初始化页表缓冲区的关键函数。它从brk区域申请一块内存,并将其划分为页表页,供后续的alloc_low_page()分配使用。
代码逐行解析
void __init early_alloc_pgt_buf(void) { unsigned long tables = INIT_PGT_BUF_SIZE; // 1. 计算需要的内存大小 phys_addr_t base; base = __pa(extend_brk(tables, PAGE_SIZE)); // 2. 从 brk 区域扩展并转为物理地址 pgt_buf_start = base >> PAGE_SHIFT; // 3. 记录缓冲区起始页帧号 pgt_buf_end = pgt_buf_start; // 4. 分配游标初始化为起始位置 pgt_buf_top = pgt_buf_start + (tables >> PAGE_SHIFT); // 5. 记录缓冲区结束页帧号 }关键变量与操作
INIT_PGT_BUF_SIZE:这是需要预留的总字节数,由INIT_PGD_PAGE_COUNT * PAGE_SIZE计算得出。INIT_PGD_PAGE_COUNT会根据是否启用 KASLR 和 5 级页表动态调整,通常在 6 到 16 个页面之间。extend_brk(tables, PAGE_SIZE):从内核镜像的brk区域“扩展”出tables字节的空间。brk区域是内核镜像的一部分,在内核启动早期必然被映射,因此可以作为页表分配的“启动资本”。PAGE_SIZE参数确保扩展后的地址按页对齐。__pa():将brk区域的虚拟地址转换为物理地址。页表项中需要填写的是物理地址,因此这一步是必需的。三个关键全局变量:
pgt_buf_start:缓冲区的起始页帧号(PFN)。pgt_buf_end:当前分配位置,初始时等于pgt_buf_start。pgt_buf_top:缓冲区的结束页帧号。
在启动流程中的角色
这个函数在setup_arch()中被调用,执行时机非常早。它解决了“先有鸡还是先有蛋”的问题:
要建立物理内存的直接映射,需要页表。
但要分配存放页表的内存,又需要先有直接映射。
pgt_buf就是在这个矛盾中提供的一个安全内存池。在直接映射完全建立之前,alloc_low_page()会优先从这个缓冲区中取用内存来分配页表页。当pgt_buf_end追上pgt_buf_top(即缓冲区耗尽)时,内核才会转向memblock去查找更多已映射的内存。
设计意图
这个函数体现了内核启动早期对确定性和安全性的追求:使用brk区域而不是动态内存分配器,是因为此时伙伴系统尚未就绪,而brk区域是内核镜像的一部分,其映射关系是确定且可靠的。