4. 页面分配API:alloc_pages、alloc_page、__get_free_pages等接口解析

Linux内核提供了多个页面分配接口,它们本质上是围绕伙伴系统(Buddy System)的封装。理解这些API的差异、使用场景以及内部调用路径,是进行内存性能调优的基础。本章将深入解析最核心的三个分配接口:alloc_pages、alloc_page 和 __get_free_pages。

4.1 核心API概览与对比

下表列出了三个主要页面分配接口的声明、返回值类型及关键特性,帮助开发者快速选择正确的API。

API 名称 函数原型 返回值类型 分配粒度 是否清零 主要用途
alloc_pages struct page *alloc_pages(gfp_t gfp_mask, unsigned int order) struct page * 2order 个连续物理页 否 内核内部管理,需要直接操作 struct page 的场景
alloc_page struct page *alloc_page(gfp_t gfp_mask) struct page * 1 个物理页 (order=0) 否 分配单页的快捷宏,本质是 alloc_pages(gfp_mask, 0)
__get_free_pages unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) 虚拟地址 (unsigned long) 2order 个连续物理页 否 需要直接访问线性映射虚拟地址的场景(如驱动DMA缓冲区)
get_zeroed_page unsigned long get_zeroed_page(gfp_t gfp_mask) 虚拟地址 (unsigned long) 1 个物理页 (order=0) 是 需要安全清零页面的场景(如用户态页面分配)
性能提示: __get_free_pages 返回的是线性映射区的虚拟地址,可以直接读写。但该地址对应的物理页是连续的,且不能用于高端内存(HIGHMEM)。如果需要分配高端内存页,必须使用 alloc_pages 并通过 kmap 映射。

4.2 API内部调用链路与伙伴系统交互

所有页面分配API最终都会调用伙伴系统的核心函数 __alloc_pages_nodemask。下图展示了从用户态或内核态发起分配请求到伙伴系统分配物理页面的完整调用路径。

页面分配API内部调用链路 页面分配API内部调用链路 调用者 (Driver/FS/MM) alloc_pages() alloc_page() __get_free_pages() __alloc_pages_nodemask() (伙伴系统核心入口) 快速路径 (Fast Path) 慢速路径 (Slow Path) rmqueue() / 从 freelist 摘取 内存回收 / OOM killer / 碎片整理 返回 struct page *
关键路径分析:
  • 快速路径 (Fast Path):直接从当前内存节点的 per-CPU pageset 或伙伴系统的空闲链表(freelist)中摘取页面。此路径无锁或仅使用本地锁,延迟极低(通常 < 1μs)。
  • 慢速路径 (Slow Path):当快速路径分配失败时触发。内核会尝试内存回收(kswapd)、异步/同步压缩(compaction),甚至调用 OOM killer 杀死进程以释放内存。此路径延迟可达毫秒级。

4.3 关键数据结构:struct page 与 gfp_t 标志

4.3.1 struct page:物理页的描述符

alloc_pages 返回的 struct page * 是内核管理物理页的核心元数据结构。每个物理页框(PFN)对应一个 struct page。该结构体包含的关键字段如下:

字段 类型 说明
flags unsigned long 页面标志位,如 PG_locked、PG_dirty、PG_swapbacked
_refcount atomic_t 页面的引用计数,决定页面是否可以被释放
mapping struct address_space * 指向该页所属的地址空间(用于文件页/匿名页)
lru struct list_head 用于将页面链接到 LRU 链表(活跃/非活跃链表)
private unsigned long 私有数据,用于块层或文件系统私有信息

4.3.2 gfp_t 标志:分配行为控制

gfp_t (Get Free Pages flags) 是控制分配行为的位掩码。正确设置 gfp_t 标志是避免内存分配死锁和性能问题的关键。

分类 标志 含义 性能影响
Zone 修饰符 __GFP_DMA 从 DMA 区域分配(ZONE_DMA,通常 < 16MB) 限制分配范围,可能降低分配成功率
__GFP_HIGHMEM 允许从高端内存分配(ZONE_HIGHMEM,仅32位系统) 扩大可用内存池,但需要额外 kmap 映射
__GFP_MOVABLE 分配可迁移页面(用于内存规整/防碎片) 有助于减少外部碎片,但分配延迟可能略增
行为修饰符 __GFP_WAIT (已弃用,使用 __GFP_RECLAIM) 允许分配器进入睡眠等待内存回收 可能导致进程阻塞,增加延迟
__GFP_IO 允许分配器发起磁盘 I/O 进行内存回收 显著增加分配延迟(可能 > 10ms)
__GFP_FS 允许分配器调用文件系统操作 可能引发递归锁问题,需谨慎使用
常用组合 GFP_KERNEL __GFP_RECLAIM | __GFP_IO | __GFP_FS 标准内核分配,允许阻塞和I/O
GFP_ATOMIC __GFP_HIGH (不允许睡眠/回收) 中断上下文使用,分配失败返回 NULL
调优警告: 在中断上下文或持有自旋锁时,严禁使用 GFP_KERNEL。必须使用 GFP_ATOMIC。否则会导致内核崩溃(内核态睡眠锁死)。GFP_ATOMIC 分配失败率较高,应尽量在初始化阶段预分配内存。

