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

通过条件:

  1. 日志显示nr_mapped为3(无IOMMU时)或大于0(IOMMU合并后可能小于3);
  2. 总长度等于SG_COUNT × BUF_SIZE = 12288字节,证明所有缓冲区均被正确映射;
  3. 模块加载和卸载均无错误提示,无内存泄漏警告。

关键说明

  • 总长度计算:必须使用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/平台设备。