3. 分散/聚集DMA映射
任务目标
本案例实现一个包含3个独立缓冲区的分散/聚集(SG)列表,通过dma_map_sg()建立IOMMU/总线地址映射,正确计算映射后的总长度,并在DMA传输完成后调用dma_unmap_sg()解除映射。代码以可编译的内核模块形式提供,可直接在目标硬件上加载验证。
核心实现
// sg_dma_map.c — 分散/聚集DMA映射示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/pci.h>
#include <linux/scatterlist.h>
#include <linux/slab.h>
#define SG_COUNT 3
#define BUF_SIZE 4096
static struct device *dev;
static struct scatterlist sg[SG_COUNT];
static void *bufs[SG_COUNT];
static int nr_mapped;
static int __init sg_map_init(void)
{
int i, ret;
size_t total_len = 0;
// 使用平台设备作为示例DMA设备(实际驱动中替换为你的设备)
struct platform_device *pdev = platform_device_register_simple(
"sg-dma-demo", -1, NULL, 0);
if (IS_ERR(pdev))
return PTR_ERR(pdev);
dev = &pdev->dev;
// 分配3个独立缓冲区
for (i = 0; i < SG_COUNT; i++) {
bufs[i] = kmalloc(BUF_SIZE, GFP_KERNEL);
if (!bufs[i]) {
ret = -ENOMEM;
goto err_alloc;
}
}
// 初始化SG列表
sg_init_table(sg, SG_COUNT);
for (i = 0; i < SG_COUNT; i++)
sg_set_buf(&sg[i], bufs[i], BUF_SIZE);
// 建立DMA映射
nr_mapped = dma_map_sg(dev, sg, SG_COUNT, DMA_BIDIRECTIONAL);
if (nr_mapped == 0) {
dev_err(dev, "dma_map_sg failed\n");
ret = -EIO;
goto err_map;
}
// 计算映射后的总长度(使用dma_length,而非sg->length)
for (i = 0; i < nr_mapped; i++)
total_len += sg_dma_len(&sg[i]);
dev_info(dev, "SG映射成功: %d个条目, 总长度=%zu bytes\n",
nr_mapped, total_len);
// 实际DMA传输在此处进行(省略)
// ...
// 解除映射
dma_unmap_sg(dev, sg, SG_COUNT, DMA_BIDIRECTIONAL);
// 清理
for (i = 0; i < SG_COUNT; i++)
kfree(bufs[i]);
platform_device_unregister(pdev);
return 0;
err_map:
for (i = 0; i < SG_COUNT; i++)
kfree(bufs[i]);
platform_device_unregister(pdev);
return ret;
err_alloc:
while (--i >= 0)
kfree(bufs[i]);
platform_device_unregister(pdev);
return ret;
}
static void __exit sg_map_exit(void)
{
/* 所有清理已在init中完成 */
}
module_init(sg_map_init);
module_exit(sg_map_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Scatter/Gather DMA Mapping Example");
编译与加载
# Makefile
obj-m += sg_dma_map.o
# 编译
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# 加载(需要root)
insmod sg_dma_map.ko
# 查看内核日志
dmesg | tail -5
# 卸载
rmmod sg_dma_map
验证方法
加载模块后,执行dmesg应看到类似输出:
sg-dma-demo sg-dma-demo: SG映射成功: 3个条目, 总长度=12288 bytes
通过条件:
- 日志显示
nr_mapped为3(无IOMMU时)或大于0(IOMMU合并后可能小于3); - 总长度等于
SG_COUNT × BUF_SIZE = 12288字节,证明所有缓冲区均被正确映射; - 模块加载和卸载均无错误提示,无内存泄漏警告。
关键说明
- 总长度计算:必须使用
sg_dma_len()而非sg->length,因为IOMMU可能合并或拆分SG条目,只有映射后的dma_len才是硬件实际可见的长度。 - 映射方向:示例使用
DMA_BIDIRECTIONAL,实际驱动应根据传输方向选择DMA_TO_DEVICE或DMA_FROM_DEVICE。 - 解除映射时机:必须在DMA传输完全结束后调用
dma_unmap_sg(),否则可能导致缓存一致性问题或IOMMU页表泄漏。 - 平台设备说明:示例动态注册了一个虚拟平台设备作为DMA设备,实际驱动中应使用
dev指针指向真实的PCI/平台设备。