4. 内存分配API(下):vmalloc与vmap、ioremap、alloc_pages与__get_free_pages、slab分配器与kmem_cache

本章继续深入Linux内核内存分配的核心API。上一节我们讨论了以kmalloc为代表的连续物理内存分配器,本节将覆盖虚拟地址连续但物理地址可能不连续的vmalloc系列、用于映射外设寄存器的ioremap、底层页分配器alloc_pages/__get_free_pages,以及高性能对象缓存slab分配器。理解这些API的适用场景与性能特征,是编写高效设备驱动的关键。

核心原则: 物理连续 ≠ 虚拟连续。驱动开发者必须根据硬件DMA需求(物理连续)和软件访问便利性(虚拟连续)选择合适的分配器。

4.1 vmalloc 与 vmap:虚拟连续映射

vmalloc 分配虚拟地址连续、但物理地址可能不连续的内存区域。它适用于需要大块内存但不需要物理连续性的场景,例如动态加载的内核模块、大型软件缓冲区等。其性能开销高于 kmalloc,因为需要建立新的页表映射。

#include <linux/vmalloc.h>

void *vmalloc(unsigned long size);
void vfree(const void *addr);

// 带标志的变体
void *vmalloc_flags(unsigned long size, gfp_t flags);

vmap 则将一个已分配的物理页数组(struct page **)映射到连续的虚拟地址空间。它不分配物理页,只建立映射。

void *vmap(struct page **pages, unsigned int count,
           unsigned long flags, pgprot_t prot);
void vunmap(const void *addr);
性能对比: vmalloc 分配耗时约为 kmalloc 的 10~100 倍,且可能引起 TLB 刷新。在中断上下文或性能关键路径上应避免使用。
vmalloc 与 vmap 虚拟内存映射架构 vmalloc 分配的虚拟地址空间 内核页表(非连续映射) 物理页0 物理页1 物理页2 vmap 映射的虚拟地址空间 内核页表(映射已分配页) 已分配页A 已分配页B 已分配页C vmalloc:分配物理页 + 建立非连续映射 vmap:仅建立映射,物理页由调用者提供

4.2 ioremap:外设寄存器映射

ioremap 是设备驱动中最常用的映射函数之一。它将外设的物理地址(寄存器或内存)映射到内核虚拟地址空间,使驱动可以通过指针访问硬件。其核心特性是禁止CPU缓存,确保对寄存器的读写直接作用于硬件。

#include <asm/io.h>

void __iomem *ioremap(phys_addr_t offset, unsigned long size);
void iounmap(void __iomem *addr);

// 变体:指定缓存属性
void __iomem *ioremap_cache(phys_addr_t offset, unsigned long size);
void __iomem *ioremap_wc(phys_addr_t offset, unsigned long size); // 写合并
映射类型缓存策略典型用途
ioremap非缓存、非缓冲(Device memory)控制寄存器、状态寄存器
ioremap_cache回写缓存(Normal memory)帧缓冲、大容量片上SRAM
ioremap_wc写合并(Write-combine)显存、DMA缓冲区(提高写入性能)
关键警告: 对 ioremap 返回的地址,必须使用 readl()/writel() 等访问函数,而非直接解引用。这保证了在ARM、PowerPC等弱一致性架构上的正确性。

4.3 alloc_pages 与 __get_free_pages:底层页分配器

这两个函数直接操作伙伴系统(Buddy System),以2的幂次个连续物理页为单位进行分配。它们是内核最底层的内存分配接口,也是 kmalloc 和 vmalloc 的基础。

#include <linux/gfp.h>

// 返回 struct page *,适用于需要操作页结构体的场景
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
struct page *alloc_page(gfp_t gfp_mask); // order = 0

// 返回虚拟地址,适用于直接访问内存
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
unsigned long __get_free_page(gfp_t gfp_mask); // order = 0

// 释放
void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);

order 参数: 分配 2order 个连续物理页。order 最大值为 10(即 4MB,页面大小4KB时),但实际可用值受系统内存碎片程度限制。

伙伴系统分配:alloc_pages 流程 请求 order=2 (16KB) 查找空闲链表 分割更大块 分配 4 个连续页 剩余伙伴块入空闲链表 alloc_pages 返回 struct page *,适用于 DMA 映射、页高速缓存 __get_free_pages 返回虚拟地址,适用于直接内存访问

4.4 slab分配器与kmem_cache:对象缓存

slab分配器是内核用于管理小对象(通常小于一页)的高效缓存机制。它避免了频繁的伙伴系统操作,通过对象复用和着色(coloring)提高缓存命中率。对于驱动中频繁分配/释放的固定大小结构体(如 struct urb、struct sk_buff),应使用 kmem_cache 创建专用缓存。

#include <linux/slab.h>

// 创建缓存
struct kmem_cache *kmem_cache_create(const char *name,
                                      unsigned int size,
                                      unsigned int align,
                                      slab_flags_t flags,
                                      void (*ctor)(void *));

// 分配/释放对象
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
void kmem_cache_free(struct kmem_cache *cachep, void *objp);

// 销毁缓存
void kmem_cache_destroy(struct kmem_cache *cachep);
特性kmallockmem_cache
对象大小任意(向上取整到2的幂)固定大小(创建时指定)
内存利用率可能有内部碎片无内部碎片,完全匹配
分配/释放速度较快极快(常驻CPU缓存)
构造函数不支持支持(对象首次分配时调用)
调试能力有限支持红区检测、对象毒化等
驱动最佳实践: 如果驱动需要频繁分配同一结构体(例如每秒数千次),请使用 kmem_cache_create 创建专用缓存。这不仅能提高性能,还能通过 slab 调试机制捕获内存越界和释放后使用(use-after-free)错误。
slab 分配器内部结构 kmem_cache slabs_full (全满) slabs_partial (部分空闲) slabs_free (全空) 单个 slab(通常为 1 个或多个连续物理页) 对象0 对象1 对象2 对象3 ... 对象N 着色偏移 (cacheline对齐) 着色(coloring)通过偏移不同 slab 的起始位置,使对象均匀分布在不同 cacheline,减少缓存行冲突

4.5 API选择决策指南

在设备驱动开发中,选择正确的内存分配API直接影响系统稳定性和性能。以下决策树总结了选择依据:

  1. 是否需要物理连续?
    • 是 → 进入第2步
    • 否 → vmalloc(大块)或 kmalloc(小块,< 128KB)
  2. 是否用于DMA?
    • 是 → dma_alloc_coherent 或 __get_free_pages + DMA API
    • 否 → 进入第3步
  3. 对象大小是否固定且频繁分配?
    • 是 → kmem_cache_alloc(slab缓存)
    • 否 → kmalloc 或 alloc_pages
  4. 是否需要映射外设寄存器?
    • 是 → ioremap
内存分配标志(gfp_mask)注意事项: 在中断上下文、软中断、自旋锁持有期间,必须使用 GFP_ATOMIC 标志,禁止睡眠。进程上下文可以使用 GFP_KERNEL。错误使用 GFP_KERNEL 在原子上下文中会导致内核崩溃(内核调度 while atomic 错误)。