DMA-BUF 由浅入深(六) —— begin / end cpu_access

作者 by adtxl / 2022-02-23 / 暂无评论 / 457 个足迹

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

1. 前言

本篇我们将一起来学习 dma-buf 用于 Cache 同步操作的 begin_cpu_access 和 end_cpu_access 这两个接口。之所以将这两个接口放在第六篇讲解,是因为它们在内核中的使用频率并不高,只有在特殊场景下才派的上用场。

Cache 一致性
下图显示了 CPU 与 DMA 访问 DDR 之间的区别:

image3d5ddc01627a9fb7.png

可以看到,CPU 在访问内存时是要经过 Cache 的,而 DMA 外设则是直接和 DDR 打交道,因此这就存在 Cache 一致性的问题了,即 Cache 里面的数据是否和 DDR 里面的数据保持一致。比如 DMA 外设早已将 DDR 中的数据改写了,而 CPU 却浑然不知,仍然在访问 Cache 里面暂存的旧数据。

所以 Cache 一致性问题,只有在 CPU 参与访问的情况下才会发生。如果一个 dma-buf 自始自终都只被一个硬件访问(要么CPU,要么DMA),那么 Cache 一致性问题就不会存在。

当然,如果一个 dma-buf 所对应的物理内存本身就是 Uncache 的(也叫一致性内存),或者说该 buffer 在被分配时是以 coherent 方式分配的,那么这种情况下,CPU 是不经过 cache 而直接访问 DDR 的,自然 Cache 一致性问题也就不存在了。

有关更多 Cache 一致性的问题,推荐阅读宋宝华的文章《关于DMA ZONE和dma alloc coherent若干误解的彻底澄清》,本文不做赘述。

2. 为什么需要 begin / end 操作?

在前面的《dma-buf 由浅入深(三) —— map attachment》文章中,我们了解到 dma_buf_map_attachment() 函数的一个重要功能,那就是同步 Cache 操作。但是该函数通常使用的是 dma_map_{single,sg} 这种流式 DMA 映射接口来实现 Cache 同步操作,这类接口的特点就是 Cache 同步只是一次性的,即在 dma map 的时候执行一次 Cache Flush 操作,在 dma unmap 的时候执行一次 Cache Invalidate 操作,而这中间的过程是不保证 Cache 和 DDR 上数据的一致性的。因此如果 CPU 在 dma map 和 unmap 之间又去访问了这块内存,那么有可能 CPU 访问到的数据就只是暂存在 Cache 中的旧数据,这就带来了问题。

那么什么情况下会出现 CPU 在 dma map 和 unmap 期间又去访问这块内存呢?一般不会出现 DMA 硬件正在传输过程中突然 CPU 发起访问的情况,而更多的是在 DMA 硬件发起传输之前,或 DMA 硬件传输完成之后,并且仍然处于 dma map 和 unmap 操作之间的时候,CPU 对这段内存发起了访问。下面举2个例子:

  1. 这是内核文档 DMA-API-HOWTO.txt 中描述的一个网卡驱动例子,非常经典。网卡驱动首先通过 dma_map_single() 将接收缓冲区映射给了网卡 DMA 硬件,此后便发起了 DMA 传输请求,等待网卡接收数据完成。当网卡接收完数据后,会触发中断,此时网卡驱动需要在中断里检查本次传输数据的有效性。如果是有效数据,则调用 dma_unmap_single() 结束本次 DMA 映射;如果不是,则丢弃本次数据,继续等待下一次 DMA 接收的数据。在这个过程中,检查数据有效性是通过 CPU 读取接收缓冲区中的包头来实现的,也只有在数据检查完成后,才能决定是否执行 dma_unmap_single() 操作。因此这里出现了 dma map 和 unmap 期间 CPU 要访问这段内存的需求。
  2. 这是在显示系统中遇到的一个 SPI 屏的例子,也很常见。通常 SPI 屏对总线上传输数据的字节序有严格要求,比如 16bit RGB565 屏幕,要求发送图像数据时,必须先发送高8bit,再发送低8bit。如果平台 SoC SPI 控制器的 DMA 通道只能以byte为单位从低地址向高地址顺序访问,那么它发送出去的数据顺序只能是低8bit在前,高8bit在后,那么就不能满足外设 LCD 的要求,所以需要软件在 SPI 发起传输之前,将显存中的字节序交换一下,此时便涉及到 CPU 访问的需求。也就是说,DRM GEM 驱动首先拿到了 GPU 绘制完成的 buffer,然后对它进行 dma_map_single() 操作,当这块 buffer 交到 CRTC 驱动手里的时候,CPU 需要对该 buffer 再做个字节序交换,然后才送给 SPI DMA,待 DMA 传输完成后执行 dma_unmap_single() 操作。因此这里也出现了 dma map 和 unmap 期间 CPU 要访问这段内存的需求。

