4、内存管理与DMA的雷区:内存泄漏、DMA缓冲区对齐、Cache一致性、内存碎片化
内存管理,说白了就是驱动和硬件之间的“物流系统”。你想想看,数据要从外设搬到系统内存,再从内存搬回去,中间但凡出一点岔子——泄漏、对齐错误、Cache不同步、碎片化——轻则性能暴跌,重则系统直接挂掉。我这些年调试过的驱动,至少有一半的稳定性问题都出在这四个坑里。
嗯,咱们一个一个来拆。
4.1 内存泄漏:看不见的“黑洞”
内存泄漏在驱动里比在应用层更致命。应用层泄漏,进程退出时系统会回收;驱动泄漏?不重启机器,那块内存就永远丢了。
典型场景:每次IO操作都分配一块DMA缓冲区,操作完成后忘了释放。或者中断处理函数里分配了内存,但异常分支没走到free。
核心原则:谁分配,谁释放;分配点和释放点要成对出现,最好写在同一个函数层级里。
我个人习惯在代码里用“资源获取即初始化”的思路。分配内存时,立刻在下一行写好释放的goto标签或者cleanup块。举个例子:
static int my_dev_io(struct my_dev *dev, size_t size)
{
void *buf = NULL;
int ret = 0;
buf = dma_alloc_coherent(dev->dev, size, &dev->dma_handle, GFP_KERNEL);
if (!buf)
return -ENOMEM;
// 业务逻辑...
if (some_error_condition) {
ret = -EIO;
goto err_free_buf; // 异常路径统一释放
}
// 正常路径
dev->buf = buf;
return 0;
err_free_buf:
dma_free_coherent(dev->dev, size, buf, dev->dma_handle);
return ret;
}
小技巧:在模块卸载函数里加一个全局计数器,记录当前分配的缓冲区数量。如果卸载时计数器不为零,打印警告。这个习惯帮我抓到了至少三次泄漏。
4.2 DMA缓冲区对齐:硬件有它的脾气
DMA控制器不是软件,它没那么多灵活性。很多硬件要求缓冲区起始地址必须对齐到某个边界——比如32字节、64字节、甚至1KB。不对齐?数据传输直接错位,或者硬件直接报错。
我在项目中遇到过一块网卡,文档里写着“缓冲区必须16字节对齐”。我偷懒用了kmalloc,结果在某个平台上偶尔丢包。查了两天才发现,kmalloc在大多数情况下返回的地址是8字节对齐的,但16字节对齐并不保证。
解决方案:
- 使用dma_alloc_coherent:它保证返回的地址是cacheline对齐的,通常也满足DMA对齐要求。
- 使用dma_pool:如果你需要大量固定大小的DMA缓冲区,dma_pool可以保证对齐,还能减少碎片。
- 手动对齐:如果必须用kmalloc,可以多分配一些空间,然后手动对齐到目标边界。
| 分配方式 | 对齐保证 | 适用场景 |
|---|---|---|
| kmalloc | 至少8字节(架构相关) | 小数据、非DMA场景 |
| dma_alloc_coherent | Cacheline对齐(通常64字节) | DMA传输、一致性映射 |
| dma_pool | 自定义对齐 | 大量固定大小DMA缓冲区 |
| __get_free_pages | 页对齐(4KB) | 大块连续内存 |
警告:不要假设kmalloc返回的地址能满足DMA对齐要求。除非你明确知道硬件只要求8字节对齐,否则请使用专用的DMA分配接口。
4.3 Cache一致性:CPU和DMA的“信息差”
这是DMA驱动里最隐蔽的坑。CPU有Cache,DMA直接读写内存。如果CPU修改了某个缓冲区,但数据还留在Cache里没写回内存,DMA读到的就是旧数据。反过来,DMA写入了新数据,但CPU的Cache里还存着旧副本,CPU读到的也是错的。
为什么会这样?因为Cache对CPU是透明的,但对DMA不是。DMA控制器看不到Cache,它只认物理内存地址。
解决办法:
- 一致性DMA映射(dma_alloc_coherent):分配时就把Cache和内存同步好,之后CPU和DMA看到的永远是一致的数据。代价是性能稍低,因为每次访问都可能穿透Cache。
- 流式DMA映射(dma_map_single / dma_unmap_single):在DMA传输前手动刷Cache,传输完成后手动无效化Cache。性能更好,但需要开发者精确控制同步点。
我记得有一次调试一个视频采集驱动,画面时不时出现撕裂。查到最后发现是DMA写入了新帧,但CPU读取时Cache里还是旧帧。加上dma_sync_single_for_cpu调用后,问题立刻消失。
经验之谈:如果你不确定用哪种映射方式,优先用一致性映射。等驱动稳定了,再根据性能需求考虑是否切换到流式映射。
4.4 内存碎片化:长时间运行的“慢性病”
内存碎片化在驱动里比在应用层更棘手。应用层有虚拟内存,碎片化大不了多占点页表。驱动里呢?DMA需要物理连续内存。系统跑个几天几周后,物理内存被切成无数小块,你申请一个稍大的连续区域就可能失败。
我曾经维护过一个网络驱动,客户反馈设备运行一周后吞吐量骤降。查日志发现是dma_alloc_coherent频繁失败,驱动退回到了PIO模式。根本原因就是内存碎片化。
应对策略:
- 预分配:在驱动初始化时就分配好所有DMA缓冲区,运行期间不再动态分配。这是最彻底的办法。
- 使用内存池:dma_pool或kmem_cache可以复用固定大小的缓冲区,减少碎片。
- 使用CMA(Contiguous Memory Allocator):内核专门为DMA预留的连续内存区域,可以在系统启动时通过cmdline指定大小。
- 监控碎片化程度:通过/proc/buddyinfo观察内存碎片情况,设置阈值告警。
实用建议:在驱动里加一个定时器,每隔一段时间检查dma_alloc_coherent的成功率。如果失败次数超过阈值,打印告警并尝试触发内存压缩(echo 1 > /proc/sys/vm/compact_memory)。
4.5 知识体系总览
下面这张图把四个雷区的关系串起来了。你可以把它当作调试时的检查清单:
4.6 避坑指南:我曾经踩过的坑
最后分享几个真实案例,希望能帮你少走弯路:
- 案例一:我曾经在中断处理函数里调用了dma_alloc_coherent。中断上下文不能睡眠,而dma_alloc_coherent可能会触发页面回收导致睡眠。结果系统随机死锁。后来改成在中断上半部只做标记,下半部用工作队列来分配内存。
- 案例二:有一次我忘了在DMA传输完成后调用dma_unmap_single。驱动卸载时dma_debug报了一堆“DMA API: device driver tries to free DMA memory it has not allocated”的错误。从那以后我每次写DMA代码,都会在分配后立刻写好对应的释放调用。
- 案例三:一个音频驱动在播放24小时后有杂音。查到最后是dma_pool里的缓冲区被反复分配释放,产生了内部碎片。改用预分配固定数量的缓冲区后,问题解决。
最后提醒:内存和DMA的问题往往不会立刻暴露。它们像定时炸弹,可能在设备运行几天甚至几周后才引爆。所以,在驱动开发阶段就做好压力测试和长时间稳定性测试,比事后排查要高效得多。