4.4 代码示例与使用模式

4.4.1 使用 alloc_pages 分配多页

#include <linux/gfp.h>
#include <linux/mm.h>

struct page *pages;
int order = 3;  // 分配 2^3 = 8 个连续物理页

// 分配 8 页,允许阻塞和 I/O
pages = alloc_pages(GFP_KERNEL, order);
if (!pages) {
    pr_err("Failed to allocate %d pages\n", 1 << order);
    return -ENOMEM;
}

// 使用 pages 结构体操作
// 获取第一个页的虚拟地址(仅限低端内存)
void *addr = page_address(pages);
memset(addr, 0, PAGE_SIZE << order);

// 释放页面
__free_pages(pages, order);

4.4.2 使用 __get_free_pages 获取虚拟地址

#include <linux/gfp.h>

unsigned long vaddr;
int order = 1;  // 分配 2 页

// 分配 2 个连续物理页,返回线性映射虚拟地址
vaddr = __get_free_pages(GFP_KERNEL, order);
if (!vaddr) {
    pr_err("__get_free_pages failed\n");
    return -ENOMEM;
}

// 直接使用虚拟地址
memset((void *)vaddr, 0, PAGE_SIZE << order);

// 释放
free_pages(vaddr, order);

4.4.3 性能敏感场景下的分配策略

在数据路径(data path)或实时任务中,应避免使用可能触发慢速路径的分配标志。推荐使用以下模式:

// 预分配 + 原子后备模式
struct page *prealloc_pages[POOL_SIZE];
gfp_t gfp_atomic = GFP_ATOMIC;  // 快速失败,不睡眠

// 在初始化阶段预分配
for (i = 0; i < POOL_SIZE; i++) {
    prealloc_pages[i] = alloc_pages(GFP_KERNEL, 0);
}

// 在热路径中从池中获取
struct page *get_page_from_pool(void)
{
    struct page *page;
    
    // 优先从预分配池获取
    page = prealloc_pool_get();
    if (page)
        return page;
    
    // 后备:原子分配(可能失败)
    page = alloc_pages(gfp_atomic, 0);
    if (!page)
        pr_warn_ratelimited("Atomic page allocation failed!\n");
    
    return page;
}

4.5 性能调优要点

  • 减少 order 值:高 order 分配(如 order > 3)容易触发内存规整(compaction),导致延迟抖动。尽量使用 order=0 的单页分配,或使用更大的 vmalloc 区域。
  • 使用 per-CPU 页集:内核为每个 CPU 缓存了少量单页(struct per_cpu_pageset)。频繁分配/释放单页时,应确保使用 GFP_KERNEL 以利用此缓存。
  • 避免 GFP_ATOMIC 滥用:GFP_ATOMIC 只能从内存节点的紧急预留池(min_free_kbytes)中分配。过度使用会导致系统内存紧张时无法分配。
  • 监控分配延迟:使用 tracepoint:mm_page_alloc 和 tracepoint:mm_page_free 跟踪分配延迟。高延迟通常意味着触发了慢速路径。
调优检查清单:
  1. 检查代码中是否有 order > 3 的分配,评估是否可以拆分为单页。
  2. 确认中断上下文是否误用了 GFP_KERNEL。
  3. 使用 /proc/buddyinfo 监控伙伴系统的碎片化程度。
  4. 在性能关键路径上,使用预分配内存池替代动态分配。

4.6 总结

alloc_pages、alloc_page 和 __get_free_pages 是内核内存分配的基石。选择哪个API取决于你需要 struct page * 还是虚拟地址。理解 gfp_t 标志对分配行为的影响,以及快速/慢速路径的触发条件,是进行内存分配性能调优的核心。在实际开发中,应优先使用 order=0 的分配,并避免在热路径中触发内存回收或规整。