DMA-BUF缓冲区共享和同步
DMA-BUF子系统提供了一个框架,用于在多个设备驱动程序和子系统之间共享硬件(DMA)访问的缓冲区,并用于同步异步硬件访问。
例如,drm的“prime”多GPU支持就使用了这个框架,但当然不仅限于GPU的使用情况。
这个框架的三个主要组件是:(1)dma-buf,代表一个sg_table,并以文件描述符的形式暴露给用户空间,以允许在设备之间传递;(2)fence,提供了一种机制,用于在一个设备完成访问时发出信号;(3)reservation,管理与缓冲区关联的共享或独占fence。
共享DMA缓冲区
本文档作为设备驱动程序编写者的指南,介绍了dma-buf缓冲区共享API是什么,以及如何将其用于导出和使用共享缓冲区。
任何希望成为DMA缓冲区共享一部分的设备驱动程序,可以作为缓冲区的“导出者”或“用户”或“导入者”来实现。
假设驱动程序A想要使用由驱动程序B创建的缓冲区,那么我们称B为导出者,A为缓冲区用户/导入者。
导出者
- 实现并管理缓冲区的struct dma_buf_ops中的操作,
- 允许其他用户使用dma_buf共享API共享缓冲区,
- 管理缓冲区分配的细节,封装在struct dma_buf中,
- 决定实际的后备存储位置,
- 并负责任何(共享)用户对该缓冲区的scatterlist的迁移。
缓冲区用户
- 是缓冲区的(许多)共享用户之一。
- 不需要担心缓冲区是如何分配的,或者在哪里。
- 需要一种机制来访问组成该缓冲区的scatterlist,以便将其映射到自己的地址空间中,以便访问相同的内存区域。这个接口由struct dma_buf_attachment提供。
dma-buf缓冲区共享框架的任何导出者或用户必须在各自的Kconfigs中具有“select DMA_SHARED_BUFFER”。
用户空间接口注意事项
大多数情况下,DMA缓冲区文件描述符对于用户空间来说只是一个不透明的对象,因此公开的通用接口非常简单。但也有一些需要考虑的事项:
- 自内核3.12以来,dma-buf FD支持llseek系统调用,但只支持offset=0和whence=SEEK_END|SEEK_SET。支持SEEK_SET是为了允许通常的大小发现模式size = SEEK_END(0); SEEK_SET(0)。其他任何llseek操作都将报告为-EINVAL。
- 如果dma-buf FD上不支持llseek,则内核将对所有情况报告-ESPIPE。用户空间可以使用这一点来检测使用llseek发现dma-buf大小的支持。
- 为了避免在执行时泄漏fd,文件描述符必须设置FD_CLOEXEC标志。这不仅仅是资源泄漏,还可能是一个潜在的安全漏洞。它可能会使新执行的应用程序通过泄漏的fd访问缓冲区,而本来不应该被允许访问。
- 通过单独的fcntl()调用来做这件事,与在创建fd时原子地进行相比,这在多线程应用程序中是固有的竞争。当是库代码打开/创建文件描述符时,问题变得更加严重,因为应用程序甚至可能不知道fd的存在。
- 为了避免这个问题,用户空间必须有一种方法来请求在创建dma-buf fd时设置O_CLOEXEC标志。因此,提供给导出驱动程序创建dmabuf fd的任何API必须提供一种让用户空间控制传递给dma_buf_fd()的O_CLOEXEC标志设置的方法。
- 还支持内存映射DMA缓冲区的内容。有关完整详情,请参阅下面关于CPU访问DMA缓冲区对象的讨论。
- DMA缓冲区FD也是可轮询的,请参阅下面的隐式fence轮询支持以获取详细信息。
- DMA缓冲区FD还支持一些特定于dma-buf的ioctl,请参阅下面的DMA缓冲区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()获得的引用。
对于导出者预期实现的详细语义,请参见dma_buf_ops。
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。这在许多处理管道中是必需的(例如将软件渲染的图像输入到硬件管道中,缩略图创建,快照等)。此外,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-buf fd
- 对于CPU中的每个绘制/上传周期 1. SYNC_START ioctl,2. 读/写到mmap区域 3. SYNC_END ioctl。这可以重复进行(新数据被GPU或扫描设备消耗)。
- 一旦不再需要缓冲区,可以进行munmap。
- 为了正确性和最佳性能,在访问映射地址之前和之后,总是需要使用SYNC_START和SYNC_END。即使在某些系统中,即使没有调用这些ioctl,用户空间也不能依赖一致的访问。
- 以及用户空间处理管道中的CPU回退。
与内核CPU访问的动机类似,重要的是给定导入子系统的用户空间代码可以使用与导入的dma-buf缓冲区对象相同的接口。这对于drm来说尤其重要,因为当代OpenGL、X和其他驱动程序的用户空间部分非常庞大,重新设计它们以使用不同的方式来mmap缓冲区会相当具有侵入性。
当前dma-buf接口的假设是重定向初始mmap就足够了。对一些现有子系统的调查显示,没有驱动程序似乎会做任何像与设备上的未完成的异步处理同步,或者在错误时分配特殊资源之类的坏事。因此,希望这已经足够好了,因为添加接口来拦截页错误并允许pte shootdown将会增加相当多的复杂性。
接口:
int dma_buf_mmap(struct dma_buf \*, struct vm_area_struct \*, unsigned long);
如果导入子系统只是提供了一个特殊用途的mmap调用来在用户空间设置映射,那么使用dma_buf.file调用do_mmap同样可以为dma-buf对象实现这一点。
隐式fence轮询支持
为了支持缓冲区访问的跨设备和跨驱动程序同步,可以将隐式fence(在内核中用struct dma_fence表示)附加到dma_buf上。dma_resv结构提供了这方面的支持。
用户空间可以使用poll()和相关的系统调用来查询这些隐式跟踪的fence的状态:
- 检查EPOLLIN,即读访问,可以用来查询最近写入或独占fence的状态。
- 检查EPOLLOUT,即写访问,可以用来查询所有附加的fence,包括共享的和独占的。
请注意,这只是信号相应fence的完成,即DMA传输完成。在CPU访问开始之前,仍然需要进行缓存刷新和任何其他必要的准备工作。
作为对poll()的替代,可以使用dma_buf_sync_file_export将DMA缓冲区上的一组fence导出为sync_file。
DMA-BUF统计信息
/sys/kernel/debug/dma_buf/bufinfo提供了系统中每个DMA-BUF的概述。但是,由于debugfs不安全,不能在生产环境中挂载,因此可以使用procfs和sysfs来收集生产系统上的DMA-BUF统计信息。
procfs中的/proc/<pid>/fdinfo/<fd>
文件可用于收集关于DMA-BUF fd的信息。有关接口的详细文档在/proc文件系统中有详细说明。
不幸的是,现有的procfs接口只能提供关于那些进程持有fd或将缓冲区映射到其地址空间的DMA-BUF的信息。这促使创建了DMA-BUF sysfs统计接口,以在生产系统上提供每个缓冲区的信息。
当启用CONFIG_DMABUF_SYSFS_STATS时,/sys/kernel/dmabuf/buffers接口会公开有关每个DMA-BUF的信息。
该接口公开了以下统计信息:
/sys/kernel/dmabuf/buffers/<inode_number>/exporter_name
/sys/kernel/dmabuf/buffers/<inode_number>/size
该接口中的信息也可以用于推导每个导出者的统计信息。该接口的数据可以在错误条件或其他重要事件发生时收集,以提供DMA-BUF使用情况的快照。它也可以通过遥测定期收集,以监视各种指标。
有关接口的详细文档在Documentation/ABI/testing/sysfs-kernel-dmabuf-buffers中有详细说明。
DMA Buffer ioctls
https://www.kernel.org/doc/html/v6.6/driver-api/dma-buf.html#dma-buffer-ioctls
预约对象
预约对象提供了一种管理与资源关联的 dma_fence 对象容器的机制。一个预约对象可以附加任意数量的栅栏。每个栅栏都携带一个使用参数,确定栅栏所代表的操作如何使用资源。RCU 机制用于保护来自锁定写侧更新的栅栏的读访问。
更多细节请参阅 struct dma_resv。
https://www.kernel.org/doc/html/v6.6/driver-api/dma-buf.html#c.dma_resv_init
DMA Fence(DMA 栅栏)
DMA 栅栏,由 struct dma_fence 表示,是用于 DMA 操作的内核内部同步原语,例如 GPU 渲染、视频编解码或在屏幕上显示缓冲区。
栅栏通过 dma_fence_init() 进行初始化,并通过 dma_fence_signal() 进行完成。栅栏与上下文相关联,通过 dma_fence_context_alloc() 分配,同一上下文中的所有栅栏都是完全有序的。
由于栅栏的目的是促进跨设备和跨应用程序的同步,有多种使用方式:
- 可以将单个栅栏公开为 sync_file,从用户空间作为文件描述符访问,通过调用 sync_file_create() 创建。这称为显式栅栏,因为用户空间传递显式同步点。
- 一些子系统还具有自己的显式栅栏原语,如 drm_syncobj。与 sync_file 相比,drm_syncobj 允许更新底层栅栏。
- 还有隐式栅栏,其中同步点作为共享的 dma_buf 实例的一部分隐式传递。这种隐式栅栏存储在通过 dma_buf.resv 指针的 struct dma_resv 中。
DMA 栅栏跨驱动程序契约
由于 dma_fence 提供跨驱动程序契约,所有驱动程序必须遵循相同的规则:
- 栅栏必须在合理的时间内完成。代表由用户空间提交的内核和着色器的栅栏可能永远运行,必须由超时和 GPU 挂起恢复代码支持。最低限度上,该代码必须防止进一步的命令提交,并强制完成所有正在进行的栅栏,例如当驱动程序或硬件不支持 GPU 重置,或者 GPU 重置由于某种原因失败。理想情况下,驱动程序支持仅影响有问题的用户空间上下文的 GPU 恢复,而不影响其他用户空间提交。
- 驱动程序可能对合理时间内的完成有不同的理解。一些挂起恢复代码使用固定超时,其他使用观察前进进展和越来越严格的超时之间的混合。驱动程序不应尝试猜测来自其他驱动程序的栅栏超时处理。
- 为了确保 dma_fence_wait() 不会与其他锁发生死锁,驱动程序应该使用 dma_fence_begin_signalling() 和 dma_fence_end_signalling() 注释到达 dma_fence_signal() 所需的所有代码。
- 驱动程序允许在持有 dma_resv_lock() 时调用 dma_fence_wait()。这意味着用于栅栏完成的任何代码都不能获取 dma_resv 锁。请注意,这也涉及围绕 dma_resv_lock() 和 dma_resv_unlock() 的整个已建立的锁层次结构。
- 驱动程序允许在其收缩器回调中调用 dma_fence_wait()。这意味着用于栅栏完成的任何代码都不能使用 GFP_KERNEL 分配内存。
- 驱动程序允许在其 mmu_notifier 和 mmu_interval_notifier 回调中调用 dma_fence_wait()。这意味着用于栅栏完成的任何代码都不能使用 GFP_NOFS 或 GFP_NOIO 分配内存。只有允许使用 GFP_ATOMIC,这可能会失败。
请注意,只有 GPU 驱动程序有合理的理由同时需要 mmu_interval_notifier 和收缩器回调,同时又需要使用 dma_fence 跟踪异步计算工作。除了 drivers/gpu 之外的任何驱动程序都不应在这种情况下调用 dma_fence_wait()。
DMA 栅栏信号注释
通过代码审查和测试来证明围绕 dma_fence 的所有内核代码的正确性是有难度的,原因如下:
- 这是一个跨驱动程序的契约,因此所有驱动程序必须遵循锁嵌套顺序、各种函数的调用上下文以及对内核接口重要的任何其他事项的相同规则。但是在单个机器上测试所有驱动程序是不可能的,因此不可能进行所有组合的暴力 N 对 N 测试。即使仅限于可能的组合也是不可行的。
- 涉及大量驱动程序代码。对于渲染驱动程序,存在命令提交的尾部,栅栏发布后的调度器代码,中断和处理作业完成的工作程序,超时、GPU 重置和 GPU 挂起恢复代码。此外,与核心 mm 集成还有 mmu_notifier,分别是 mmu_interval_notifier 和收缩器。对所有这些代码进行审计,跨所有驱动程序,是不可行的。
- 由于涉及许多其他子系统以及引入的锁层次结构,驱动程序特定差异的余地极小。dma_fence 通过 dma_resv、dma_resv_lock() 和 dma_resv_unlock() 与几乎所有核心内存处理进行交互。另一方面,它还通过 mmu_notifier 和收缩器通过所有分配站点进行交互。
此外,lockdep 不处理交叉释放依赖关系,这意味着无法通过一些快速测试在运行时捕获 dma_fence_wait() 和 dma_fence_signal() 之间的死锁。最简单的例子是一个线程在持有锁时等待 dma_fence,而另一个线程正试图获取相同的锁,这会阻止它对先前线程等待的栅栏进行信号:
lock(A); dma_fence_wait(B); unlock(A);
而另一个线程正试图获取相同的锁,这会阻止它对先前线程等待的栅栏进行信号:
lock(A); unlock(A); dma_fence_signal(B);
通过手动注释与 dma_fence 信号相关的所有代码,我们可以教会 lockdep 关于这些依赖关系,这也有助于验证头痛问题,因为现在 lockdep 可以为我们检查所有规则:
cookie = dma_fence_begin_signalling(); lock(A); unlock(A); dma_fence_signal(B); dma_fence_end_signalling(cookie);
为了使用 dma_fence_begin_signalling() 和 dma_fence_end_signalling() 注释关键部分,需要遵守以下规则:
- 必须注释完成 dma_fence 所需的所有代码,从栅栏对其他线程可访问的点到调用 dma_fence_signal() 的点。未注释的代码可能包含死锁问题,由于非常严格的规则和许多边缘情况,仅通过审查或正常压力测试是不可行的。
- struct dma_resv 值得特别注意,因为读者仅由 rcu 保护。这意味着信号关键部分在安装新栅栏后立即开始,甚至在调用 dma_resv_unlock() 之前。
- 唯一的例外是快速路径和机会信号代码,它纯粹作为优化调用 dma_fence_signal(),但不需要保证完成 dma_fence。通常的例子是等待 IOCTL 调用 dma_fence_signal(),而强制完成路径通过硬件中断和可能的作业完成工作程序。
- 为了帮助代码的可组合性,注释可以自由嵌套,只要整体锁层次结构是一致的。注释还适用于中断和进程上下文。由于实现细节,这要求调用者从 dma_fence_begin_signalling() 传递不透明 cookie 到 dma_fence_end_signalling()。
通过在启动时使用相关层次结构为 lockdep 提供验证,实现了对跨驱动程序契约的验证。这意味着即使只是使用单个设备进行测试也足以验证驱动程序,至少就涉及 dma_fence_wait() 与 dma_fence_signal() 之间的死锁而言是如此。
DMA 栅栏截止时间提示
在理想的世界中,可以对工作负载进行足够的流水线处理,以便利用基于利用率的设备频率调节器可以达到满足用例要求的最低频率,以最小化功耗。但在现实世界中,有许多工作负载违反了这一理想。例如,但不限于:
- 在设备和 CPU 之间来回传送的工作负载,交替出现 CPU 等待设备和设备等待 CPU 的周期。这可能导致 devfreq 和 cpufreq 在各自的领域中看到空闲时间,从而降低频率。
- 与周期性时间截止相关的工作负载,例如双缓冲 GPU 渲染与 vblank 同步的页面翻转。在这种情况下,错过 vblank 截止时间会导致 GPU 上的空闲时间增加(因为它必须等待额外的 vblank 周期),向 GPU 的 devfreq 发送信号以降低频率,而实际上需要相反的操作。
为此,可以通过 dma_fence_set_deadline 在 dma_fence 上设置截止时间提示。截止时间提示提供了一种等待驱动程序或用户空间传达适当紧急性给信号驱动程序的方式。
截止时间提示以绝对 ktime(用户空间面向 API 的 CLOCK_MONOTONIC)给出。时间可以是将来的某个时间点(例如页面翻转的基于 vblank 的截止时间,或合成器的合成周期的开始),也可以是当前时间,以指示即时截止时间提示(即,直到信号此栅栏之前无法取得进展)。
可以在给定的栅栏上设置多个截止时间,甚至并行。有关 dma_fence_ops.set_deadline 的文档,请参阅文档。
截止时间提示只是一个提示。创建栅栏的驱动程序可能会通过增加频率、进行不同的调度选择等做出反应。或者根本不做任何操作。
DMA Fences Functions Reference
https://www.kernel.org/doc/html/v6.6/driver-api/dma-buf.html#dma-fences-functions-reference