第1章:驱动中的内存管理——kmalloc与kzalloc、vmalloc使用场景、内存屏障、DMA一致性映射

大家好,我是你们的老朋友。今天咱们聊聊驱动开发里最基础也最容易翻车的一块——内存管理。

说实话,我刚开始写驱动那会儿,觉得内存分配不就是调个函数嘛,有啥好学的?结果呢,踩坑踩到怀疑人生。后来才明白,内核里的内存管理,跟用户态完全是两码事。你想想看,用户态程序内存不够了可以swap,驱动可不行——你是在跟硬件打交道,内存用错了,系统直接崩给你看。

1. kmalloc与kzalloc:最常用的两个兄弟

先说说kmalloc。这玩意儿跟用户态的malloc有点像,但有个关键区别——它分配的是物理连续的内存。为什么需要物理连续?因为很多硬件设备(比如DMA控制器)访问内存时,要求地址必须是连续的,否则它没法干活。

kmalloc的原型长这样:

#include <linux/slab.h>

void *kmalloc(size_t size, gfp_t flags);

第二个参数flags,我建议你多用GFP_KERNEL。这是最常规的分配标志,意思是“如果内存不够,我可以睡眠等待”。但注意了——如果你在中断上下文、自旋锁保护的临界区里调用kmalloc,千万别用GFP_KERNEL,否则系统会死给你看。这时候得用GFP_ATOMIC,它不会睡眠,但分配失败的概率也更高。

kzalloc呢?说白了就是kmalloc + memset(0)。它把分配的内存全部清零。我个人习惯是:只要分配的内存是用来存结构体、或者后续要给硬件用的,一律用kzalloc。为什么?因为不清零的话,里面残留的垃圾数据可能会让硬件产生莫名其妙的动作。我曾经遇到过一个问题:一个网卡驱动在初始化时忘了清零一个控制结构体,结果每次重启后网卡的行为都不一样,排查了整整两天才发现是内存里有上次残留的脏数据。

核心区别总结:

  • kmalloc:分配内存,内容不确定(可能是上次用过的垃圾数据)
  • kzalloc:分配内存,并自动清零
  • 两者都分配物理连续的内存

2. vmalloc:大块内存的救星

kmalloc虽然好用,但它有个硬伤——分配大块内存时容易失败。因为物理内存是碎片化的,你要分配一个8MB的连续物理块,很可能找不到这么大的空闲区域。

这时候vmalloc就派上用场了。它分配的是虚拟地址连续、物理地址可能不连续的内存。说白了,就是内核帮你把分散的物理页框映射到一个连续的虚拟地址空间里。

#include <linux/vmalloc.h>

void *vmalloc(unsigned long size);

vmalloc适合什么场景?我举个例子:你要给一个帧缓冲设备分配显存,或者给一个加密模块分配临时工作缓冲区。这些场景下,硬件并不要求物理连续,只要软件能通过一个连续的虚拟地址访问就行。

但注意了,vmalloc有性能开销。每次访问vmalloc分配的内存,都会触发页表遍历。所以高频访问的场景,别用vmalloc。我见过有人把DMA缓冲区用vmalloc分配,结果数据传输速率直接腰斩——因为每次DMA完成中断后,CPU去读数据都要走一遍页表,慢得离谱。

我的选择原则:

  • 小内存(< 128KB)、需要物理连续 → kmalloc/kzalloc
  • 大内存(> 128KB)、不需要物理连续 → vmalloc
  • DMA缓冲区 → 必须物理连续,用kmalloc或DMA专用API

3. 内存屏障:别让编译器坑了你

内存屏障这个话题,很多新手觉得玄乎。其实说白了,就是告诉编译器和CPU:“别给我乱排序!”

为什么会需要这个?因为现代编译器和CPU为了优化性能,会重新安排指令的执行顺序。在单线程程序里这没问题,但在驱动里——你想想看,你往一个寄存器里写了个值,然后立刻去读另一个寄存器的状态。如果编译器把读操作挪到了写操作前面,那你读到的就是旧值,逻辑全乱套了。

内核提供了几种内存屏障:

// 通用屏障
mb()    // 读写屏障
rmb()   // 读屏障
wmb()   // 写屏障

// 带volatile的屏障
READ_ONCE(x)
WRITE_ONCE(x, val)

我个人的经验是:跟硬件寄存器打交道时,一定要用内存屏障。比如你写一个DMA启动寄存器,然后等待DMA完成标志位被硬件置位。如果不加屏障,编译器可能把“读取完成标志”这条指令优化到“写启动寄存器”之前——那你就永远等不到DMA完成了。

避坑指南:我曾经在移植一个SPI控制器驱动时,发现数据总是错位。调试了三天,最后发现是写FIFO之后没有加wmb(),导致CPU写入的数据还没真正到达硬件,下一个数据又写进去了。加上wmb()之后,问题立刻消失。

4. DMA一致性映射:让CPU和DMA看到同一份数据

DMA是驱动开发里最复杂的部分之一,而内存一致性问题是DMA的“头号杀手”。

简单来说,CPU有缓存(Cache),DMA控制器没有。当DMA把数据写入内存时,如果CPU的Cache里还保留着这块内存的旧副本,那CPU读到的就是脏数据——明明DMA已经写入了新数据,CPU却看不到。

解决这个问题有两种方式:

方式 API 特点
一致性映射 dma_alloc_coherent() 分配时就保证CPU和DMA看到的数据一致,适合长期使用的缓冲区
流式映射 dma_map_single() / dma_unmap_single() 临时映射,需要手动管理Cache一致性,适合短时传输

我建议:能用一致性映射就用一致性映射。虽然它可能会禁用Cache(导致访问速度稍慢),但胜在简单可靠。流式映射虽然性能更好,但需要你在每次DMA传输前后手动调用Cache刷新函数,稍有不慎就会出问题。

// 一致性映射示例
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

// 使用完后释放
dma_free_coherent(dev, size, cpu_addr, dma_handle);

注意dma_alloc_coherent返回两个地址:cpu_addr是CPU视角的虚拟地址,dma_handle是DMA视角的总线地址。这两个地址指向同一块物理内存,但视角不同。

关键点:DMA一致性映射分配的内存,CPU和DMA控制器看到的永远是同一份数据。不需要你手动做任何Cache同步操作。这是它最大的优势。

小结

嗯,这一章的内容就到这里。内存管理是驱动开发的基石,kmalloc/kzalloc、vmalloc、内存屏障、DMA映射,这四个知识点你如果吃透了,大部分驱动的内存问题你都能应付。

下一章我们会聊聊设备树——这是现代Linux驱动开发绕不开的话题。到时候我会分享一些我在设备树解析上踩过的坑,保证让你少走弯路。

记住:驱动开发没有银弹,但扎实的基础知识能让你在遇到问题时,知道从哪里下手。

公众号:蓝海资料掘金营,微信deep3321