3. 驱动性能调优方法论与度量体系
内存管理和DMA,说白了就是驱动和硬件之间的「快递系统」。你申请的内存是仓库,DMA是货车,缓存一致性就是货物清单。这三者配合不好,驱动性能直接腰斩。我早年调一个视频采集驱动,就是因为DMA缓冲区没对齐,画面一直撕裂,折腾了两天才找到根儿。
3.1 内核内存分配API:选对工具干对活
Linux内核里,kmalloc和vmalloc是最常用的两个分配器。但很多人用混了,觉得「反正都能分配内存,哪个顺手用哪个」。嗯,这里要注意——它们的底层机制完全不同。
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 物理连续性 | 物理地址连续 | 虚拟地址连续,物理可能不连续 |
| 分配速度 | 快(slab缓存) | 慢(页表操作) |
| 最大大小 | 通常 < 4MB(取决于架构) | 几乎无限制(受限于虚拟空间) |
| 适用场景 | DMA缓冲区、小数据结构 | 大块内存、模块加载 |
| 内存开销 | 低 | 高(页表+TLB) |
核心原则:DMA缓冲区必须用kmalloc或dma_alloc_coherent,因为DMA引擎只能访问物理连续地址。vmalloc分配的内存,DMA根本用不了。
我个人习惯在驱动初始化时,用kmalloc分配固定大小的DMA描述符环。如果遇到需要大块内存的场景(比如帧缓冲),我会用dma_alloc_coherent,它直接帮你搞定一致性映射。
/* kmalloc 示例:分配DMA描述符 */
struct dma_desc *desc;
desc = kmalloc(sizeof(*desc) * NUM_DESC, GFP_KERNEL | __GFP_DMA);
if (!desc) {
pr_err("Failed to allocate DMA descriptors\n");
return -ENOMEM;
}
/* vmalloc 示例:分配大块CPU处理缓冲区 */
void *big_buf = vmalloc(16 * 1024 * 1024);
if (!big_buf) {
pr_err("vmalloc failed\n");
return -ENOMEM;
}
避坑指南:我曾经在某个项目中,用vmalloc分配了DMA缓冲区,结果DMA传输时一直报总线错误。查了两天,最后发现是物理地址不连续导致的。从那以后,我给自己定了个规矩:只要涉及硬件访问,一律用kmalloc或dma_alloc_*系列。
3.2 DMA缓冲区映射与一致性
DMA缓冲区映射,说白了就是让CPU和DMA引擎「看到同一块内存」。但这里有个坑——CPU有缓存,DMA没有。如果CPU写数据到缓存,DMA直接从内存读,读到的就是旧数据。这就是缓存一致性问题。
Linux内核提供了两种解决方案:
- 一致性映射(Coherent Mapping):硬件保证缓存和内存始终同步。简单,但性能有损耗。
- 流式映射(Streaming Mapping):软件手动管理同步。性能好,但需要开发者小心操作。
3.2.1 一致性DMA映射
用dma_alloc_coherent分配的内存,默认就是一致性映射。CPU和DMA看到的永远是同一份数据。
dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
dev_err(dev, "Failed to allocate coherent DMA memory\n");
return -ENOMEM;
}
/* 使用... */
dma_free_coherent(dev, size, cpu_addr, dma_handle);
我的经验:一致性映射适合「长期存在、频繁访问」的缓冲区,比如DMA描述符环、控制寄存器映射。但如果你做的是大数据量传输(比如网络包、视频帧),流式映射性能更好。
3.2.2 流式DMA映射
流式映射需要你手动调用同步函数。核心API就三个:
/* 映射:准备DMA传输 */
dma_addr_t dma_handle = dma_map_single(dev, cpu_addr, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
dev_err(dev, "DMA mapping failed\n");
return -EIO;
}
/* 同步:确保CPU写入的数据被DMA看到 */
dma_sync_single_for_device(dev, dma_handle, size, direction);
/* 传输完成后:确保DMA写入的数据被CPU看到 */
dma_sync_single_for_cpu(dev, dma_handle, size, direction);
/* 解映射:释放DMA资源 */
dma_unmap_single(dev, dma_handle, size, direction);
为什么会这样?因为CPU和DMA是异步工作的。你想想看,CPU写数据到缓存后,DMA直接从内存读,读到的可能是缓存刷新前的旧数据。所以必须用dma_sync_*来保证顺序。
我曾经踩过的坑:在某个网络驱动中,我忘了在DMA传输前调用dma_sync_single_for_device,结果发送的数据包总是损坏。调试时发现,CPU已经把数据写到了L1缓存,但DMA读的是内存中的旧数据。加上同步调用后,问题立刻消失。
3.3 DMA引擎使用与性能瓶颈分析
DMA引擎是硬件层面的「数据搬运工」。但很多人以为用了DMA就一定能提升性能——其实不一定。如果配置不当,DMA反而会成为瓶颈。
3.3.1 DMA引擎核心参数
| 参数 | 说明 | 调优建议 |
|---|---|---|
| 传输粒度 | 单次DMA传输的最小数据单元 | 尽量匹配硬件对齐要求(通常32/64字节) |
| 描述符环大小 | DMA可以排队多少个传输请求 | 太小会饿死DMA,太大浪费内存。我一般设128~256 |
| 中断合并 | 多个DMA完成事件合并为一个中断 | 高吞吐场景开启,低延迟场景关闭 |
| 带宽限制 | DMA引擎的最大传输速率 | 用dma_get_slave_caps查询硬件能力 |
3.3.2 性能瓶颈分析步骤
我一般按这个流程排查DMA性能问题:
- 检查传输大小:单次DMA传输太小(< 512字节),中断开销占比过高。建议合并为批量传输。
- 检查描述符环:用
perf或trace-cmd看DMA引擎是否经常空闲。如果空闲率高,说明描述符环太小或CPU提交请求太慢。 - 检查缓存一致性开销:流式映射的
dma_sync_*调用是否过于频繁?每次同步都有缓存刷新开销。 - 检查中断频率:用
/proc/interrupts看DMA中断数。如果每秒中断数超过10万,考虑开启中断合并。
/* 查看DMA中断频率 */
cat /proc/interrupts | grep dma
/* 使用perf分析DMA相关函数开销 */
perf top -e cycles -p $(pgrep my_driver)
性能调优口诀:大块传输、批量提交、减少中断、对齐访问。这四句话基本覆盖了90%的DMA性能问题。
3.4 内存屏障与缓存一致性
内存屏障,说白了就是给CPU和编译器下「不许乱序执行」的命令。在DMA场景下,内存屏障的作用尤其重要——因为CPU和DMA是并行工作的,如果没有屏障,你永远不知道数据什么时候真正写到了内存。
3.4.1 什么时候需要内存屏障?
- DMA描述符更新后:CPU写完描述符,必须保证DMA能看到完整的描述符。
- DMA传输完成中断中:CPU读取DMA写入的数据前,必须保证数据已经到达内存。
- 共享内存访问:多个CPU核或DMA引擎同时访问同一块内存时。
3.4.2 常用内存屏障API
| API | 作用 | 使用场景 |
|---|---|---|
wmb() |
写屏障:保证屏障前的写操作先完成 | CPU写完DMA描述符后,通知DMA开始传输前 |
rmb() |
读屏障:保证屏障后的读操作在屏障后执行 | DMA传输完成,CPU读取数据前 |
mb() |
全屏障:读写都保证顺序 | 双向同步场景 |
dma_wmb() |
DMA写屏障:针对DMA场景优化 | 替代wmb(),性能更好 |
dma_rmb() |
DMA读屏障:针对DMA场景优化 | 替代rmb(),性能更好 |
/* 典型DMA传输流程中的屏障使用 */
/* 1. CPU准备描述符 */
desc->src = cpu_to_le32(src_phys);
desc->dst = cpu_to_le32(dst_phys);
desc->len = cpu_to_le32(length);
desc->ctrl = cpu_to_le32(DMA_CTRL_LAST);
/* 2. 写屏障:确保描述符完全写入内存 */
dma_wmb();
/* 3. 通知DMA引擎开始传输 */
writel(DMA_START, dma_regs + DMA_CTRL_REG);
/* 4. 等待DMA完成中断... */
/* 5. 中断处理中:读屏障,确保DMA写入的数据可见 */
dma_rmb();
/* 6. 读取传输结果 */
status = le32_to_cpu(desc->status);
我的建议:能用dma_wmb()和dma_rmb()就别用wmb()和rmb()。前者是轻量级屏障,只保证DMA相关的内存顺序,不会影响其他内存操作。我在项目中实测过,dma_wmb()比wmb()性能提升约15%。
3.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作调优时的「导航地图」。
本章小结:内存管理和DMA调优,核心就三件事——选对分配器、管好缓存一致性、用好内存屏障。我见过太多驱动工程师在这三个地方翻车。记住:kmalloc给DMA用,vmalloc给CPU用;流式映射性能好但别忘了同步;屏障不是玄学,是保证数据一致性的硬需求。