DMA-BUF 由浅入深(三) —— map attachment

作者 by adtxl / 2022-02-22 / 暂无评论 / 329 个足迹

版权声明:本文为CSDN博主「何小龙」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/hexiaolong2009/article/details/102596772

1. 前言

在上一篇《dma-buf 由浅入深(二)—— kmap/vmap》中,我们学习了如何使用 CPU 在 kernel 空间访问 dma-buf 物理内存,但通常这种操作方法在内核中出现的频率并不高,因为 dma-buf 设计之初就是为满足那些大内存访问需求的硬件而设计的,如GPU/DPU。在这种场景下,如果使用CPU直接去访问 memory,那么性能会大大降低。因此,dma-buf 在内核中出现频率最高的还是它的 dma_buf_attach()dma_buf_map_attachment() 接口。本篇我们就一起来学习如何通过这两个 API 来实现 DMA 硬件对 dma-buf 物理内存的访问。

2. DMA Access

dma-buf 提供给 DMA 硬件访问的 API 主要就两个:

  • dma_buf_attach()
  • dma_buf_map_attachment()

这两个接口调用有严格的先后顺序,必须先 attach,再 map attachment,因为后者的参数是由前者提供的,所以通常这两个接口形影不离。

与上面两个 API 相对应的反向操作接口为: dma_buf_dettach()dma_buf_unmap_attachment(),具体我就不细说了。

3. sg_table

由于 DMA 操作涉及到内核中 dma-mapping 诸多接口及概念,本篇为避重就轻,无意讲解。但 sg_table 的概念必须要提一下,因为它是 dma-buf 供 DMA 硬件访问的终极目标,也是 DMA 硬件访问离散 memory 的唯一途径!

sg_table 本质上是由一块块单个物理连续的 buffer 所组成的链表,但是这个链表整体上看却是离散的,因此它可以很好的描述从 高端内存 上分配出的离散 buffer。当然,它同样可以用来描述从 低端内存 上分配出的物理连续 buffer。

如下图所示:

image5120031b1e3b50a1.png

sg_table 代表着整个链表,而它的每一个链表项则由 scatterlist 来表示。因此,1个scatterlist 也就对应着一块 物理连续 的 buffer。我们可以通过如下接口来获取一个 scatterlist 对应 buffer 的物理地址和长度:

  • sg_dma_address(sgl)
  • sg_dma_len(sgl)

有了 buffer 的物理地址和长度,我们就可以将这两个参数配置到 DMA 硬件寄存器中,这样就可以实现 DMA 硬件对这一小块 buffer 的访问。那如何访问整块离散 buffer 呢?当然是用个 for 循环,不断的解析 scatterlist,不断的配置 DMA 硬件寄存器啦!

对于现代多媒体硬件来说,IOMMU 的出现,解决了程序员编写 for 循环的烦恼。因为在 for 循环中,每次配置完 DMA 硬件寄存器后,都需要等待本次 DMA 传输完毕,然后才能进行下一次循环,这大大降低了软件的执行效率。而 IOMMU 的功能就是用来解析 sg_table 的,它会将 sg_table 内部一个个离散的小 buffer 映射到自己内部的设备地址空间,使得这整块 buffer 在自己内部的设备地址空间上是连续的。这样,在访问离散 buffer 的时候,只需要将 IOMMU 映射后的设备地址(与 MMU 映射后的 CPU 虚拟地址不是同一概念)和整块 buffer 的 size 配置到 DMA 硬件寄存器中即可,中途无需再多次配置,便完成了 DMA 硬件对整块离散 buffer 的访问,大大的提高了软件的效率。

4. dma_buf_attach()

该函数实际是 “dma-buf attach device” 的缩写,用于建立一个 dma-buf 与 device 的连接关系,这个连接关系被存放在新创建的 dma_buf_attachment 对象中,供后续调用 dma_buf_map_attachment() 使用。

该函数对应 dma_buf_ops 中的 attach 回调接口,如果 device 对后续的 map attachment 操作没有什么特殊要求,可以不实现。

5. dma_buf_map_attachment()

该函数实际是 “dma-buf map attachment into sg_table” 的缩写,它主要完成2件事情:

  1. 生成 sg_table
  2. 同步 Cache

选择返回 sg_table 而不是物理地址,是为了兼容所有 DMA 硬件(带或不带 IOMMU),因为 sg_table 既可以表示连续物理内存,也可以表示非连续物理内存。

