4. 内核内存分配:kmalloc、vmalloc、kmap、ioremap、DMA内存分配、原子上下文分配、内存分配GFP标志

内存分配,是驱动开发里最基础也最容易翻车的环节。我见过不少新手,上来就 kmalloc 一把梭,结果在中断上下文里睡死过去,或者分配了物理不连续的内存给 DMA 用,数据直接乱掉。这一章,我把常用的几种分配方式掰开揉碎讲清楚。

4.1 分配家族总览

先看一张总图,把几个核心 API 的定位理清:

内核内存分配 API 全景 用户态:malloc / mmap / brk 内核态内存分配 API kmalloc 物理连续,≤ 4MB vmalloc 虚拟连续,物理离散 __get_free_pages 页分配器,2^n 页 kmap / kmap_atomic 高端内存映射 ioremap IO 内存映射 dma_alloc_coherent DMA 一致性内存 💡 选择原则 小对象、物理连续 → kmalloc 大对象、无需物理连续 → vmalloc DMA 设备 → dma_alloc_coherent

4.2 kmalloc —— 最常用的小块分配

kmalloc 分配的是物理连续的内存,底层走 slab 分配器。适合分配几百字节到几 MB 的小对象。

void *kmalloc(size_t size, gfp_t flags);
void kfree(const void *objp);

我个人的习惯是:能用 kmalloc 就不用 vmalloc。物理连续意味着 cache 友好,DMA 也能直接用。但注意,kmalloc 最大能分配的大小受 slab 缓存限制,一般不超过 4MB。想分配更大的?要么用 __get_free_pages,要么上 vmalloc。

经验之谈: 我在调试一个网络驱动时,发现 kmalloc 分配 64KB 以上经常失败,后来改用 vmalloc 才稳定。但 vmalloc 有 TLB 压力,高频路径上慎用。

4.3 vmalloc —— 大块虚拟连续

vmalloc 分配的是虚拟地址连续、物理地址不连续的内存。底层通过分配物理页并建立页表映射实现。

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

说白了,vmalloc 就是内核版的 malloc。它适合分配大块内存(比如几 MB 到上百 MB),但每次分配都要修改页表,开销比 kmalloc 大得多。我建议只在模块加载、配置读取等慢速路径上用。

注意: vmalloc 分配的内存不能直接给 DMA 用!DMA 需要物理连续地址,vmalloc 的物理页是离散的。我曾经见过有人把 vmalloc 地址传给 DMA 描述符,结果数据写到错误的位置,排查了两天才发现。

4.4 kmap 与 kmap_atomic —— 高端内存映射

在 32 位系统上,内核直接映射区只有 1GB(ZONE_NORMAL),超过 896MB 的物理内存属于高端内存(ZONE_HIGHMEM),不能直接访问。这时候就需要 kmap 或 kmap_atomic 临时建立映射。

void *kmap(struct page *page);
void kunmap(struct page *page);

void *kmap_atomic(struct page *page);
void kunmap_atomic(void *addr);

kmap 可能睡眠(因为要获取映射槽),所以不能在中断上下文用。而 kmap_atomic 是原子的,它用固定的预留映射槽,不会睡眠。但 kmap_atomic 的映射是 CPU 本地的,不能跨 CPU 访问。

64 位系统上基本用不到: 64 位内核的线性映射区足够大,所有物理内存都在直接映射范围内,kmap 基本成了空操作。但为了代码可移植性,驱动里还是建议用 page_address 或 kmap 来获取页的虚拟地址。

4.5 ioremap —— IO 内存映射

外设的寄存器或内存通常映射在物理地址空间的高端(比如 0xF0000000 以上)。内核不能直接访问这些物理地址,必须通过 ioremap 将其映射到内核虚拟地址空间。

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

ioremap 返回的地址要用 readl/writel 等访问函数来读写,不能直接解引用。为什么?因为有些架构对 IO 内存有特殊的访问语义(比如内存屏障、字节序转换)。