以上第一个例子是 CPU 在 DMA 传输后发起访问,第二个例子是在 DMA 传输前发起访问。针对这种情况,就需要在 CPU 访问内存前,先将 DDR 数据同步到 Cache 中(Invalidate);在 CPU 访问结束后,将 Cache 中的数据回写到 DDR 上(Flush),以便 DMA 能获取到 CPU 更新后的数据。这也就是 dma-buf 给我们预留 {begin,end}_cpu_access 的原因。

3. Kernel API

dma-buf 为我们提供了如下内核 API,用来在 dma map 期间发起 CPU 访问操作:

  • dma_buf_begin_cpu_access()
  • dma_buf_end_cpu_access()

它们分别对应 dma_buf_ops 中的 begin_cpu_accessend_cpu_access 回调接口。

通常在驱动设计时, begin_cpu_access / end_cpu_access 使用如下流式 DMA 接口来实现 Cache 同步:

  1. dma_sync_single_for_cpu() / dma_sync_single_for_device()
  2. dma_sync_sg_for_cpu() / dma_sync_sg_for_device()

CPU 访问内存之前,通过调用 dma_sync_{single,sg}_for_cpu() 来 Invalidate Cache,这样 CPU 在后续访问时才能重新从 DDR 上加载最新的数据到 Cache 上。
CPU 访问内存结束之后,通过调用 dma_sync_{single,sg}_for_device() 来 Flush Cache,将 Cache 中的数据全部回写到 DDR 上,这样后续 DMA 才能访问到正确的有效数据。

关于更多流式 DMA 映射的介绍,推荐阅读 wowotech 翻译的《Dynamic DMA mapping Guide》文章。

4. User API

考虑到 mmap() 操作,dma-buf 也为我们提供了 Userspace 的同步接口,通过 DMA_BUF_IOCTL_SYNC ioctl() 来实现。该 cmd 需要一个 struct dma_buf_sync 参数,用于表明当前是 begin 还是 end 操作,是 read 还是 write 操作。

dma-buf: Add ioctls to allow userspace to flush

常用写法如下:

struct dma_buf_sync sync = { 0 };

sync.flags = DMA_BUF_SYNC_RW | DMA_BUF_SYNC_START;
ioctl(dmabuf_fd, DMA_BUF_IOCTL_SYNC, &sync);

// execute cpu access, for example: memset() ...

sync.flags = DMA_BUF_SYNC_RW | DMA_BUF_SYNC_END;
ioctl(dmabuf_fd, DMA_BUF_IOCTL_SYNC, &sync);

5. 示例

本示仅仅用于演示 dma-buf begin / end API 的调用方法,并未考虑真实使用场景的可靠性,各位读者心领神会即可。

5.1 exporter 驱动

我们基于《dma-buf 由浅入深(四) —— mmap》中的示例一 exporter-fd.c 文件进行修改,新增 begin_cpu_access 和 end_cpu_access 回调接口,并调用 dma_sync_singlefor{cpu,device} 来完成 Cache 的同步。

exporter-sync.c

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

struct dma_buf *dmabuf_exported;
EXPORT_SYMBOL(dmabuf_exported);

...

static int exporter_begin_cpu_access(struct dma_buf *dmabuf,
                      enum dma_data_direction dir)
{
    dma_addr_t dma_addr = virt_to_phys(dmabuf->priv);

