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的适用场景与性能特征,是编写高效设备驱动的关键。
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);
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时),但实际可用值受系统内存碎片程度限制。
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);
| 特性 | kmalloc | kmem_cache |
|---|---|---|
| 对象大小 | 任意(向上取整到2的幂) | 固定大小(创建时指定) |
| 内存利用率 | 可能有内部碎片 | 无内部碎片,完全匹配 |
| 分配/释放速度 | 较快 | 极快(常驻CPU缓存) |
| 构造函数 | 不支持 | 支持(对象首次分配时调用) |
| 调试能力 | 有限 | 支持红区检测、对象毒化等 |
kmem_cache_create 创建专用缓存。这不仅能提高性能,还能通过 slab 调试机制捕获内存越界和释放后使用(use-after-free)错误。
4.5 API选择决策指南
在设备驱动开发中,选择正确的内存分配API直接影响系统稳定性和性能。以下决策树总结了选择依据:
- 是否需要物理连续?
- 是 → 进入第2步
- 否 →
vmalloc(大块)或kmalloc(小块,< 128KB)
- 是否用于DMA?
- 是 →
dma_alloc_coherent或__get_free_pages+ DMA API - 否 → 进入第3步
- 是 →
- 对象大小是否固定且频繁分配?
- 是 →
kmem_cache_alloc(slab缓存) - 否 →
kmalloc或alloc_pages
- 是 →
- 是否需要映射外设寄存器?
- 是 →
ioremap
- 是 →
GFP_ATOMIC 标志,禁止睡眠。进程上下文可以使用 GFP_KERNEL。错误使用 GFP_KERNEL 在原子上下文中会导致内核崩溃(内核调度 while atomic 错误)。