void __iomem *regs;
regs = ioremap(0xF0000000, 0x1000);
if (!regs) {
    pr_err("ioremap failed\n");
    return -ENOMEM;
}
writel(0x1, regs + 0x10);  // 写寄存器
iounmap(regs);
避坑指南: 我曾经在 ioremap 后直接用了 memcpy,结果在 ARM 上触发对齐异常。记住:ioremap 的内存必须用 readl/writel 系列函数,它们内部会处理对齐和屏障。

4.6 DMA 内存分配

DMA 设备需要物理连续的内存,而且要求地址在设备可寻址范围内。内核提供了专门的 API:

void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t flag);
void dma_free_coherent(struct device *dev, size_t size,
                       void *cpu_addr, dma_addr_t dma_handle);

这个函数分配的内存是 cache 一致的——CPU 和 DMA 控制器看到的数据是一致的,不需要手动刷 cache。底层可能使用 CMA(Contiguous Memory Allocator)来保证大块连续物理内存。

API 物理连续性 Cache 一致性 典型用途
kmalloc 连续 需手动维护 小对象、驱动内部数据
vmalloc 不连续 需手动维护 大块数据、模块加载
dma_alloc_coherent 连续 硬件保证 DMA 缓冲区
__get_free_pages 连续 需手动维护 页级分配、DMA(配合 dma_map)
重要: dma_alloc_coherent 可能睡眠,不能在中断上下文调用。如果必须在原子上下文中分配 DMA 内存,考虑使用 dma_pool 或预分配。

4.7 原子上下文分配

在中断处理函数、软中断、spinlock 保护的临界区中,分配内存不能睡眠。这时候必须使用 GFP_ATOMIC 标志。

void *ptr = kmalloc(size, GFP_ATOMIC);
if (!ptr) {
    // 处理分配失败
}

GFP_ATOMIC 分配会从内存紧急预留池中取,不会触发页面回收或 IO 操作,因此不会睡眠。但代价是分配成功率较低,尤其是在内存紧张时。

我的建议: 原子上下文里尽量少做动态分配。如果必须做,提前在初始化时预分配好内存池,运行时从池里取。我在一个网卡驱动的 NAPI 回调里就是这么干的,避免了 GFP_ATOMIC 失败导致的丢包。

4.8 GFP 标志详解

GFP(Get Free Pages)标志控制内存分配的行为。常用的标志组合如下:

标志 含义 能否睡眠 典型场景
GFP_KERNEL 普通内核分配 可以 进程上下文
GFP_ATOMIC 原子分配,不睡眠 不能 中断、spinlock 内
GFP_DMA 从 ZONE_DMA 分配 取决于组合 DMA 缓冲区(16MB 以下)
GFP_HIGHUSER 允许高端内存 可以 用户空间页面
GFP_NOIO 不触发 IO 可以 块设备层
GFP_NOFS 不触发文件系统操作 可以 文件系统内部

标志可以按位或组合使用,比如 GFP_KERNEL | GFP_DMA 表示从 ZONE_DMA 分配且允许睡眠。

调试技巧: 如果怀疑内存分配失败,可以打开内核的分配跟踪:echo 1 > /proc/sys/vm/oom_dump_tasks,或者用 trace_mm_page_alloc 跟踪点。我调试一个 OOM 问题时,就是靠 tracepoint 定位到某个驱动在原子上下文里分配了 1MB 内存。

4.9 总结:选型决策流

最后给一个简单的决策流程,帮你快速选择:

  1. 是否需要物理连续? 是 → 走 kmalloc 或 __get_free_pages;否 → 走 vmalloc
  2. 是否给 DMA 用? 是 → 走 dma_alloc_coherent 或 dma_map_single
  3. 是否在原子上下文? 是 → 必须用 GFP_ATOMIC
  4. 是否访问 IO 内存? 是 → 走 ioremap
  5. 是否操作高端内存页? 是 → 走 kmap/kmap_atomic

嗯,这一章的内容就到这里。记住:选对 API 比写对代码更重要。分配错了,后面怎么调都白搭。