缓冲区共享和同步dma_buf 之一


dma-buf 子系统提供了用于跨多个设备驱动程序和子系统共享硬件 (DMA) 访问缓冲区以及同步异步硬件访问的框架。

内核子系统使用 dma-buf 提供的三个主要原语:

  • dma-buf,表示 sg_table 并作为文件描述符供用户空间使用,以允许在进程、子系统、设备等之间传递;
  • dma-fence,提供一种在异步硬件操作完成时发出信号的机制;
  • dma-resv,管理特定 dma-buf 的一组 DMA-fence,允许隐式(内核排序)工作同步,以保持一致访问的错觉

CPU和DMA地址

DMA API 涉及到下列三种地址。

虚拟地址

内核通常使用虚拟地址返回的任何地址kmalloc()、vmalloc()和类似接口是虚拟地址,可以存储在“void *”中。

CPU物理地址

虚拟内存系统(TLB、页表等)将虚拟内存转换为内存地址到CPU物理地址,存储为“phys_addr_t”或
“资源大小_t”。 内核管理设备资源,如寄存器物理地址。 这些是 /proc/iomem 中的地址。 物理方面
地址对驾驶员没有直接用处;它必须使用 ioremap() 来映射空间并产生虚拟地址。

总线地址

I/O 设备使用第三种地址:“总线地址”。 如果一个设备有寄存器在 MMIO 地址,或者是否执行 DMA 来读取或写入系统内存中,设备使用的地址是总线地址。 在一些系统中,总线地址与 CPU 物理地址相同,但在一般来说,他们不是。 IOMMU 和主桥可以产生任意物理地址和总线地址之间的映射。

从设备的角度来看,DMA 使用总线地址空间,但它可能被限制在该空间的子集内。 例如,即使一个系统支持主内存和 PCI BAR 的 64 位地址,它可以使用 IOMMU因此设备只需要使用32位DMA地址。

不同类型地址之间的转换

在枚举过程中,内核得到 I/O 设备,它们的 MMIO空间,以及将它们连接到系统的桥。

如果 PCI 设备有 BAR,则内核从BAR读取总线地址A,并将其转换为CPU物理地址B。 地址B存储在结构资源中,可以通过 /proc/iomem存取。 当一个驱动程序声明一个设备,它通常使用 ioremap(),将物理地址B映射到虚拟地址C。 然后,它可以使用 ioread32© 来访问该设备寄存器,这个寄存器的总线地址是A。

如果设备支持DMA,驱动程序使用 kmalloc或类似API函数来设置缓冲区,kmalloc返回虚地址X。存储器管理系统将虚地址X映射成物理地址Y。驱动器可以使用虚地址X来访问缓冲区,但是设备本身不能,因为DMA不使用经过CPU使用的存储器管理系统。
在这里插入图片描述
一些简单的系统中,设备可以直接对物理地址Y进行DMA。但许多其他系统使用IOMMU硬件,该硬件将DMA地址转换成物理地址,就是,将Z转换为Y。
一部分DMA API就是完成这些功能。驱动程序可以用一个虚拟地址调用API dma_map_single() ,它将返回 DMA 地址Z,Z可以用来设置IOMMU映射。然后驱动程序告诉设备对Z进行DMA,IOMMU将其映射到系统中地址Y处的缓冲区内存。

DMA需要什么样的内存

如果要写带有DMA要求的驱动器,必须要了解内核内存怎样可以与 DMA 映射工具一起使用。

如果您通过页面分配器获取内存(__get_free_page*()),或通用内存分配器(kmalloc() 或 kmem_cache_alloc()),那么,这些API返回的地址,可以直接将这些地址用于DMA。

从vmalloc() 返回的地址,不能用于DMA。

内核映像地址(数据/文本/bss 段中的项目),模块映像地址,堆栈地址都不能用于DMA。

DMA 映射的类型

DMA 映射有两种类型, 两种类型的 DMA 映射都没有来自以下方面的对齐限制:底层总线,尽管某些设备可能有这样的限制。此外,具有非 DMA 一致性缓存的系统会工作得更好当底层缓冲区不与其他数据共享缓存行时。

一致的 DMA 映射通常映射

一致的DMA映射,通常,在驱动程序初始化,完成映射;在驱动器推出时,解除映射。硬件应该保证设备和CPU可以并行地访问数据,并且能看到彼此所做的更新,而无需任何显式软件刷新。