    dma_sync_single_for_cpu(NULL, dma_addr, PAGE_SIZE, dir);

    return 0;
}

static int exporter_end_cpu_access(struct dma_buf *dmabuf,
                enum dma_data_direction dir)
{
    dma_addr_t dma_addr = virt_to_phys(dmabuf->priv);

    dma_sync_single_for_device(NULL, dma_addr, PAGE_SIZE, dir);

    return 0;
}

static const struct dma_buf_ops exp_dmabuf_ops = {
    ...
    .begin_cpu_access = exporter_begin_cpu_access,
    .end_cpu_access = exporter_end_cpu_access,
};

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 long exporter_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int fd = dma_buf_fd(dmabuf_exported, O_CLOEXEC);
    return copy_to_user((int __user *)arg, &fd, sizeof(fd));
}

static struct file_operations exporter_fops = {
    .owner      = THIS_MODULE,
    .unlocked_ioctl = exporter_ioctl,
};

static struct miscdevice mdev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "exporter",
    .fops = &exporter_fops,
};

static int __init exporter_init(void)
{
    dmabuf_exported = exporter_alloc_page();
    return misc_register(&mdev);
}

module_init(exporter_init);

5.2 importer 驱动

我们基于《dma-buf 由浅入深(二) —— kmap/vmap》中的 importer-kmap.c 进行修改,如下:

importer-sync.c

#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)
{
    void *vaddr;

    dma_buf_begin_cpu_access(dmabuf, DMA_FROM_DEVICE);

    vaddr = dma_buf_kmap(dmabuf, 0);
    pr_info("read from dmabuf kmap: %s\n", (char *)vaddr);
    dma_buf_kunmap(dmabuf, 0, vaddr);

    vaddr = dma_buf_vmap(dmabuf);
    pr_info("read from dmabuf vmap: %s\n", (char *)vaddr);
    dma_buf_vunmap(dmabuf, vaddr);

    dma_buf_end_cpu_access(dmabuf, DMA_FROM_DEVICE);

    return 0;
}

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

module_init(importer_init);

该 importer 驱动将原来的 kmap / vmap 操作放到了 begin / end 操作中间,以确保读取数据的正确性(虽然在本示例中没有任何意义)。

5.3 userspace 程序

我们基于《dma-buf 由浅入深(四) —— mmap》中的示例一 mmap_dmabuf.c 文件进行修改,如下:

dmabuf_sync.c

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/dma-buf.h>

int main(int argc, char *argv[])
{
    int fd;
    int dmabuf_fd = 0;
    struct dma_buf_sync sync = { 0 };

    fd = open("/dev/exporter", O_RDONLY);

    ioctl(fd, 0, &dmabuf_fd);
    close(fd);

    sync.flags = DMA_BUF_SYNC_READ | DMA_BUF_SYNC_START;
    ioctl(dmabuf_fd, DMA_BUF_IOCTL_SYNC, &sync);

    char *str = mmap(NULL, 4096, PROT_READ, MAP_SHARED, dmabuf_fd, 0);
    printf("read from dmabuf mmap: %s\n", str);

    sync.flags = DMA_BUF_SYNC_READ | DMA_BUF_SYNC_END;
    ioctl(dmabuf_fd, DMA_BUF_IOCTL_SYNC, &sync);

    return 0;
}

该测试程序将原来的 mmap() 操作放到了 ioctl SYNC_START / SYNC_END 之间,以确保读取数据的正确性(虽然在本示例中没有任何意义)。

6. 运行

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

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

输出结果如下:

read from dmabuf kmap: hello world!
read from dmabuf vmap: hello world!

接着执行如下命令:

# ./dmabuf_sync

输出:

read from dmabuf mmap: hello world!

输出的结果其实和之前的程序没有任何区别。

7. 总结

只有在 DMA map/unmap 期间 CPU 又要访问内存的时候,才有必要使用 begin / end 操作;
{ begin,end }_cpu_access 实际是 dma_sync()* 接口的封装,目的是要 invalidate 或 flush cache;
Usespace 通过DMA_BUF_IOCTL_SYNC 来触发 begin / end 操作;

独特见解