2. 页分配器:alloc_pages 与 __get_free_pages 接口、页分配标志 GFP_KERNEL 与 GFP_ATOMIC
页分配器是 Linux 内核最底层的内存分配引擎,直接管理物理内存的页框(通常为 4KB)。所有更高级的分配机制(如 slab 分配器、kmalloc)最终都依赖页分配器从伙伴系统获取连续物理页。本章深入剖析两个核心接口 alloc_pages 与 __get_free_pages,以及决定分配行为的关键标志 GFP_KERNEL 与 GFP_ATOMIC。
2.1 伙伴系统与页分配器架构
页分配器基于伙伴系统(Buddy System)算法,将物理内存按 2 的幂次阶(order)组织成链表。分配时从满足需求的最小阶开始搜索,必要时分裂高阶块;释放时合并相邻的伙伴块以抵抗碎片化。
核心概念:阶(order)表示连续物理页的数量,实际页数为 2order。例如 order=0 为 1 页(4KB),order=3 为 8 页(32KB)。
2.2 alloc_pages 与 __get_free_pages 接口对比
两个接口都用于分配连续物理页,但返回类型和适用场景不同:
| 接口 | 原型 | 返回值 | 典型用途 |
|---|---|---|---|
alloc_pages |
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order) |
struct page * 指针 |
需要直接操作页描述符的场景,如建立页表映射、DMA 缓冲区 |
__get_free_pages |
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) |
虚拟地址(线性地址) | 需要直接读写内存内容的场景,如内核临时缓冲区 |
alloc_page |
struct page *alloc_page(gfp_t gfp_mask) |
struct page * 指针 |
单页分配的快捷宏(order=0) |
get_zeroed_page |
unsigned long get_zeroed_page(gfp_t gfp_mask) |
虚拟地址(已清零) | 需要零初始化页面的安全敏感场景 |
关键区别:
alloc_pages 返回 struct page *,适合与 MMU 页表操作配合;__get_free_pages 返回虚拟地址,适合直接内存访问。两者底层都调用 __alloc_pages_nodemask,性能差异极小。
2.3 页分配标志:GFP_KERNEL 与 GFP_ATOMIC
分配标志(GFP flags)决定了页分配器的行为策略,包括是否允许睡眠、是否允许回收内存、是否允许使用紧急保留内存等。其中 GFP_KERNEL 和 GFP_ATOMIC 是最常用的两个标志。
2.3.1 GFP_KERNEL:标准进程上下文分配
GFP_KERNEL 是内核中最常用的分配标志,用于进程上下文(可以睡眠)的内存分配请求。
- 允许睡眠:当内存不足时,分配器可以阻塞当前进程,等待内存回收或页面换出。
- 允许回收:可以触发 kswapd 内核线程进行页面回收,或直接进行同步回收。
- 允许使用文件系统:在回收过程中可能涉及文件系统操作(如回写脏页)。
- 适用场景:系统调用、文件系统操作、设备驱动中的非中断上下文分配。
// GFP_KERNEL 典型用法
struct page *page = alloc_pages(GFP_KERNEL, 3); // 分配 8 页(32KB)
if (!page) {
pr_err("Failed to allocate 8 pages\n");
return -ENOMEM;
}
// 或使用 __get_free_pages
unsigned long addr = __get_free_pages(GFP_KERNEL, 1); // 分配 2 页(8KB)
if (!addr) {
pr_err("Failed to allocate 2 pages\n");
return -ENOMEM;
}
2.3.2 GFP_ATOMIC:原子上下文分配
GFP_ATOMIC 用于不能睡眠的上下文,如中断处理程序、软中断、自旋锁保护区域、tasklet 等。
- 禁止睡眠:分配器不会阻塞当前执行流,只能从现有空闲内存中分配。
- 使用紧急保留内存:当常规空闲内存不足时,可以消耗系统预留的紧急内存(min 水线以下的部分)。
- 禁止回收:不会触发页面回收或 I/O 操作,分配失败直接返回 NULL。
- 适用场景:中断处理程序、定时器回调、持有自旋锁的代码路径。
// GFP_ATOMIC 典型用法(中断上下文)
irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
struct page *page;
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
// 原子上下文分配,不能睡眠
page = alloc_pages(GFP_ATOMIC, 0); // 分配 1 页
if (!page) {
pr_warn("Atomic allocation failed in interrupt\n");
spin_unlock_irqrestore(&my_lock, flags);
return IRQ_NONE;
}
// ... 使用 page ...
spin_unlock_irqrestore(&my_lock, flags);
return IRQ_HANDLED;
}
2.4 GFP_KERNEL 与 GFP_ATOMIC 详细对比
| 对比维度 | GFP_KERNEL | GFP_ATOMIC |
|---|---|---|
| 允许睡眠 | 是 | 否 |
| 允许页面回收 | 是(同步/异步) | 否 |
| 允许 I/O 操作 | 是 | 否 |
| 使用紧急保留内存 | 否(仅 min 水线以上) | 是(可消耗 min 以下) |
| 分配失败行为 | 等待后重试或 OOM | 立即返回 NULL |
| 典型调用上下文 | 进程上下文、系统调用 | 中断、软中断、自旋锁内 |
| 底层标志值 | __GFP_WAIT | __GFP_IO | __GFP_FS |
__GFP_HIGH | __GFP_ATOMIC |
常见错误:在中断处理程序中使用
GFP_KERNEL 会导致内核崩溃(schedule while atomic)。驱动开发中务必根据当前上下文选择正确的 GFP 标志。如果不确定当前是否在原子上下文,可以使用 in_atomic() 或 irqs_disabled() 辅助判断。
2.5 其他常用 GFP 标志组合
除了 GFP_KERNEL 和 GFP_ATOMIC,内核还提供了多种预定义组合:
| GFP 标志 | 含义 | 典型场景 |
|---|---|---|
GFP_NOIO |
禁止 I/O 操作,但允许睡眠 | 块设备层内部分配,避免递归回写 |
GFP_NOFS |
禁止文件系统操作,但允许 I/O | 文件系统内部分配,避免递归锁 |
GFP_USER |
用于用户空间内存分配 | 缺页处理、用户态缓冲区 |
GFP_HIGHUSER |
允许从高端内存分配 | 用户空间大块分配 |
GFP_DMA |
限制从 DMA 可用区域分配 | DMA 缓冲区分配(低 16MB 或特定区域) |
2.6 页释放接口
分配的页必须正确释放,否则会造成内存泄漏。释放接口与分配接口对应:
// 释放 alloc_pages 分配的页
void __free_pages(struct page *page, unsigned int order);
// 释放 __get_free_pages 分配的页
void free_pages(unsigned long addr, unsigned int order);
// 释放单页的快捷宏
void free_page(unsigned long addr);
最佳实践:分配和释放的 order 必须一致。例如 order=3 分配了 8 页,释放时也必须使用 order=3。错误使用 order 会导致伙伴系统元数据损坏,引发内核崩溃。
2.7 实际驱动示例:使用页分配器管理缓冲区
以下示例展示了一个字符设备驱动如何使用页分配器管理 DMA 缓冲区:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/gfp.h>
#include <linux/mm.h>
#define BUF_ORDER 4 // 16 页 = 64KB
static struct page *buf_pages;
static unsigned long buf_addr;
static int __init page_demo_init(void)
{
// 使用 GFP_KERNEL 分配 16 页连续物理内存
buf_pages = alloc_pages(GFP_KERNEL, BUF_ORDER);
if (!buf_pages) {
pr_err("Failed to allocate %d pages\n", 1 << BUF_ORDER);
return -ENOMEM;
}
// 获取虚拟地址
buf_addr = (unsigned long)page_address(buf_pages);
pr_info("Allocated %d pages at physical: %llx, virtual: %lx\n",
1 << BUF_ORDER,
(unsigned long long)page_to_phys(buf_pages),
buf_addr);
// 清零缓冲区
memset((void *)buf_addr, 0, PAGE_SIZE << BUF_ORDER);
return 0;
}
static void __exit page_demo_exit(void)
{
if (buf_pages) {
__free_pages(buf_pages, BUF_ORDER);
pr_info("Freed %d pages\n", 1 << BUF_ORDER);
}
}
module_init(page_demo_init);
module_exit(page_demo_exit);
MODULE_LICENSE("GPL");
调试技巧:使用
cat /proc/buddyinfo 可以查看伙伴系统各 order 的空闲页数量,帮助诊断内存碎片化问题。分配大 order 失败时,通常是因为物理内存碎片化严重。
2.8 本章小结
- 页分配器是内核内存管理的基石,基于伙伴系统算法管理连续物理页。
- alloc_pages 返回
struct page *,适合页表操作;__get_free_pages 返回虚拟地址,适合直接内存访问。 - GFP_KERNEL 允许睡眠和回收,用于进程上下文;GFP_ATOMIC 禁止睡眠,用于原子上下文。
- 选择 GFP 标志的核心原则:根据当前执行上下文判断是否允许睡眠。
- 分配和释放的 order 必须严格匹配,否则会破坏伙伴系统数据结构。