当前默认是返回一致内存,其内容为DMA空间的低32位。
下面是一致映射例子
- 网卡 DMA 环描述符。
- SCSI 适配器邮箱命令数据结构。

流 DMA 映射

该类映射通常仅用于一个DMA传输,在该传输完成后,立即取消该映射。
下面是流映射的例子:

  • 设备发送/接收的网络缓冲区。
  • 文件系统缓冲区由 SCSI 设备写入/读取。

使用这种类型映射,通常是为了实现硬件性能的优化。

共享DMA缓冲区

设备驱动程序编写者提供有关什么是 dma-buf 缓冲区共享 API、如何使用它导出和使用共享缓冲区的指南。
任何希望成为 DMA 缓冲区共享一部分的设备驱动程序,都可以作为缓冲区的“导出者”、缓冲区的“用户”或“导入者”。
假设驱动程序 A 想使用驱动程序 B 创建的缓冲区,那么我们将 B 称为导出者,将 A 称为缓冲区用户/导入者。

导出者

  • 实现并管理 struct dma_buf_ops 中缓冲区的操作,
  • 允许其他用户通过使用 dma_buf 共享 API 来共享缓冲区,
  • 管理缓冲区分配的详细信息,包装在 dma_buf 结构体中,
  • 决定发生分配的实际后备存储,
  • 负责该缓冲区的所有(共享)用户的分散列表的任何迁移。

缓冲用户

  • 是缓冲区的(许多)共享用户之一。
  • 不需要担心缓冲区是如何分配的,或者在哪里分配的。
  • 并且需要一种机制来访问构成内存中缓冲区的分散列表,映射到自己的地址空间,以便它可以访问同一内存区域。该接口由 struct dma_buf_attachment 提供。

dma-buf 缓冲区共享框架的任何导出者或用户都必须在各自的 Kconfig 中具有“select DMA_SHARED_BUFFER”。

用户空间接口

大多数情况下,DMA 缓冲区文件描述符只是用户空间的不透明对象,因此公开的通用接口非常少。但有一些事情需要考虑:

  • 从内核 3.12 开始,dma-buf FD 支持 llseek 系统调用,但仅限 offset=0 且 wherece=SEEK_END|SEEK_SET。支持 SEEK_SET 以允许通常的大小发现模式 size = SEEK_END(0); SEEK_SET(0)。所有其他 llseek 操作都会报告 -EINVAL。
    如果不支持 dma-buf FD 上的 llseek,则内核将在所有情况下报告 -ESPIPE。用户空间可以使用它来检测对使用 llseek 发现 dma-buf 大小的支持。
  • 为了避免 exec 上的 fd 泄漏,必须在文件描述符上设置 FD_CLOEXEC 标志。这不仅仅是资源泄漏,而且是潜在的安全漏洞。它可以通过泄漏的文件描述符,让新执行的应用程序访问缓冲区,否则,它不应该被允许访问。
    通过单独的 fcntl() 调用来执行此操作,而不是在创建 fd 时以原子方式执行此操作,问题在于,这在多线程应用程序中本质上是很活跃的[3]。当库代码打开/创建文件描述符时,问题会变得更糟,因为应用程序甚至可能不知道 fd。
    为了避免这个问题,用户空间必须有一种方法来请求在创建 dma-buf fd 时设置 O_CLOEXEC 标志。因此,导出驱动程序提供的用于创建 dmabuf fd 的任何 API 都必须提供一种方法,让用户空间控制传递给 dma_buf_fd() 的 O_CLOEXEC 标志的设置。
  • 支持 DMA 缓冲区内容的内存映射。
  • DMA 缓冲区 FD是可查询。
  • DMA 缓冲区FD支持一些 dma-buf 特定的 ioctl。

基本操作和设备DMA访问

设备DMA访问共享DMA缓冲区,通常需要采取下列步骤:

  • 导出器使用 DEFINE_DMA_BUF_EXPORT_INFO() 定义其导出器实例,并调用 dma_buf_export() ,将私有缓冲区对象封装到 dma_buf 中。然后,调用 dma_buf_fd() 将 dma_buf 作为文件描述符导出到用户空间。
  • 用户空间将此文件描述符传递给它希望共享此缓冲区的所有驱动程序:首先使用 dma_buf_get() 将文件描述符转换为 dma_buf;然后使用 dma_buf_attach() 将缓冲区附加到设备。
    到目前为止,导出器仍然可以自由迁移或重新分配用于数据存储的存储器。
  • 一旦缓冲区连接到所有设备,用户空间就可以启动对共享缓冲区的DMA 访问。在内核中,这是通过调用 dma_buf_map_attachment() 和 dma_buf_unmap_attachment() 来完成的。
  • 一旦驱动程序使用完共享缓冲区,调用 dma_buf_detach(),然后再调用 dma_buf_put() 释放使用 dma_buf_get() 获取的引用。