同步 Cache 是为了防止该 buffer 事先被 CPU 填充过,数据暂存在 Cache 中而非 DDR 上,导致 DMA 访问的不是最新的有效数据。通过将 Cache 中的数据回写到 DDR 上可以避免此类问题的发生。同样的,在 DMA 访问内存结束后,需要将 Cache 设置为无效,以便后续 CPU 直接从 DDR 上(而非 Cache 中)读取该内存数据。通常我们使用如下流式 DMA 映射接口来完成 Cache 的同步:

  • dma_map_single() / dma_unmap_single()
  • dma_map_page() / dma_unmap_page()
  • dma_map_sg() / dma_unmap_sg()

关于更多 dma_map_*() 函数的说明,推荐大家阅读 落尘纷扰 的博客《Linux内存管理 —— DMA和一致性缓存》,言简意赅,适合初学者。

dma_buf_map_attachment() 对应 dma_buf_ops 中的 map_dma_buf 回调接口,如第一篇《最简单的 dma-buf 驱动程序》所述,该回调接口(包括 unmap_dma_buf 在内)被强制要求实现,否则 dma_buf_export() 将执行失败。

6. 为什么需要 attach 操作 ?

同一个 dma-buf 可能会被多个 DMA 硬件访问,而每个 DMA 硬件可能会因为自身硬件能力的限制,对这块 buffer 有自己特殊的要求。比如硬件 A 的寻址能力只有0x0 ~ 0x10000000,而硬件 B 的寻址能力为 0x0 ~ 0x80000000,那么在分配 dma-buf 的物理内存时,就必须以硬件 A 的能力为标准进行分配,这样硬件 A 和 B 都可以访问这段内存。否则,如果只满足 B 的需求,那么 A 可能就无法访问超出 0x10000000 地址以外的内存空间,道理其实类似于木桶理论。

因此,attach 操作可以让 exporter 驱动根据不同的 device 硬件能力,来分配最合适的物理内存。

通过设置 device->dma_params 参数,来告知 exporter 驱动该 DMA 硬件的能力限制。

但是在上一篇的示例中,dma-buf 的物理内存都是在 dma_buf_export() 的时候就分配好了的,而 attach 操作只能在 export 之后才能执行,那我们如何确保已经分配好的内存是符合硬件能力要求的呢?这就引出了下面的话题。

7. 何时分配内存?

答案是:既可以在 export 阶段分配,也可以在 map attachment 阶段分配,甚至可以在两个阶段都分配,这通常由 DMA 硬件能力来决定。

首先,驱动人员需要统计当前系统中都有哪些 DMA 硬件要访问 dma-buf;
然后,根据不同的 DMA 硬件能力,来决定在何时以及如何分配物理内存。

通常的策略如下(假设只有 A、B 两个硬件需要访问 dma-buf ):

  • 如果硬件 A 和 B 的寻址空间有交集,则在 export 阶段进行内存分配,分配时以 A / B 的交集为准;
  • 如果硬件 A 和 B 的寻址空间没有交集,则只能在 map attachment 阶段分配内存。

对于第二种策略,因为 A 和 B 的寻址空间没有交集(即完全独立),所以它们实际上是无法实现内存共享的。此时的解决办法是: A 和 B 在 map attachment 阶段,都分配各自的物理内存,然后通过 CPU 或 通用DMA 硬件,将 A 的 buffer 内容拷贝到 B 的 buffer 中去,以此来间接的实现 buffer “共享”。

另外还有一种策略,就是不管三七二十一,先在 export 阶段分配好内存,然后在首次 map attachment 阶段通过 dma_buf->attachments 链表,与所有 device 的能力进行一一比对,如果满足条件则直接返回 sg_table;如果不满足条件,则重新分配符合所有 device 要求的物理内存,再返回新的 sg_table

关于更多 dma_buf_map_attachment() 的说明,详见参考资料中的 ELCE-DMABUF.pdf 文档。

8. 示例代码

本示例基于第一篇的 exporter-dummy.c 进行修改,对 dma_buf_ops 中的 attach 和 map_dma_buf 回调接口进行实现。当然,为了方便演示,我们仍然像之前那样,在 exporter_alloc_page() 中事先分配好了 dma-buf 的物理内存。

exporter-sg.c

#include <linux/dma-buf.h>
#include <linux/module.h>
#include <linux/slab.h>

struct dma_buf *dmabuf_exported;
EXPORT_SYMBOL(dmabuf_exported);

static int exporter_attach(struct dma_buf *dmabuf, struct device *dev,
            struct dma_buf_attachment *attachment)
{
    pr_info("dmabuf attach device: %s\n", dev_name(dev));
    return 0;
}

static void exporter_detach(struct dma_buf *dmabuf, struct dma_buf_attachment *attachment)
{
    pr_info("dmabuf detach device: %s\n", dev_name(attachment->dev));
}

