4. 内核内存分配:kmalloc、vmalloc、kmap、ioremap、DMA内存分配、原子上下文分配、内存分配GFP标志
内存分配,是驱动开发里最基础也最容易翻车的环节。我见过不少新手,上来就 kmalloc 一把梭,结果在中断上下文里睡死过去,或者分配了物理不连续的内存给 DMA 用,数据直接乱掉。这一章,我把常用的几种分配方式掰开揉碎讲清楚。
4.1 分配家族总览
先看一张总图,把几个核心 API 的定位理清:
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。
4.3 vmalloc —— 大块虚拟连续
vmalloc 分配的是虚拟地址连续、物理地址不连续的内存。底层通过分配物理页并建立页表映射实现。
void *vmalloc(unsigned long size);
void vfree(const void *addr);
说白了,vmalloc 就是内核版的 malloc。它适合分配大块内存(比如几 MB 到上百 MB),但每次分配都要修改页表,开销比 kmalloc 大得多。我建议只在模块加载、配置读取等慢速路径上用。
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 访问。
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);
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) |
4.7 原子上下文分配
在中断处理函数、软中断、spinlock 保护的临界区中,分配内存不能睡眠。这时候必须使用 GFP_ATOMIC 标志。
void *ptr = kmalloc(size, GFP_ATOMIC);
if (!ptr) {
// 处理分配失败
}
GFP_ATOMIC 分配会从内存紧急预留池中取,不会触发页面回收或 IO 操作,因此不会睡眠。但代价是分配成功率较低,尤其是在内存紧张时。
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 总结:选型决策流
最后给一个简单的决策流程,帮你快速选择:
- 是否需要物理连续? 是 → 走 kmalloc 或 __get_free_pages;否 → 走 vmalloc
- 是否给 DMA 用? 是 → 走 dma_alloc_coherent 或 dma_map_single
- 是否在原子上下文? 是 → 必须用 GFP_ATOMIC
- 是否访问 IO 内存? 是 → 走 ioremap
- 是否操作高端内存页? 是 → 走 kmap/kmap_atomic
嗯,这一章的内容就到这里。记住:选对 API 比写对代码更重要。分配错了,后面怎么调都白搭。