CPU访问DMA缓冲区对象

支持 CPU 访问 DMA 缓冲区对象有多种原因:

内核中的应变操作

当设备通过 USB 连接时,内核需要先对数据进行重新组合,然后再将其发送出去。通过将所有事务组合在一起,来处理缓存一致性,事务组合是通过调用 dma_buf_begin_cpu_access() 和 dma_buf_end_cpu_access() 来实现的。

由于大多数内核内部 dma-buf 访问需要整个缓冲区,因此引入了 vmap 接口。请注意,在非常旧的 32 位体系结构上,vmalloc 空间可能有限,这导致 vmap 调用失败。
接口:

void *dma_buf_vmap(struct dma_buf *dmabuf, struct iosys_map *map)
void dma_buf_vunmap(struct dma_buf *dmabuf, struct iosys_map *map)

如果导出器中没有 vmap 支持,或者 vmalloc 空间不足,则 vmap 调用可能会失败。请注意,dma-buf 层会保留所有 vmap 访问的引用计数,仅当不存在 vmapping 时, 才调用导出器的 vmap 函数,取消映射仅会发生一次。使用 dma_buf.lock 互斥锁,防止vmap/vunmap 并发调用。

在导入器端完全兼容用户空间接口

为了在导入端与现有的用户空间接口完全兼容,现有的用户空间接口可能已经支持 mmap’ing 缓冲区。这在许多处理管道中都是需要的(例如,将软件渲染的图像输入硬件管道、缩略图创建、快照等)。此外,Android 的 ION 框架已经支持这一点,并且需要 DMA 缓冲区文件描述符来替换 ION 缓冲区 mmap 支持。

没有特殊的接口,用户空间只是使用dma-buf的fd调用 mmap。对于CPU访问,需要将实际访问组合起来,这是由 ioctl (DMA_BUF_IOCTL_SYNC) 处理的。在必须重新启动情况下,DMA_BUF_IOCTL_SYNC 可能会失败,并返回-EAGAIN 或 -EINTR。

某些系统可能需要某种缓存一致性管理,例如当CPU 和 GPU 同时访问dma-buf时。为了避免这个问题,将开始/结束一致性标记,直接转发到现有的 dma-buf 设备驱动程序 vfunc 挂钩。用户空间可以通过 DMA_BUF_IOCTL_SYNC ioctl 使用这些标记。该序列的使用方式如下:

  • mmap DMA 缓冲区 fd
  • 对于 CPU 中的每个提取/上传周期 1. SYNC_START ioctl,2. 读/写 mmap 区域 3. SYNC_END ioctl。可以根据需要多次重复这个操作
  • 一旦您不再需要缓冲区,就可以使用 munmap

为了正确性和最佳性能,在访问映射地址时,始终需要分别在之前和之后使用 SYNC_START 和 SYNC_END。用户空间不能依赖一致的访问。

隐式围栏(fence)查询支持

为了支持缓冲区访问的跨设备和跨驱动程序同步,可以将隐式栅栏(在内核内部用 struct dma_fence 表示)附加到 dma_buf。 dma_resv 结构中提供了它的粘合剂和一些相关的东西。

用户空间可以使用 poll() 和相关系统调用,查询这些隐式跟踪的栅栏的状态:

  • 检查 EPOLLIN(即读访问)可用于查询最近的写,或独占栅栏的状态。
  • 检查 EPOLLOUT(即写访问)可用于查询所有附加栅栏(共享栅栏和独占栅栏)的状态。

请注意,这仅表示相应栅栏已完成,即 DMA 传输已完成。在 CPU 访问开始之前,缓存刷新和任何其他必要的准备工作仍然需要进行。

作为 poll() 的替代方案,可以使用 dma_buf_sync_file_export 将 DMA 缓冲区上的栅栏集导出为sync_file。

  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值