static struct sg_table *exporter_map_dma_buf(struct dma_buf_attachment *attachment,
                     enum dma_data_direction dir)
{
    void *vaddr = attachment->dmabuf->priv;
    struct sg_table *table;

    table = kmalloc(sizeof(*table), GFP_KERNEL);

    sg_alloc_table(table, 1, GFP_KERNEL);
    sg_dma_len(table->sgl) = PAGE_SIZE;
    sg_dma_address(table->sgl) = dma_map_single(NULL, vaddr, PAGE_SIZE, dir);

    return table;
}

static void exporter_unmap_dma_buf(struct dma_buf_attachment *attachment,
                   struct sg_table *table,
                   enum dma_data_direction dir)
{
    dma_unmap_single(NULL, sg_dma_address(table->sgl), PAGE_SIZE, dir);
    sg_free_table(table);
    kfree(table);
}

...

static const struct dma_buf_ops exp_dmabuf_ops = {
    .attach = exporter_attach,
    .detach = exporter_detach,
    .map_dma_buf = exporter_map_dma_buf,
    .unmap_dma_buf = exporter_unmap_dma_buf,
    ...
};

static struct dma_buf *exporter_alloc_page(void)
{
    DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
    struct dma_buf *dmabuf;
    void *vaddr;

    vaddr = kzalloc(PAGE_SIZE, GFP_KERNEL);

    exp_info.ops = &exp_dmabuf_ops;
    exp_info.size = PAGE_SIZE;
    exp_info.flags = O_CLOEXEC;
    exp_info.priv = vaddr;

    dmabuf = dma_buf_export(&exp_info);

    sprintf(vaddr, "hello world!");

    return dmabuf;
}

static int __init exporter_init(void)
{
    dmabuf_exported = exporter_alloc_page();
    return 0; 
}

module_init(exporter_init);

在上面的 attach 实现中,我们仅仅只是打印了一句 log,其他什么事情也不做。在 map_dma_buf 实现中,我们构造了一个 sg_table 对象,并通过调用 dma_map_single() 来获取 dma_addr 以及实现 Cache 同步操作。

importer-sg.c

#include <linux/device.h>
#include <linux/dma-buf.h>
#include <linux/module.h>
#include <linux/slab.h>

extern struct dma_buf *dmabuf_exported;

static int importer_test(struct dma_buf *dmabuf)
{
    struct dma_buf_attachment *attachment;
    struct sg_table *table;
    struct device *dev;
    unsigned int reg_addr, reg_size;

    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    dev_set_name(dev, "importer");

    attachment = dma_buf_attach(dmabuf, dev);
    table = dma_buf_map_attachment(attachment, DMA_BIDIRECTIONAL);

    reg_addr = sg_dma_address(table->sgl);
    reg_size = sg_dma_len(table->sgl);
    pr_info("reg_addr = 0x%08x, reg_size = 0x%08x\n", reg_addr, reg_size);

    dma_buf_unmap_attachment(attachment, table, DMA_BIDIRECTIONAL);
    dma_buf_detach(dmabuf, attachment);

    return 0;
}

static int __init importer_init(void)
{
    return importer_test(dmabuf_exported);
}

module_init(importer_init);

示例描述:

  1. exporter 通过 kzalloc 分配了一个 PAGE 大小的物理连续 buffer;
  2. importer 驱动通过 extern 关键字导入了 exporter 的 dma-buf,并通过 dma_buf_map_attachment() 接口获取到了该物理内存所对应的 sg_table,然后将该 sg_table 中的 address 和 size 解析到 reg_addrreg_size 这两个虚拟寄存器中。

9. 运行

在 my-qemu 仿真环境中执行如下命令:

# insmod /lib/modules/4.14.143/kernel/drivers/dma-buf/exporter-sg.ko
# insmod /lib/modules/4.14.143/kernel/drivers/dma-buf/importer-sg.ko

将看到如下打印结果:

dmabuf attach device: importer
reg_addr = 0x7f6ee000, reg_size = 0x00001000
dmabuf detach device: importer

注意:执行 lsmod 命令时,必须先加载 exporter-sg.ko,后加载 importer-sg.ko,否则将出现符号依赖错误。

10. 总结

  1. sg_table 是 DMA 硬件操作的关键;
  2. attach 的目的是为了让后续 map attachment 操作更灵活;
  3. map attachment 主要完成两件事:生成 sg_table 和 Cache 同步;
  4. DMA 的硬件能力决定了 dma-buf 物理内存的分配时机;

通过本篇,我们学习了如何通过 dma_buf_attach()dma_buf_map_attachment() 来实现 DMA 硬件对 dma-buf 的访问。在下一篇,我们将一起来学习如何在 userspace 来访问 dma-buf 的物理内存。

独特见解