4. 内核中的 struct page 与页分配:页分配器、alloc_pages、__get_free_pages、page 结构体详解
物理内存是 Linux 驱动与硬件交互的底层基础。在编写 mmap 驱动时,驱动开发者必须理解内核如何管理物理页、如何分配连续物理内存,以及如何通过 struct page 描述每一页的状态。本章深入剖析内核页分配器的核心机制、三个关键分配接口的差异,以及 struct page 结构体的关键字段,为后续的物理内存映射实战打下坚实基础。
4.1 内核页分配器(Page Allocator)概述
Linux 内核使用伙伴系统(Buddy System)作为物理页分配器的核心算法。它将物理内存划分为 2order 个连续页的块(order 从 0 到 MAX_ORDER-1),通过合并与分裂来减少外部碎片。
关键概念:页分配器管理的最小单位是物理页帧(Page Frame),每个物理页帧对应一个
struct page 实例。分配器维护 11 个链表(order 0~10),每个链表挂载对应大小的空闲块。
驱动开发者提示:在 mmap 驱动中,通常使用 order=0(单页)或较小的 order 值分配物理内存。大块连续物理内存(order≥4)在长时间运行的系统上容易分配失败,建议在驱动初始化时预分配。
4.2 核心分配接口:alloc_pages 与 __get_free_pages
内核提供两套主要的物理页分配接口,它们最终都调用伙伴系统的核心函数 __alloc_pages_nodemask(),但返回类型和适用场景不同。
| 接口 | 返回类型 | 物理地址连续性 | 适用场景 |
|---|---|---|---|
alloc_pages(gfp_mask, order) |
struct page * |
连续 | 需要直接操作 struct page 的场景(如 mmap、DMA、页表操作) |
__get_free_pages(gfp_mask, order) |
unsigned long(内核虚拟地址) |
连续 | 仅需内核虚拟地址访问的场景(如内核缓冲区、临时数据) |
get_zeroed_page(gfp_mask) |
unsigned long |
连续(单页) | 需要清零的单页分配 |
4.2.1 alloc_pages 接口详解
alloc_pages 返回 struct page * 指针,这是内核中描述物理页的核心结构。驱动通过该指针可以获取页的物理地址、设置页属性、建立页表映射。
#include <linux/gfp.h>
#include <linux/mm.h>
struct page *page;
int order = 2; // 分配 2^2 = 4 个连续物理页(16KB)
// 分配物理页,GFP_KERNEL 表示可睡眠等待
page = alloc_pages(GFP_KERNEL, order);
if (!page) {
pr_err("alloc_pages failed\n");
return -ENOMEM;
}
// 获取物理地址(页帧号 PFN)
unsigned long pfn = page_to_pfn(page);
phys_addr_t phys_addr = page_to_phys(page);
pr_info("Allocated 4 pages, PFN: %lu, Phys: %pa\n", pfn, &phys_addr);
// 获取内核虚拟地址(若需要)
void *kaddr = page_address(page);
pr_info("Kernel virtual addr: %p\n", kaddr);
4.2.2 __get_free_pages 接口详解
__get_free_pages 直接返回内核虚拟地址(线性映射区),适合驱动只需要在 CPU 上读写内存、不需要操作 struct page 的场景。
unsigned long vaddr;
int order = 1; // 分配 2 个连续物理页(8KB)
vaddr = __get_free_pages(GFP_KERNEL, order);
if (!vaddr) {
pr_err("__get_free_pages failed\n");
return -ENOMEM;
}
// 直接使用虚拟地址
memset((void *)vaddr, 0, PAGE_SIZE * (1 << order));
pr_info("Allocated 2 pages at vaddr: 0x%lx\n", vaddr);
// 如果需要物理地址,需要转换
struct page *page = virt_to_page(vaddr);
phys_addr_t phys = page_to_phys(page);
pr_info("Physical addr: %pa\n", &phys);
重要区别:
__get_free_pages 返回的虚拟地址位于内核线性映射区(通常为 3GB~4GB 或 4GB 以上,取决于架构)。在 64 位系统上,该区域与物理内存有固定偏移关系。驱动在 mmap 时,必须使用 struct page * 或物理地址,不能直接使用该虚拟地址。
4.3 struct page 结构体深度解析
struct page 是内核中最重要的数据结构之一,每个物理页帧对应一个实例。它占用约 40~64 字节(取决于架构和配置),通过联合体(union)复用字段以节省内存。
4.3.1 关键字段详解
- flags:页状态标志位,使用
PG_*宏定义。例如PG_locked(页被锁定)、PG_dirty(脏页)、PG_uptodate(数据有效)、PG_reserved(保留页,驱动分配的内存通常设置此标志)。 - _refcount:页的引用计数,内核使用
get_page()/put_page()操作。当计数降为 0 时,页被释放回伙伴系统。 - _mapcount:页表映射计数,表示该页被映射到多少个进程的页表中。mmap 驱动中,每次建立用户映射都会增加此计数。
- mapping:指向
address_space的指针。文件页指向文件的address_space;匿名页最低位为 1 表示匿名;驱动分配的页通常为NULL或指向驱动自定义结构。 - private:驱动私有数据字段。在 mmap 驱动中,常用来存储驱动自定义的上下文指针或标志位。
- virtual:页的内核虚拟地址。对于高端内存(HIGHMEM)的页,此字段为
NULL,需要通过kmap()临时映射。
4.4 页分配标志(GFP Flags)详解
GFP(Get Free Pages)标志控制分配器的行为,包括是否可睡眠、从哪个内存区域分配、是否允许回收等。
| GFP 标志 | 含义 | 驱动典型场景 |
|---|---|---|
GFP_KERNEL |
普通内核分配,可睡眠,允许 I/O 和回收 | 大多数驱动分配场景(进程上下文) |
GFP_ATOMIC |
原子分配,不可睡眠,不允许 I/O | 中断上下文、自旋锁保护区域 |
GFP_DMA |
从 DMA 区域分配(低 16MB 或 4GB 以下) | ISA DMA、某些 32 位 DMA 设备 |
GFP_DMA32 |
从 DMA32 区域分配(4GB 以下) | 64 位系统上的 32 位 DMA 设备 |
GFP_HIGHUSER |
从用户空间区域分配,可回收 | 用户态页分配(通常不用于驱动) |
__GFP_ZERO |
返回清零的页(与 GFP 标志按位或) | 需要初始化数据的场景 |
// 驱动中常见的 GFP 组合示例
// 1. 进程上下文,可睡眠,需要清零
struct page *page = alloc_pages(GFP_KERNEL | __GFP_ZERO, 2);
// 2. 中断上下文,不可睡眠
unsigned long vaddr = __get_free_pages(GFP_ATOMIC, 0);
// 3. 需要 DMA 区域的物理内存(某些硬件要求)
struct page *dma_page = alloc_pages(GFP_KERNEL | GFP_DMA, 1);
// 4. 大块分配,允许使用紧急内存池
struct page *emergency_page = alloc_pages(GFP_KERNEL | __GFP_MEMALLOC, 4);
最佳实践:在 mmap 驱动中,推荐使用
alloc_pages(GFP_KERNEL | __GFP_ZERO, order) 分配物理页。清零可以避免向用户空间泄露内核敏感数据,同时确保页的 PG_reserved 标志被正确设置。
4.5 页释放接口
分配的内存必须正确释放,否则会造成内存泄漏。释放接口与分配接口成对使用。
| 分配接口 | 释放接口 | 说明 |
|---|---|---|
alloc_pages(gfp, order) |
__free_pages(page, order) |
传入 struct page * 和 order |
__get_free_pages(gfp, order) |
free_pages(addr, order) |
传入虚拟地址和 order |
get_zeroed_page(gfp) |
free_page(addr) |
单页释放的快捷方式 |
// 释放示例
struct page *page = alloc_pages(GFP_KERNEL, 2);
if (page) {
// ... 使用内存 ...
__free_pages(page, 2); // 释放 4 页
}
unsigned long vaddr = __get_free_pages(GFP_KERNEL, 1);
if (vaddr) {
// ... 使用内存 ...
free_pages(vaddr, 1); // 释放 2 页
}
常见错误:释放时必须使用与分配时相同的 order 值。如果 order 不匹配,伙伴系统会破坏空闲链表,导致内核崩溃。此外,驱动在模块卸载时务必释放所有分配的物理页,否则系统内存泄漏会逐渐耗尽可用内存。
4.6 驱动 mmap 中的页分配实战模式
在 mmap 驱动中,物理页的分配通常遵循以下模式:
- 预分配模式:在驱动初始化(probe)时分配一大块连续物理内存,mmap 时直接映射已分配的页。适合固定大小的缓冲区。
- 按需分配模式:在 mmap 回调中,根据用户请求的大小动态分配物理页。适合大小可变的映射。
- 预留内存模式:通过设备树或内核参数预留专用物理内存区域,驱动直接管理该区域。适合对延迟和连续性要求极高的场景(如 GPU、VPU)。
4.7 本章小结
- 内核页分配器基于伙伴系统,管理 2order 大小的连续物理页块。
alloc_pages返回struct page *,适合 mmap 和 DMA 场景;__get_free_pages返回虚拟地址,适合内核内部使用。struct page是物理页的描述符,驱动 mmap 时重点关注flags、_refcount、private和virtual字段。- GFP 标志控制分配行为,驱动中常用
GFP_KERNEL(进程上下文)和GFP_ATOMIC(中断上下文)。 - 分配和释放必须使用相同的 order 值,避免内存泄漏和内核崩溃。
- 驱动 mmap 的页分配模式包括预分配、按需分配和预留内存,开发者应根据场景选择。