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)。
伙伴系统页分配器架构 伙伴系统页分配器架构 分配接口层 alloc_pages(gfp_mask, order) __get_free_pages(gfp_mask, order) alloc_page(gfp_mask) get_zeroed_page(gfp_mask) 返回 struct page * 或虚拟地址 unsigned long 核心分配层 __alloc_pages_nodemask() zone 水线检查 内存回收唤醒 OOM killer 决策 快速路径 / 慢速路径 伙伴系统数据结构 free_area[MAX_ORDER] 每个 order 一个链表 struct page 描述符 zone->free_area[] MAX_ORDER = 11 (0~10阶) 分配路径:接口层 → 核心分配层(水线检查/回收/OOM)→ 伙伴系统链表操作

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() 辅助判断。
GFP_KERNEL vs GFP_ATOMIC 决策流程 页分配标志决策流程 内存分配请求 原子上下文? (中断/自旋锁内) 是 否 GFP_ATOMIC 禁止睡眠/回收 GFP_KERNEL 允许睡眠/回收 分配成功 返回页/地址 分配成功 返回页/地址 等待回收 / OOM 立即返回 NULL 选择 GFP 标志的核心原则:能否睡眠?能则 GFP_KERNEL,不能则 GFP_ATOMIC

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 必须严格匹配,否则会破坏伙伴系统数据结构。