buffer共享-2-dma_buf-2-内核文档翻译

注:翻译自Linux-6.6.0
Buffer Sharing and Synchronization (dma-buf): https://www.kernel.org/doc/html/v6.6/driver-api/dma-buf.html#dma-fences


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

例如,drm "prime" multi-GPU 支持就使用了该框架,但当然不限于 GPU 用例。

该框架的三个主要组件是:
(1) dma-buf,它代表一个 sg_table,并作为文件描述符暴露给用户空间,以允许在设备之间传递;
(2) fence,它提供一种机制,用于在一个设备完成访问时发出信号;
(3) reservation,它管理与缓冲区关联的共享或独占 fence。


一、Shared DMA Buffers

本文档旨在为设备驱动程序编写者提供指南,介绍什么是 dma-buf 缓冲区共享 API,以及如何使用它来导出和使用共享缓冲区。

任何希望参与 DMA 缓冲区共享的设备驱动程序,都可以作为缓冲区的“导出者”,也可以作为缓冲区的“用户”或“导入者”

假设驱动程序 A 想要使用驱动程序 B 创建的缓冲区,那么我们称 B 为导出者,A 为缓冲区的用户/导入者。


导出器(exporter):

(1) 在 dma_buf_ops 结构体中实现和管理缓冲区的操作,

(2) 允许其他用户使用 dma_buf 共享 API 共享缓冲区,

(3) 管理缓冲区分配的细节(封装在 dma_buf 结构体中),

(4) 决定分配的实际后备存储,

(5) 并负责此缓冲区所有(共享)用户的散射列表迁移。


缓冲区用户:

(1) 是缓冲区的(众多)共享用户之一。

(2) 无需担心缓冲区的分配方式或分配位置。

(3) 并且需要一种机制来访问构成此缓冲区的散射列表(映射到其自己的地址空间),以便访问相同的内存区域。此接口由 dma_buf_attachment 结构体提供。

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


1. Userspace Interface Notes

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

(1) 从内核 3.12 开始,dma-buf 文件描述符支持 llseek 系统调用,但仅限于 offset=0 且 whence=SEEK_END|SEEK_SET 的情况。支持 SEEK_SET 是为了允许通常的大小发现模式 size = SEEK_END(0); SEEK_SET(0)。其他所有 llseek 操作都会报告 -EINVAL。

如果 dma-buf 文件描述符不支持 llseek,内核会在所有情况下报告 -ESPIPE 。用户空间可以使用此错误来检测是否支持使用 llseek 发现 dma-buf 大小。

(2) 为了避免 exec 时出现文件描述符泄漏,必须在文件描述符上设置 FD_CLOEXEC 标志。这不仅是资源泄漏,还是一个潜在的安全漏洞。它可以让新执行的应用程序通过泄漏的 fd 访问缓冲区,而原本它不应该被允许访问这些缓冲区。

与在创建 fd 时以原子方式执行此操作相比,通过单独的 fcntl() 调用执行此操作的问题在于,这在多线程应用程序中本质上是危险的[3]。当库代码打开/创建文件描述符时,问题会变得更加严重,因为应用程序甚至可能不知道 fd 的存在。

为了避免这个问题,用户空间必须有一种方法可以在创建 dma-buf fd 时请求设置 O_CLOEXEC 标志。因此,导出驱动程序提供的任何用于创建 dmabuf fd 的 API 都必须提供一种方法来让用户空间控制传递给 dma_buf_fd() 的 O_CLOEXEC 标志的设置。

(3) 还支持对 DMA 缓冲区内容进行内存映射。有关完整详细信息,请参阅下面关于 CPU 访问 DMA 缓冲区对象的讨论。

(4) DMA 缓冲区 FD 也可轮询,详情请参阅下文的“Implicit Fence Poll Support”。

(5) DMA 缓冲区 FD 还支持一些特定于 DMA 缓冲区的 ioctl,详情请参阅下文的“DMA Buffer ioctls”。


2. Basic Operation and Device DMA Access

对于设备 DMA 访问共享 DMA 缓冲区,通常的操作顺序相当简单:

(1) 导出器(exporter)使用 DEFINE_DMA_BUF_EXPORT_INFO() 定义其导出器实例,并调用 dma_buf_export() 将私有缓冲区对象包装到 dma_buf 中。然后,它通过调用 dma_buf_fd() 将该 dma_buf 作为文件描述符导出到用户空间。

(2) 用户空间将该文件描述符传递给所有希望与其共享此缓冲区的进程的驱动程序:首先,使用 dma_buf_get() 将文件描述符转换为 dma_buf。然后,使用 dma_buf_attach() 将缓冲区连接到设备。

在此阶段之前,导出器仍然可以自由迁移或重新分配后备存储。

(3) 一旦缓冲区连接到所有设备,用户空间就可以启动对共享缓冲区的 DMA 访问。在内核中,这通过调用 dma_buf_map_attachment() 和 dma_buf_unmap_attachment() 来完成。

(4) 驱动程序使用完共享缓冲区后,需要调用 dma_buf_detach()(在清除所有映射之后),然后通过调用 dma_buf_put() 释放通过 dma_buf_get() 获取的引用。

有关导出器预期实现的详细语义,请参阅 dma_buf_ops。


3. CPU Access to DMA Buffer Objects

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

(1) 内核中的回退操作,例如当设备通过 USB 连接时,内核需要先对数据进行混淆,然后再发送数据。缓存一致性是通过调用 dma_buf_begin_cpu_access() 和 dma_buf_end_cpu_access() 访问来处理所有事务的。

由于大多数内核内部 DMA 缓冲区访问需要整个缓冲区,因此引入了 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 调用。


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

没有特殊的接口,用户空间只需在 dma-buf 文件描述符上调用 mmap 即可。但是,与 CPU 访问一样,需要将实际访问括起来,该访问由 ioctl (DMA_BUF_IOCTL_SYNC) 处理。请注意,DMA_BUF_IOCTL_SYNC 可能会失败并返回 -EAGAIN 或 -EINTR,在这种情况下必须重新启动。

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

a. mmap dma-buf fd

b. CPU 中的每个drawing/upload周期:1. SYNC_START ioctl;2. 读写 mmap 区域;3. SYNC_END ioctl。此操作可以根据需要重复执行(新数据会被 GPU 或扫描输出设备使用)。

c. 一旦不再需要缓冲区,就执行 munmap。

为了确保正确性和最佳性能,在访问映射地址时,始终需要在之前和之后分别使用 SYNC_START 和 SYNC_END ioctl。用户空间不能依赖一致性访问,即使在某些系统中无需调用这些 ioctl 也能正常工作。


(3) 作为用户空间处理流水线中的 CPU 回退方案。

与内核 CPU 访问的动机类似,同样重要的是,给定导入子系统的用户空间代码能够使用与原生缓冲区对象相同的接口来处理导入的 dma-buf 缓冲区对象。这对于 drm 尤其重要,因为当代 OpenGL、X 和其他驱动程序的用户空间部分非常庞大,而重新设计它们以使用不同的方式对缓冲区进行 mmap 操作则相当具有侵入性。

当前 dma-buf 接口的假设是,只需重定向初始化 mmap 即可。对一些现有子系统的调查显示,似乎没有任何驱动程序会执行任何恶意操作,例如与设备上未完成的异步处理同步或在故障时分配特殊资源。所以希望这足够了,因为添加用于拦截页面错误和允许 pte 击落的接口会大大增加复杂性。

接口:

int dma_buf_mmap(struct dma_buf *, struct vm_area_struct *, unsigned long);

如果导入子系统仅提供一个专用的 mmap 调用来在用户空间中建立映射,则使用 dma_buf.file 调用 do_mmap 也可以为 dma-buf 对象实现同样的功能。


4. Implicit Fence Poll Support

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

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

(1) 检查 EPOLLIN(即读访问权限)可用于查询最近的写或独占栅栏的状态。

(2) 检查 EPOLLOUT(即写访问权限)可用于查询所有附加栅栏(共享栅栏和独占栅栏)的状态。

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

作为 poll() 的替代方法,可以使用 dma_buf_sync_file_export 将 DMA 缓冲区上的一组围栏导出为 sync_file。


5. DMA-BUF statistics

/sys/kernel/debug/dma_buf/bufinfo 提供了系统中每个 DMA-BUF 的概览。但是,由于在生产环境中挂载 debugfs 并不安全,因此可以使用 procfs 和 sysfs 来收集生产系统中的 DMA-BUF 统计信息。

procfs 中的 /proc/<pid>/fdinfo/<fd> 文件可用于收集有关 DMA-BUF fds 的信息。有关该接口的详细文档请参阅 /proc 文件系统。I: 即谁创建的dma_fd对应的fd文件创建在谁的进程下。

遗憾的是,现有的 procfs 接口只能提供有关哪些进程持有 fds 或将缓冲区 mmapplied 到其地址空间的 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”。


6. DMA Buffer ioctls

struct dma_buf_sync: 与CPU访问同步。

描述:

struct dma_buf_sync {
    __u64 flags;
};

成员:

flags: 访问标志集

DMA_BUF_SYNC_START: 指示映射访问会话的开始。
DMA_BUF_SYNC_END: 指示映射访问会话的结束。
DMA_BUF_SYNC_READ: 指示客户端将通过 CPU 映射读取映射的 DMA 缓冲区。
DMA_BUF_SYNC_WRITE: 指示客户端将通过 CPU 映射写入映射的 DMA 缓冲区。
DMA_BUF_SYNC_RW: DMA_BUF_SYNC_READ | DMA_BUF_SYNC_WRITE 的别名。

描述:

当通过 mmap 从 CPU 访问 DMA 缓冲区时,并非总能保证 CPU 可见映射与底层内存之间的一致性。为了管理一致性,必须使用 DMA_BUF_IOCTL_SYNC 来限制任何 CPU 访问,以便内核有机会根据需要对内存进行重新排序。

在访问映射之前,客户端必须使用 DMA_BUF_SYNC_START 和相应的读/写标志调用 DMA_BUF_IOCTL_SYNC。访问完成后,客户端应使用 DMA_BUF_SYNC_END 和相同的读/写标志调用 DMA_BUF_IOCTL_SYNC。

通过 DMA_BUF_IOCTL_SYNC 提供的同步仅提供缓存一致性。它不会阻止其他进程或设备同时访问内存。如果需要与 GPU 或其他设备驱动程序同步,客户端有责任等待缓冲区准备好进行读写操作,然后再使用 DMA_BUF_SYNC_START 调用此 ioctl。同样,客户端必须确保在使用 DMA_BUF_SYNC_END? 调用此 ioctl 之前,后续工作不会提交给 GPU 或其他设备驱动程序。

如果客户端与之交互的驱动程序或 API 使用隐式同步,则可以通过对 DMA 缓冲区文件描述符调用 poll() 来等待先前工作完成。如果驱动程序或 API 需要显式同步,则客户端可能需要等待 sync_file 或其他超出 DMA 缓冲区 API 范围的同步原语。


struct dma_buf_export_sync_file

从 dma-buf 获取 sync_file

struct dma_buf_export_sync_file {
    __u32 flags;
    __s32 fd;
};

成员介绍:

flags: 读/写标志

必须是 DMA_BUF_SYNC_READ、DMA_BUF_SYNC_WRITE 或两者。

如果设置了 DMA_BUF_SYNC_READ 而未设置 DMA_BUF_SYNC_WRITE,则返回的同步文件将等待 DMA-buf 的所有写入操作完成。等待返回的同步文件相当于使用 POLLIN 调用 poll()。

如果设置了 DMA_BUF_SYNC_WRITE,则返回的同步文件将等待 DMA-buf 的所有用户操作(读或写)完成。等待返回的同步文件相当于使用 POLLOUT 调用 poll()。如果同时设置了 DMA_BUF_SYNC_WRITE 和 DMA_BUF_SYNC_READ,则相当于仅使用 DMA_BUF_SYNC_WRITE。

fd: 返回的同步文件描述符

说明:

用户空间可以执行 DMA_BUF_IOCTL_EXPORT_SYNC_FILE 来检索 dma-buf 文件描述符上的当前栅栏集合,并将其作为 sync_file 进行检索。CPU 通过 poll() 或其他驱动程序特定机制进行的等待,通常会在等待开始时等待 dma-buf 上的所有栅栏。这与 DMA_BUF_IOCTL_EXPORT_SYNC_FILE 类似,只是它会对 dma-buf 上的当前栅栏进行快照,以便稍后等待,而不是立即等待。这对于 Vulkan 等现代图形 API 非常有用,因为它们假设使用显式同步模型,但仍需要与 dma-buf 进行交互。

预期使用模式如下:

(1) 通过 DMA_BUF_IOCTL_EXPORT_SYNC_FILE 导出一个 sync_file,其中包含与预期 GPU 使用率对应的标志。

(2) 提交使用 dma-buf 的渲染工作。该工作应在渲染之前等待导出的同步文件,并在完成后生成另一个 sync_file。

(3) 通过 DMA_BUF_IOCTL_IMPORT_SYNC_FILE 将渲染完成的 sync_file 导入 dma-buf,并使用与 GPU 使用情况对应的标志。

与通过 GPU 内核驱动程序的 exec ioctl 进行隐式同步不同,上述操作并非单个原子操作。如果用户空间希望通过这些栅栏确保顺序,则用户空间有责任使用锁或其他机制来确保在上述步骤 (1) 和步骤 (3) 之间没有其他上下文添加栅栏或提交工作。


struct dma_buf_import_sync_file

将 sync_file 插入到 dma-buf 中

struct dma_buf_import_sync_file {
    __u32 flags;
    __s32 fd;
};

成员介绍:

flags: 读/写标志

必须是 DMA_BUF_SYNC_READ、DMA_BUF_SYNC_WRITE 或两者。

如果设置了 DMA_BUF_SYNC_READ 而未设置 DMA_BUF_SYNC_WRITE,则会将 sync_file 插入为只读栅栏。任何后续对此 DMA-buf 的隐式同步写入都将在此栅栏上等待,但读取操作则不会。

如果设置了 DMA_BUF_SYNC_WRITE,则会将 sync_file 插入为写栅栏。所有后续对此 DMA-buf 的隐式同步访问都将在此栅栏上等待。

fd: 同步文件描述符

说明:

用户空间可以执行 DMA_BUF_IOCTL_IMPORT_SYNC_FILE,将 sync_file 插入 dma-buf,以便与其他 dma-buf 消费者进行隐式同步。这允许使用显式同步 API(例如 Vulkan)的客户端与需要隐式同步的 dma-buf 消费者(例如 OpenGL 或大多数媒体驱动程序/视频)进行互操作。


7. DMA-BUF locking convention

为了避免 dma-buf 导出器和导入器之间出现死锁情况,所有 dma-buf API 用户必须遵循通用的 dma-buf 锁定约定。

导入器约定:

(1) 导入器在调用以下函数时必须持有 dma-buf 预留锁:

dma_buf_pin()
dma_buf_unpin()
dma_buf_map_attachment()
dma_buf_unmap_attachment()
dma_buf_vmap()
dma_buf_vunmap()

(2) 调用这些函数时,导入器不得持有 dma-buf 预留锁:

dma_buf_attach()
dma_buf_dynamic_attach()
dma_buf_detach()
dma_buf_export()
dma_buf_fd()
dma_buf_get()
dma_buf_put()
dma_buf_mmap()
dma_buf_begin_cpu_access()
dma_buf_end_cpu_access()
dma_buf_map_attachment_unlocked()
dma_buf_unmap_attachment_unlocked()
dma_buf_vmap_unlocked()
dma_buf_vunmap_unlocked()

导出器约定:

(1) 这些 dma_buf_ops 回调函数在 dma-buf 预留空间解锁的情况下调用,导出器可以获取该锁:

dma_buf_ops.attach()
dma_buf_ops.detach()
dma_buf_ops.release()
dma_buf_ops.begin_cpu_access()
dma_buf_ops.end_cpu_access()
dma_buf_ops.mmap()

(2) 这些 dma_buf_ops 回调在 dma-buf 预留锁锁定的情况下调用,并且导出器不能获取此锁:

dma_buf_ops.pin()
dma_buf_ops.unpin()
dma_buf_ops.map_dma_buf()
dma_buf_ops.unmap_dma_buf()
dma_buf_ops.vmap()
dma_buf_ops.vunmap()

(3) 调用这些函数时,导出器必须持有 dma-buf 预留锁:

dma_buf_move_notify()


8. Kernel Functions and Structures Reference

struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info)

创建一个新的 dma_buf,并将一个匿名文件与此缓冲区关联,以便将其导出。同时将分配器特定的数据和操作连接到缓冲区。此外,为导出器提供一个名称字符串;这在调试时很有用。

参数:

const struct dma_buf_export_info *exp_info:[in] 保存导出器提供的所有导出相关信息。更多详情,请参阅 struct dma_buf_export_info。

描述:

成功时返回一个新创建的 struct dma_buf 对象,该对象包装了 struct dma_buf_ops 提供的私有数据和操作。如果缺少操作或分配 struct dma_buf 时出错,将返回负的错误值。

大多数情况下,创建 exp_info 的最简单方法是通过 DEFINE_DMA_BUF_EXPORT_INFO 宏。


int dma_buf_fd(struct dma_buf *dmabuf, int flags)

返回给定 struct dma_buf 的文件描述符。

参数:

struct dma_buf *dmabuf:[in] 指向需要生成 fd 的 dma_buf 的指针。
int flags:[in] 给 fd 的标志

描述:

成功时返回关联的“fd”。否则返回错误。


struct dma_buf *dma_buf_get(int fd)

返回与某个 fd 关联的 struct dma_buf。

参数:

int fd:[in] 与要返回的 struct dma_buf 关联的 fd。

描述:

成功时,返回与某个 fd 关联的 struct dma_buf;使用 fget 完成的文件引用计数来增加引用计数。否则返回 ERR_PTR。


void dma_buf_put(struct dma_buf *dmabuf)

减少缓冲区的引用计数

参数:

struct dma_buf *dmabuf:[in] 减少引用计数的缓冲区

描述:

使用 fput() 隐式完成的文件引用计数。

如果调用此函数后,引用计数变为 0,则调用与该文件描述符相关的“释放”文件操作。它会依次调用 dma_buf_ops.release 函数,并在导出时释放为 dmabuf 分配的内存。


struct dma_buf_attachment *dma_buf_dynamic_attach(struct dma_buf *dmabuf, struct device *dev,
const struct dma_buf_attach_ops *importer_ops, void *importer_priv)

将设备添加到 dma_buf 的附件列表

参数:

struct dma_buf *dmabuf: [in] 设备要附加到的缓冲区。
struct device *dev: [in] 要附加的设备。
const struct dma_buf_attach_ops *importer_ops: [in] 附加的导入器操作
void *importer_priv: [in] 附加的导入器私有指针

说明:

返回此附加操作的 struct dma_buf_attachment 指针。必须通过调用 dma_buf_detach() 清理附加。

(可选)调用 dma_buf_ops.attach 以允许特定于设备的附加功能。

成功时返回指向新创建的 dma_buf_attachment 的指针;失败时返回一个包含负错误代码的指针。

请注意,如果 dmabuf 的后备存储位于 dev 无法访问的位置,并且无法移动到更合适的位置,则此操作可能会失败。错误代码为 -EBUSY。


struct dma_buf_attachment *dma_buf_attach(struct dma_buf *dmabuf, struct device *dev)

dma_buf_dynamic_attach 的封装器

参数:

struct dma_buf *dmabuf: [in] 设备要附加到的缓冲区。
struct device *dev: [in] 待附加的设备。

描述:

用于为仍使用静态映射的驱动程序调用 dma_buf_dynamic_attach() 的封装器。


void dma_buf_detach(struct dma_buf *dmabuf, struct dma_buf_attachment *attach)

从 dmabuf 的附加列表中移除指定的附加

参数:

struct dma_buf *dmabuf: [in] 待分离的缓冲区。
struct dma_buf_attachment *attach: [in] 待分离的附加;调用此函数后将被释放。

描述:

清除通过调用 dma_buf_attach() 获取的设备附加。

(可选)调用 dma_buf_ops.detach 函数进行设备特定的分离。


int dma_buf_pin(struct dma_buf_attachment *attach)

锁定 DMA 缓冲区

参数:

struct dma_buf_attachment *attach: [in] 需要锁定的附加

描述:

只有动态导入器(使用 dma_buf_dynamic_attach() 设置连接)可以调用此函数,并且只能用于扫描输出等有限用例,而不能用于临时锁定操作。不允许用户空间通过此接口锁定任意数量的缓冲区。

必须通过调用 dma_buf_unpin() 来取消锁定缓冲区。

返回:

成功时返回 0,失败时返回负错误代码。


void dma_buf_unpin(struct dma_buf_attachment *attach)

解除 DMA 缓冲区的锁定

参数:

struct dma_buf_attachment *attach: [in] 需要解除锁定的附加

描述:

此函数将解除由 dma_buf_pin() 锁定的缓冲区的锁定,并允许导出器再次移动任何附加映射,并通过 dma_buf_attach_ops.move_notify 通知导入器。


struct sg_table *dma_buf_map_attachment(struct dma_buf_attachment *attach, enum dma_data_direction direction)

返回附加的scatterlist table表;映射到 _device_ 地址空间。它是 dma_buf_ops 的函数 map_dma_buf() 的包装器。

参数:

struct dma_buf_attachment *attach: [in] 待返回散射列表的附件

enum dma_data_direction direction: [in] DMA 传输方向

描述:

返回包含待返回散射列表的 sg_table;出错时返回 ERR_PTR。如果被信号中断,可能返回 -EINTR。

成功时,返回的散射列表中的 DMA 地址和长度将按 PAGE_SIZE 对齐。

必须使用 dma_buf_unmap_attachment() 取消映射。请注意,只要映射存在,底层后备存储就会被固定,因此用户/导入程序不应长时间持有映射。

重要提示:动态导入程序必须先等待连接到 DMA-BUF 的 struct dma_resv 的独占栅栏。


struct sg_table *dma_buf_map_attachment_unlocked(struct dma_buf_attachment *attach, enum dma_data_direction direction)

返回附件的 scatterlist 表;映射到_device_地址空间。它是 dma_buf_ops 函数 map_dma_buf() 的包装器。

参数:

struct dma_buf_attachment *attach: [in] 待返回散点列表的附件

enum dma_data_direction direction: [in] DMA 传输方向

描述:

dma_buf_map_attachment() 的解锁版本。


void dma_buf_unmap_attachment(struct dma_buf_attachment *attach, struct sg_table *sg_table, enum dma_data_direction direction)

取消映射并减少缓冲区的使用计数;可能会释放关联的 scatterlist。它是 dma_buf_ops 函数 unmap_dma_buf() 的包装器。

参数:

struct dma_buf_attachment *attach: [in] 需要取消映射缓冲区的附件

struct sg_table *sg_table: [in] 需要取消映射的缓冲区的散射列表信息

enum dma_data_direction direction: [in] DMA 传输方向

说明:

这将取消映射由 dma_buf_map_attachment() 获取的附加 DMA 映射。


void dma_buf_unmap_attachment_unlocked(struct dma_buf_attachment *attach, struct sg_table *sg_table, enum dma_data_direction direction)

取消映射并减少缓冲区的使用计数;可能会释放关联的 scatterlist。它是 dma_buf_ops 函数 unmap_dma_buf() 的包装器。

参数:

struct dma_buf_attachment *attach: [in] 需要取消映射缓冲区的附件

struct sg_table *sg_table: [in] 需要取消映射的缓冲区的散射列表信息

enum dma_data_direction direction: [in] DMA 传输方向

描述:

dma_buf_unmap_attachment() 的解锁版本。


void dma_buf_move_notify(struct dma_buf *dmabuf)

通知所有设备 DMA 缓冲区正在移动.

参数:

struct dma_buf *dmabuf: [in] 正在移动的缓冲区

描述:

通知所有设备需要销毁并重新创建所有映射。


int dma_buf_begin_cpu_access(struct dma_buf *dmabuf, enum dma_data_direction direction)

在内核上下文中从 CPU 访问 dma_buf 之前必须调用此函数。调用 begin_cpu_access 函数允许进行特定于导出器的准备工作。仅在指定范围内,针对指定的访问方向保证一致性。

参数:

struct dma_buf *dmabuf: [in] 准备 CPU 访问的缓冲区。

enum dma_data_direction direction: [in] 访问方向。

说明:

CPU 访问完成后,调用者应调用 dma_buf_end_cpu_access()。只有当 CPU 访问包含在两个调用中时,才能保证与其他 DMA 访问一致

此函数还会等待 dma_buf.resv 中通过隐式同步跟踪的任何 DMA 事务。对于具有显式同步的 DMA 事务,此函数仅确保缓存一致性,调用者必须自行确保与此类 DMA 事务的同步。

错误时可能返回负值,成功时返回 0。


int dma_buf_end_cpu_access(struct dma_buf *dmabuf, enum dma_data_direction direction)

在内核上下文中从 CPU 访问 dma_buf 后必须调用此函数。调用 end_cpu_access 以允许特定于导出器的操作。仅在指定范围内针对指定访问方向保证一致性。

参数:

struct dma_buf *dmabuf: [in] 完成 CPU 访问的缓冲区。

enum dma_data_direction direction: [in] 访问方向。

说明:

此函数终止由 dma_buf_begin_cpu_access() 启动的 CPU 访问。

可能返回负错误值,成功时返回 0。


int dma_buf_mmap(struct dma_buf *dmabuf, struct vm_area_struct *vma, unsigned long pgoff)

使用给定的 vma 设置用户空间 mmap

参数:

struct dma_buf *dmabuf: [in] 需要支持 vma 的缓冲区

struct vm_area_struct *vma: [in] mmap 的 vma

unsigned long pgoff: [in] 此 mmap 在 dma-buf 缓冲区中的起始偏移量(以页面为单位)。

描述:

此函数调整传入的 vma,使其指向 dma_buf 操作的文件。它还会调整起始 pgoff,并对 vma 的大小进行边界检查。然后,它调用导出器的 mmap 函数来设置映射。

可能返回负错误值,成功时返回 0。


int dma_buf_vmap(struct dma_buf *dmabuf, struct iosys_map *map)

为缓冲区对象创建到内核地址空间的虚拟映射。适用与 vmap 及其相关函数相同的限制。

参数:

struct dma_buf *dmabuf: [in] 到 vmap 的缓冲区

struct iosys_map *map: [out] 返回 vmap 指针

说明:

此调用可能由于虚拟映射地址空间不足而失败。这些调用在驱动程序中是可选的。它们的预期用途是将高使用率对象映射到内核空间的线性对象。

为了确保一致性,用户必须在通过此映射执行的任何 CPU 访问周围调用 dma_buf_begin_cpu_access() 和 dma_buf_end_cpu_access()。

成功时返回 0,否则返回负的 errno 代码。


int dma_buf_vmap_unlocked(struct dma_buf *dmabuf, struct iosys_map *map)

为缓冲区对象创建到内核地址空间的虚拟映射。适用与 vmap 及其相关函数相同的限制。

参数:

struct dma_buf *dmabuf: [in] 指向 vmap 的缓冲区

struct iosys_map *map: [out] 返回 vmap 指针

说明:

dma_buf_vmap() 的解锁版本

成功时返回 0,否则返回负的 errno 代码。


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

取消映射由 dma_buf_vmap 获取的 vmap。

参数:

struct dma_buf *dmabuf: [in] 取消映射的缓冲区

struct iosys_map *map: [in] 指向 vmap 的 vmap 指针


void dma_buf_vunmap_unlocked(struct dma_buf *dmabuf, struct iosys_map *map)

取消映射由 dma_buf_vmap 获取的 vmap。

参数:

struct dma_buf *dmabuf: [in] 取消映射的缓冲区

struct iosys_map *map: [in] 指向 vmap 的 vmap 指针


struct dma_buf_ops

可以对 struct dma_buf 进行的操作函数集。

struct dma_buf_ops {
    bool cache_sgt_mapping;
    int (*attach)(struct dma_buf *, struct dma_buf_attachment *);
    void (*detach)(struct dma_buf *, struct dma_buf_attachment *);
    int (*pin)(struct dma_buf_attachment *attach);
    void (*unpin)(struct dma_buf_attachment *attach);
    struct sg_table * (*map_dma_buf)(struct dma_buf_attachment *, enum dma_data_direction);
    void (*unmap_dma_buf)(struct dma_buf_attachment *,struct sg_table *, enum dma_data_direction);
    void (*release)(struct dma_buf *);
    int (*begin_cpu_access)(struct dma_buf *, enum dma_data_direction);
    int (*end_cpu_access)(struct dma_buf *, enum dma_data_direction);
    int (*mmap)(struct dma_buf *, struct vm_area_struct *vma);
    int (*vmap)(struct dma_buf *dmabuf, struct iosys_map *map);
    void (*vunmap)(struct dma_buf *dmabuf, struct iosys_map *map);
};

成员介绍:

cache_sgt_mapping:

如果为 true,框架将缓存为每个附加创建的第一个映射。这避免了多次创建附加映射。

attach:

此回调函数由 dma_buf_attach() 调用,以确保给定的 dma_buf_attachment.dev 可以访问提供的 dma_buf。支持位于特殊位置(例如 VRAM 或设备特定的 carveout 区域)的缓冲区对象的导出器应检查缓冲区是否可以移动到系统内存(或由提供的设备直接访问),否则应使连接操作失败。

导出器通常还应检查当前分配是否满足新设备的 DMA 约束。如果不满足,并且无法移动分配,则连接操作也应失败。

任何导出器私有的内部管理数据都可以存储在 dma_buf_attachment.priv 指针中。

此回调函数是可选的。

返回:成功时返回 0,失败时返回负错误代码。它可能会返回 -EBUSY,表示后备存储已分配,且不符合请求设备的要求。

detach:

此回调由 dma_buf_detach() 调用,用于释放 dma_buf_attachment。提供此回调的目的是为了让导出器能够清理 dma_buf_attachment 的日常事务。

此回调是可选的。

pin:

此函数由 dma_buf_pin() 调用,用于告知导出器 DMA 缓冲区无法再移动。理想情况下,导出器应该将缓冲区固定,以便所有设备都可以访问。

此函数在 dmabuf.resv 对象锁定的情况下调用,并且与 cache_sgt_mapping 互斥。

非动态导入器会从 dma_buf_attach() 中自动调用此函数。

请注意,与非动态导出器在其 map_dma_buf 回调中类似,驱动程序必须保证在此函数返回时内存可用,并且所有旧数据都已清除。内部流水线化缓冲区移动的驱动程序必须等待所有移动和清除操作完成。

返回:成功时返回 0,失败时返回负错误代码。

unpin:

此函数由 dma_buf_unpin() 调用,用于告知导出器 DMA 缓冲区可以再次移动。

此回调在 dmabuf->resv 对象锁定的情况下调用,并且与 cache_sgt_mapping 互斥。

此回调是可选的。

map_dma_buf:

此函数由 dma_buf_map_attachment() 调用,用于将共享的 dma_buf 映射到设备地址空间,并且是强制的。只有在成功调用了附加函数后才能调用此函数。

此函数可能会处于休眠状态,例如,当需要首次分配后备存储,或将其移动到适合所有当前连接设备的位置时。

请注意,此函数所需的任何特定缓冲区属性都应添加到 device_dma_parameters 中,可通过 dma_buf_attachment 中的 device.dma_params 访问。附加函数也应检查这些约束。

如果这是首次调用此函数,导出器现在可以选择扫描此缓冲区的附加函数列表,整理已连接设备的要求,并为缓冲区选择合适的后备存储。

基于枚举 dma_data_direction,可能允许多个用户同时访问(例如读取),或者允许导出器希望提供给缓冲区用户的任何其他类型的共享。

当 dynamic_mapping 标志为 true 时,此函数始终在 dmabuf->resv 对象锁定的情况下调用。

请注意,对于非动态导出器,驱动程序必须保证在此函数返回时内存可用且所有旧数据均已清除。内部流水线化缓冲区移动的驱动程序必须等待所有移动和清除操作完成。动态导出器无需遵循此规则:对于非动态导入器,缓冲区已通过 pin 固定,其要求相同。另一方面,动态导入器需要遵守 dma_resv 栅栏。

返回:

DMA 缓冲区后备存储的 sg_table 散点列表,已映射到使用提供的 dma_buf_attachment 连接的设备的设备地址空间中。散布列表中的地址和长度按 PAGE_SIZE 对齐。

失败时,返回一个包装在指针中的负错误值。如果在阻塞期间收到信号,也可能返回 -EINTR。

请注意,导出器不应尝试缓存散布列表,也不要对多个调用返回相同的列表。缓存由 DMA-BUF 代码(对于非动态导入器)或导入器完成。散布列表的所有权将转移给调用者,并由 unmap_dma_buf 返回。

unmap_dma_buf:

此函数由 dma_buf_unmap_attachment() 调用,用于取消映射并释放在 map_dma_buf 中分配的 sg_table,并且此函数是强制的。对于静态 dma_buf 处理,如果这是 DMA 缓冲区的最后一次映射,此函数还可能取消绑定后备存储。

release:

此函数在最后一次 dma_buf_put 调用后调用,用于释放 dma_buf,并且是强制的。

begin_cpu_access:

此函数由 dma_buf_begin_cpu_access() 调用,允许导出器确保内存对于 CPU 访问是一致的。导出器还需要确保 CPU 访问对于访问方向是一致的。导出器可以使用该方向来优化缓存刷新,例如,使用不同方向(读而不是写)的访问可能会返回过时甚至错误的数据(例如,当导出器需要将数据复制到临时存储时)。

请注意,对于通过 mmap 建立的用户空间映射,以及通过 vmap 建立的内核映射,此回调都会通过 DMA_BUF_IOCTL_SYNC IOCTL 命令调用。

此回调是可选的。

返回:成功时返回 0,失败时返回负错误代码。例如,当无法分配后备存储时,此回调可能会失败。当调用被中断并需要重新启动时,也可能返回 -ERESTARTSYS 或 -EINTR。

end_cpu_access:

当导入器完成对 CPU 的访问时,dma_buf_end_cpu_access() 会调用此函数。导出器可以使用它来刷新缓存并撤消 begin_cpu_access 中执行的任何其他操作。

此回调是可选的。

返回:成功时返回 0,失败时返回负错误代码。当调用被中断并需要重新启动时,可以返回 -ERESTARTSYS 或 -EINTR。

mmap:

此回调由 dma_buf_mmap() 函数使用。

请注意,映射必须是非一致性的,用户空间应使用 DMA_BUF_IOCTL_SYNC 接口来限制 CPU 访问。

由于 dma-buf 缓冲区在其生命周期内大小保持不变,因此 dma-buf 核心会检查 vma 是否过大并拒绝此类映射。因此,导出器无需重复此检查。驱动程序也无需自行检查。

如果导出器需要手动刷新缓存,并因此需要伪造一致性以支持 mmap,则它需要能够清除所有指向后备存储的 pte。现在,Linux mmp 需要一个与存储在 vma->vm_file 中的 struct file 关联的 struct address_space 结构体,以便使用函数 unmap_mapping_range 执行此操作。但是 dma_buf 框架仅支持每个 dma_buf 文件描述符 (fd) 都使用 anon_file 结构体 file,即所有 dma_buf 共享同一个文件。

因此,导出器需要通过设置 vma->vm_file 并在 dma_buf mmap 回调中调整 vma->vm_pgoff 来设置自己的文件(和 address_space)关联。在 gem 驱动程序的特定情况下,导出器可以使用 gem 已提供的共享内存文件(并设置 vm_pgoff = 0)。然后,导出器可以通过取消映射与其自身文件关联的 struct address_space 的相应范围来释放 ptes。

此回调是可选的。

返回:成功时返回 0,失败时返回负错误代码。

vmap:

[可选] 为缓冲区创建到内核地址空间的虚拟映射。适用与 vmap 及其类似函数相同的限制。

vunmap
[可选] 从缓冲区取消 vmap 的映射


struct dma_buf

共享缓冲区对象

struct dma_buf {
    size_t size;
    struct file *file;
    struct list_head attachments;
    const struct dma_buf_ops *ops;
    unsigned vmapping_counter;
    struct iosys_map vmap_ptr;
    const char *exp_name;
    const char *name;
    spinlock_t name_lock;
    struct module *owner;
    struct list_head list_node;
    void *priv;
    struct dma_resv *resv;
    wait_queue_head_t poll;
    struct dma_buf_poll_cb_t {
        struct dma_fence_cb cb;
        wait_queue_head_t *poll;
        __poll_t active;
    } cb_in, cb_out;
#ifdef CONFIG_DMABUF_SYSFS_STATS;
    struct dma_buf_sysfs_entry {
        struct kobject kobj;
        struct dma_buf *dmabuf;
    } *sysfs_entry;
#endif;
};

成员介绍:

size: 缓冲区的大小;在缓冲区的整个生命周期内保持不变。

file: 用于共享缓冲区和引用计数的文件指针。参见 dma_buf_get() 和 dma_buf_put()。

attachments: dma_buf_attachment 列表,用于指示所有已连接的设备,受 dma_resv 锁 resv 保护。

ops: 与此缓冲区对象关联的 dma_buf_ops。

vmapping_counter: 内部用于引用 dma_buf_vmap() 返回的 vmap。受锁保护。

vmap_ptr: 如果 vmapping_counter > 0,则为当前的 vmap ptr。受锁保护。

exp_name: 导出器的名称;用于调试。参见 DMA_BUF_SET_NAME IOCTL。

name: 用户空间提供的名称;用于记账和调试,在 resv 上受 dma_resv_lock() 保护,在读访问上受 name_lock 保护。

name_lock: 自旋锁,用于保护对name的读访问。

owner: 指向导出器模块的指针;当导出器是内核模块时,用于引用计数。

list_node: 用于 dma_buf 记账和调试的节点。

priv: 此缓冲区对象的导出器专用私有数据。

resv: 链接到此 dma-buf 的预留对象。

隐式同步规则:

支持隐式同步缓冲区访问(例如“Implicit Fence Poll Support”中公开的)的驱动程序必须遵循以下规则。

(1) 对于用户空间 API 认为是读访问的任何内容,驱动程序必须通过 dma_resv_add_fence() 函数添加带有 DMA_RESV_USAGE_READ 标志的读栅栏。这在很大程度上取决于 API 和窗口系统。

(2) 同样,对于用户空间 API 认为是写访问的任何内容,驱动程序必须通过 dma_resv_add_fence() 函数添加带有 DMA_RESV_USAGE_WRITE 标志的写栅栏。

(3) 驱动程序可以始终添加写栅栏,因为这只会导致不必要的同步,而不会引起正确性问题。

(4) 某些驱动程序仅公开同步用户空间 API,而没有跨驱动程序的流水线操作。这些驱动程序不会为其访问设置任何栅栏。例如 v4l。

(5) 驱动程序在检索栅栏作为隐式同步的依赖项时,应使用 dma_resv_usage_rw()。

动态导入器规则:

动态导入器(参见 dma_buf_attachment_is_dynamic())在设置栅栏方面有额外的限制:

(1) 动态导入器必须遵守写入栅栏,并等待其发出信号后才能允许通过设备访问缓冲区的底层存储。

(2) 动态导入器应为无法通过 dma_buf_attach_ops.move_notify 回调立即禁用的任何访问设置栅栏。

重要提示:

所有驱动程序和内存管理相关函数都必须遵守 struct dma_resv 规则,特别是更新和遵守栅栏的规则。有关详细说明,请参阅 enum dma_resv_usage。

poll: 用于用户空间 poll 支持.

cb_in: 用于用户空间 poll 支持.

cb_out: 用于用户空间 poll 支持.

sysfs_entry: 用于在 sysfs 中公开此缓冲区的信息。另请参阅“DMA-BUF statistics”以了解此函数启用的 uapi。

描述:

这表示一个共享缓冲区,通过调用 dma_buf_export() 创建。用户空间表示是一个普通的文件描述符,可以通过调用 dma_buf_fd() 创建。

共享 DMA 缓冲区使用 dma_buf_put() 和 get_dma_buf() 进行引用计数。

设备 DMA 访问由单独的 dma_buf_attachment 结构体处理。


struct dma_buf_attach_ops

附件的导入操作

struct dma_buf_attach_ops {
    bool allow_peer2peer;
    void (*move_notify)(struct dma_buf_attachment *attach);
};

成员介绍:

allow_peer2peer:

如果设置为 true,导入器必须能够处理没有 struct pages 的对等资源(peer resources)。

move_notify:

[可选] 通知 DMA-buf 正在移动

如果提供此回调,框架可以避免在映射存在时固定后备存储。

调用此回调时,必须持有与 dma_buf 关联的预留对象的锁,并且调用映射函数时也必须持有此锁。这确保不会在正在进行的移动操作的同时创建映射。

映射保持有效,并且不会直接受到此回调的影响。但是 DMA-buf 现在可以位于不同的物理位置,因此所有映射都应尽快销毁并重新创建。

此回调返回后,可以创建新的映射,并将其指向 DMA-buf 的新位置。

描述: 导入器实现的附加操作。


struct dma_buf_attachment

保存设备缓冲区附加数据。

struct dma_buf_attachment {
    struct dma_buf *dmabuf;
    struct device *dev;
    struct list_head node;
    struct sg_table *sgt;
    enum dma_data_direction dir;
    bool peer2peer;
    const struct dma_buf_attach_ops *importer_ops;
    void *importer_priv;
    void *priv;
};

成员介绍:

dmabuf: 此附件的缓冲区。

dev: 连接到缓冲区的设备。

node: dma_buf_attachment 列表,受 dmabuf 的 dma_resv 锁保护。

sgt: 缓存映射(缓存是散列表)。

dir: 缓存映射的方向。

peer2peer: 如果导入器可以处理无页面的对等资源,则为 true。

importer_ops: 此附件的导入器操作(如果提供),必须在持有 dma_resv 锁的情况下调用 dma_buf_map()/unmap_attachment()。

importer_priv: 导入器特定的附件数据。

priv: 导出器特定的附件数据。

描述:

此结构体保存 dma_buf 缓冲区与其用户设备之间的附件信息。该列表包含每个连接到缓冲区的设备对应的附件结构体。

通过调用 dma_buf_attach() 来创建连接,并通过调用 dma_buf_detach() 来释放连接。启动传输所需的 DMA 映射本身由 dma_buf_map_attachment() 创建,并通过调用 dma_buf_unmap_attachment() 来释放。


struct dma_buf_export_info

保存导出 dma_buf 所需的信息.

struct dma_buf_export_info {
    const char *exp_name;
    struct module *owner;
    const struct dma_buf_ops *ops;
    size_t size;
    int flags;
    struct dma_resv *resv;
    void *priv;
};

成员介绍:

exp_name: 导出器的名称 - 用于调试。

owner: 指向导出器模块的指针 - 用于内核模块的引用计数

ops: 将分配器定义的 DMA 缓冲区操作附加到新缓冲区

size: 缓冲区大小 - 在缓冲区的整个生命周期内保持不变

flags: 文件的模式标志

resv: 预留对象,为 NULL 则分配默认值

priv: 将分配器的私有数据附加到此缓冲区

说明:

此结构体保存导出缓冲区所需的信息。仅用于 dma_buf_export()。


DEFINE_DMA_BUF_EXPORT_INFO

DEFINE_DMA_BUF_EXPORT_INFO (name)

导出器辅助宏

参数:

name: export-info 名称

描述:

DEFINE_DMA_BUF_EXPORT_INFO 宏定义 dma_buf_export_info 结构体,将其清零,并预填充 exp_name 值。


void get_dma_buf(struct dma_buf *dmabuf)

get_file 的便捷包装器。

参数:

struct dma_buf *dmabuf: [in] 指向 dma_buf 的指针

描述:

增加 dma-buf 的引用计数,用于驱动程序需要在内核端创建对 dmabuf 的额外引用的情况。例如,导出器需要保留 dmabuf 指针,以便后续导出不会创建新的 dmabuf。


bool dma_buf_is_dynamic(struct dma_buf *dmabuf)

检查 DMA-buf 是否使用动态映射。

参数:

struct dma_buf *dmabuf: 要检查的 DMA-buf

描述:

如果 DMA-buf 导出器希望在 dma_resv 锁定的情况下调用 map/unmap 回调,则返回 true;如果不希望在 dma_resv 锁定的情况下调用,则返回 false。


bool dma_buf_attachment_is_dynamic(struct dma_buf_attachment *attach)

检查 DMA-buf 连接是否使用动态映射

参数:

struct dma_buf_attachment *attach: 要检查的 DMA-buf 连接

描述:

如果 DMA-buf 导入器想要在持有 dma_resv 锁的情况下调用 map/unmap 函数,则返回 true。


二、Reservation Objects

预留对象提供了一种机制来管理与资源关联的 dma_fence 对象容器。一个预留对象可以附加任意数量的栅栏。每个栅栏都带有一个使用参数,用于确定栅栏所代表的操作如何使用资源。RCU 机制用于保护栅栏的读取访问免受锁定的写入端更新的影响。

更多详情,请参阅 dma_resv 结构体。


void dma_resv_init(struct dma_resv *obj)

初始化预留对象

参数:

struct dma_resv *obj: 预留对象


void dma_resv_fini(struct dma_resv *obj)

销毁预留对象

参数:

struct dma_resv *obj: 预留对象


int dma_resv_reserve_fences(struct dma_resv *obj, unsigned int num_fences)

预留空间用于向 dma_resv 对象添加栅栏。

参数:

struct dma_resv *obj: 预留对象

unsigned int num_fences: 要添加的栅栏数量

说明:

应在 dma_resv_add_fence() 之前调用。调用时必须通过 dma_resv_lock() 锁定 obj。

请注意,如果在调用 dma_resv_add_fence() 之前的任何时间解锁 obj,则需要重新预留预分配的槽位。启用 CONFIG_DEBUG_MUTEXES 后会进行验证。

返回值:成功时返回 0,否则返回 -errno


void dma_resv_reset_max_fences(struct dma_resv *obj)

重置栅栏以进行调试

参数:

struct dma_resv *obj: 要重置的 dma_resv 对象

描述:

重置预先预留的栅栏槽位数量,以测试驱动程序是否使用 dma_resv_reserve_fences() 进行了正确的槽位分配。另请参阅 dma_resv_list.max_fences。


void dma_resv_add_fence(struct dma_resv *obj, struct dma_fence *fence, enum dma_resv_usage usage)

向 dma_resv obj 添加栅栏

参数:

struct dma_resv *obj: 预留对象

struct dma_fence *fence: 要添加的栅栏

enum dma_resv_usage usage: 栅栏的使用方法,请参阅枚举 dma_resv_usage

说明:

向槽添加栅栏,必须使用 dma_resv_lock() 锁定 obj,并且必须调用 dma_resv_reserve_fences()。

另请参阅 dma_resv.fence 以了解语义讨论。


void dma_resv_replace_fences(struct dma_resv *obj, uint64_t context, struct dma_fence *replacement, enum dma_resv_usage usage)

替换 dma_resv obj 中的栅栏

参数:

struct dma_resv *obj: 预留对象

uint64_t context: 要替换的栅栏的上下文

struct dma_fence *replacement: 要替换的新栅栏

enum dma_resv_usage usage: 新栅栏的使用方法,请参阅枚举 dma_resv_usage

说明:

用新的栅栏替换具有指定上下文的栅栏。仅当新栅栏完成时,原始栅栏所代表的操作不再能够访问 dma_resv 对象所代表的资源时,此方法才有效。

例如,使用页表更新栅栏替换抢占栅栏,使资源无法访问。


struct dma_fence *dma_resv_iter_first_unlocked(struct dma_resv_iter *cursor)

未锁定的 dma_resv 对象中的第一个栅栏。

参数:

struct dma_resv_iter *cursor: 当前位置的游标

描述:

后续栅栏使用 dma_resv_iter_next_unlocked() 进行迭代。

请注意,迭代器可能会重新启动。累积统计信息或类似操作的代码需要使用 dma_resv_iter_is_restarted() 进行检查。因此,尽可能优先使用锁定的 dma_resv_iter_first()。

返回未锁定的 dma_resv 对象中的第一个栅栏。


struct dma_fence *dma_resv_iter_next_unlocked(struct dma_resv_iter *cursor)

未锁定的 dma_resv 对象中的下一个栅栏。

参数:

struct dma_resv_iter *cursor: 当前位置的游标

说明:

请注意,迭代器可能会重新启动。累积统计信息或类似操作的代码需要使用 dma_resv_iter_is_restarted() 进行检查。因此,尽可能优先使用锁定的 dma_resv_iter_next()。

返回未锁定的 dma_resv 对象的下一个栅栏。


struct dma_fence *dma_resv_iter_first(struct dma_resv_iter *cursor)

锁定的 dma_resv 对象中的第一个栅栏

参数:

struct dma_resv_iter *cursor: 用于记录当前位置的游标

说明:

后续栅栏使用 dma_resv_iter_next_unlocked() 进行迭代。

返回 dma_resv 对象中的第一个栅栏,同时保持 dma_resv.lock 状态。


struct dma_fence *dma_resv_iter_next(struct dma_resv_iter *cursor)

锁定 dma_resv 对象的下一个栅栏

参数:

struct dma_resv_iter *cursor: 用于记录当前位置的游标

描述:

在持有 dma_resv.lock 的情况下,返回 dma_resv 对象的下一个栅栏。


int dma_resv_copy_fences(struct dma_resv *dst, struct dma_resv *src)

将所有栅栏从 src 复制到 dst。

参数:

struct dma_resv *dst: 目标预留对象

struct dma_resv *src: 源预留对象

描述:

将所有栅栏从 src 复制到 dst。必须持有 dst-lock。


int dma_resv_get_fences(struct dma_resv *obj, enum dma_resv_usage usage, unsigned int *num_fences, struct dma_fence ***fences)

在不持有更新侧锁的情况下获取对象的栅栏。

参数:

struct dma_resv *obj: 预留对象

enum dma_resv_usage usage: 控制要包含的栅栏,参见 enum dma_resv_usage。

unsigned int *num_fences: 返回的栅栏数量

struct dma_fence ***fences: 返回的栅栏指针数组(该数组已分配到所需大小,必须由调用者释放)

描述:

从预留对象中检索所有栅栏。返回零或 -ENOMEM。


int dma_resv_get_singleton(struct dma_resv *obj, enum dma_resv_usage usage, struct dma_fence **fence)

获取所有栅栏的单个栅栏

参数:

struct dma_resv *obj: 预留对象

enum dma_resv_usage usage: 控制要包含哪些栅栏,参见 enum dma_resv_usage。

struct dma_fence **fence: 生成的栅栏

说明:

获取代表 resv 对象内所有栅栏的单个栅栏。成功返回 0,失败返回 -ENOMEM。

警告:将栅栏添加回 resv 对象时不能这样使用,因为这可能导致在最终确定 dma_fence_array 时堆栈损坏。

成功返回 0,失败返回负错误值。


long dma_resv_wait_timeout(struct dma_resv *obj, enum dma_resv_usage usage, bool intr, unsigned long timeout)

等待预留对象的栅栏

参数:

struct dma_resv *obj: 预留对象

enum dma_resv_usage usage: 控制要包含哪些栅栏,参见 enum dma_resv_usage。

bool intr: 如果为 true,则执行可中断等待

unsigned long timeout: 超时值(以 jiffies 为单位),或为 0 则立即返回

描述:

调用者无需持有特定锁,但可能已经持有 dma_resv_lock() 返回值:如果被中断,则返回 -ERESTARTSYS;如果等待超时,则返回 0;如果成功,则返回大于 0 的值。


void dma_resv_set_deadline(struct dma_resv *obj, enum dma_resv_usage usage, ktime_t deadline)

设置预留对象栅栏的截止时间

参数:

struct dma_resv *obj: 预留对象

enum dma_resv_usage usage: 控制要包含的栅栏,参见 enum dma_resv_usage。

ktime_t deadline: 请求的截止时间(单调性)

描述:

无需持有 dma_resv 锁即可调用。设置所有按使用情况过滤的栅栏的截止时间。


bool dma_resv_test_signaled(struct dma_resv *obj, enum dma_resv_usage usage)

测试预留对象的栅栏是否已发出信号。

参数:

struct dma_resv *obj: 预留对象

enum dma_resv_usage usage: 控制要包含的栅栏,参见 enum dma_resv_usage。

描述:

调用者无需持有特定的锁,但可能已经持有 dma_resv_lock()。

返回值:

如果所有栅栏都已发出信号,则返回 true,否则返回 false。


void dma_resv_describe(struct dma_resv *obj, struct seq_file *seq)

将 resv 对象的描述转储到 seq_file

参数:

struct dma_resv *obj: 预留对象

struct seq_file *seq: 要将描述转储到的 seq_file

描述:

将 dma_resv 对象内部栅栏的文本描述转储到 seq_file。


enum dma_resv_usage

如何使用 dma_resv 对象中的栅栏

DMA_RESV_USAGE_KERNEL:

仅用于内核内存管理。

这应该仅用于诸如使用 DMA 硬件引擎复制或清除内存以进行内核内存管理之类的操作。

驱动程序在访问受 dma_resv 对象保护的资源之前,必须始终等待这些栅栏。唯一的例外是,当已知资源已通过先前的固定锁定到位时。

DMA_RESV_USAGE_WRITE:

隐式写入同步。

这应该仅用于添加隐式写入依赖项的用户空间命令提交。

DMA_RESV_USAGE_READ:

隐式读取同步。

这应该仅用于添加隐式读取依赖项的用户空间命令提交。

DMA_RESV_USAGE_BOOKKEEP:

无隐式同步。

这应该用于不想参与任何隐式同步的提交。

最常见的情况是抢占栅栏、页表更新、TLB 刷新以及显式同步的用户提交。

当在初始添加栅栏后需要进行隐式同步时,可以根据需要使用 dma_buf_import_sync_file() 将显式同步的用户提交提升为 DMA_RESV_USAGE_READ 或 DMA_RESV_USAGE_WRITE。

描述:

此枚举描述了 dma_resv 对象的不同用例,并控制查询时返回哪些栅栏。

一个重要的事实是,栅栏的顺序是 KERNEL < WRITE < READ < BOOKKEEP,并且当向 dma_resv 对象请求某个用例的栅栏时,也会返回较低用例的栅栏。

例如,当请求 WRITE 栅栏时,也会返回 KERNEL 栅栏。类似地,当请求 READ 栅栏时,也会返回 WRITE 和 KERNEL 栅栏。

已经使用的栅栏可以升级,例如,带有 DMA_RESV_USAGE_BOOKKEEP 的栅栏可以通过再次添加此用途变为 DMA_RESV_USAGE_READ。但是,栅栏永远不会降级,例如,带有 DMA_RESV_USAGE_WRITE 的栅栏永远不会降级为 DMA_RESV_USAGE_READ。


enum dma_resv_usage dma_resv_usage_rw(bool write)

隐式同步辅助函数

参数:

bool write: 如果创建新的隐式同步写入,则返回 true

描述:

返回写入或读取访问的隐式同步使用情况,请参阅枚举 dma_resv_usage 和 dma_buf.resv。


struct dma_resv

保留对象管理缓冲区的围栏

struct dma_resv {
    struct ww_mutex lock;
    struct dma_resv_list __rcu *fences;
};

参数:

lock:

更新侧锁。请勿直接使用,而是使用包装函数,例如 dma_resv_lock() 和 dma_resv_unlock()。

使用预留对象动态管理内存的驱动程序也使用此锁来保护缓冲区对象的状态,例如位置、分配策略或整个命令提交过程。

fences:

添加到 dma_resv 对象的栅栏数组。

通过调用 dma_resv_add_fence() 添加新的栅栏。由于这通常需要在命令提交后立即执行,因此不能失败,因此需要通过调用 dma_resv_reserve_fences() 预留足够的槽位。

描述:

这是一个用于存放 dma_fence 对象的容器,它需要处理多种用例。

一种用途是同步跨驱动程序对 struct dma_buf 的访问,用于动态缓冲区管理,或仅用于处理用户空间中缓冲区不同用户之间的隐式同步。请参阅 dma_buf.resv 了解更多深入讨论。

另一个主要用途是在基于缓冲区的内存管理器中管理驱动程序内的访问和锁定。struct ttm_buffer_object 是这里的典型示例,因为预留对象就是从这里起源的。但在驱动程序中的使用正在扩展,一些驱动程序也使用相同的方案管理 struct drm_gem_object。


struct dma_resv_iter

dma_resv 围栏中的当前位置

struct dma_resv_iter {
    struct dma_resv *obj;
    enum dma_resv_usage usage;
    struct dma_fence *fence;
    enum dma_resv_usage fence_usage;
    unsigned int index;
    struct dma_resv_list *fences;
    unsigned int num_fences;
    bool is_restarted;
};

参数:

obj: 我们要迭代的 dma_resv 对象

usage: 返回此使用率或更低的栅栏。

fence: 当前处理的栅栏

fence_usage: 当前栅栏的使用率

index: 共享栅栏的索引

fences: 共享栅栏;私有,不得解引用

num_fences: 栅栏数量

is_restarted: 如果这是第一个返回的栅栏,则返回 true

说明:

请勿在驱动程序中直接修改此值,请使用访问器函数。

重要提示:

使用无锁迭代器(例如 dma_resv_iter_next_unlocked() 或 dma_resv_for_each_fence_unlocked())时,请注意迭代器可能会被重启。累积统计数据或类似操作的代码需要使用 dma_resv_iter_is_restarted() 进行检查。


void dma_resv_iter_begin(struct dma_resv_iter *cursor, struct dma_resv *obj, enum dma_resv_usage usage)

初始化 dma_resv_iter 对象

参数:

struct dma_resv_iter *cursor: 要初始化的 dma_resv_iter 对象

struct dma_resv *obj: 要迭代的 dma_resv 对象

enum dma_resv_usage usage: 控制要包含哪些栅栏,参见 enum dma_resv_usage。


void dma_resv_iter_end(struct dma_resv_iter *cursor)

清理 dma_resv_iter 对象

参数:

struct dma_resv_iter *cursor: 需要清理的 dma_resv_iter 对象

描述:

确保游标中对栅栏的引用已正确删除。


enum dma_resv_usage dma_resv_iter_usage(struct dma_resv_iter *cursor)

返回当前栅栏的使用情况

参数:

struct dma_resv_iter *cursor: 当前位置的游标

描述:

返回当前正在处理的栅栏的使用情况。


bool dma_resv_iter_is_restarted(struct dma_resv_iter *cursor)

测试这是否是重启后的第一个栅栏

参数:

struct dma_resv_iter *cursor: 当前位置的游标

描述:

如果这是重启后迭代中的第一个栅栏,则返回 true。


dma_resv_for_each_fence_unlocked (cursor, fence)

解锁栅栏迭代器

参数:

cursor: 一个 struct dma_resv_iter 指针.

fence: 当前栅栏

描述:

迭代 struct dma_resv 对象中的栅栏,无需持有 dma_resv.lock,而是使用 RCU 锁。游标需要使用 dma_resv_iter_begin() 初始化,并使用 dma_resv_iter_end() 清理。迭代器内部持有对 dma_fence 的引用,并释放 RCU 锁。

请注意,当游标的 struct dma_resv 被修改时,迭代器可能会重新启动。累积统计信息或类似操作的代码需要使用 dma_resv_iter_is_restarted() 进行检查。因此,尽可能优先使用锁迭代器 dma_resv_for_each_fence()。


dma_resv_for_each_fence (cursor, obj, usage, fence)

栅栏迭代器

参数:

cursor: struct dma_resv_iter 指针

obj: dma_resv 对象指针

usage: 控制返回哪些栅栏

fence: 当前栅栏

描述:

在持有 dma_resv.lock 锁的情况下,迭代 struct dma_resv 对象中的栅栏。all_fences 控制是否也返回共享栅栏。游标初始化是迭代器的一部分,只要持有锁,栅栏就一直有效,因此不会对栅栏进行额外的引用。


int dma_resv_lock(struct dma_resv *obj, struct ww_acquire_ctx *ctx)

锁定预留对象

参数:

struct dma_resv *obj: 预留对象

struct ww_acquire_ctx *ctx: 锁定上下文

描述:

锁定预留对象以进行独占访问和修改。注意,该锁定仅针对其他写入者,读取者将与在 RCU 下执行的写入者并发运行。seqlock 用于在读取者与写入者发生冲突时通知他们。

由于预留对象可能被多方以未定义的顺序锁定,因此如果检测到循环,则会传递 #ww_acquire_ctx 以进行 unwind 操作。请参阅 ww_mutex_lock() 和 ww_acquire_init()。可以通过将 NULL 传递给 ctx 来锁定预留对象。

当返回 -EDEADLK 表示出现死锁情况时,必须解锁 ctx 持有的所有锁,然后在 obj 上调用 dma_resv_lock_slow()。

通过调用 dma_resv_unlock() 解锁。

另请参阅 dma_resv_lock_interruptible() 了解可中断变体。


int dma_resv_lock_interruptible(struct dma_resv *obj, struct ww_acquire_ctx *ctx)

锁定预留对象

参数:

struct dma_resv *obj: 预留对象

struct ww_acquire_ctx *ctx: 锁定上下文

描述:

锁定可中断的预留对象,以进行独占访问和修改。注意,该锁定仅针对其他写入者,读取者将与在 RCU 下执行的写入者并发运行。seqlock 用于在读取者与写入者发生冲突时通知他们。

由于预留对象可能被多方以未定义的顺序锁定,因此如果检测到循环,则会传递 #ww_acquire_ctx 以进行 unwind 操作。请参阅 ww_mutex_lock() 和 ww_acquire_init()。可以通过将 NULL 作为 ctx 传递来锁定预留对象。

当返回 -EDEADLK 表示出现死锁情况时,必须解锁 ctx 持有的所有锁,然后在 obj 上调用 dma_resv_lock_slow_interruptible()。

通过调用 dma_resv_unlock() 解锁。


void dma_resv_lock_slow(struct dma_resv *obj, struct ww_acquire_ctx *ctx)

slowpath 锁定预留对象

参数:

struct dma_resv *obj: 预留对象

struct ww_acquire_ctx *ctx: 锁定上下文

描述:

在死循环后获取预留对象。此函数将休眠,直到锁可用。另请参阅 dma_resv_lock()。

有关可中断变体,另请参阅 dma_resv_lock_slow_interruptible()。


int dma_resv_lock_slow_interruptible(struct dma_resv *obj, struct ww_acquire_ctx *ctx)

slowpath 锁定预留对象,可中断

参数:

struct dma_resv *obj: 预留对象

struct ww_acquire_ctx *ctx: 锁定上下文

描述:

在死循环后获取可中断的预留对象。此函数将休眠,直到锁可用。另请参阅 dma_resv_lock_interruptible()。


bool dma_resv_trylock(struct dma_resv *obj)

尝试锁定预留对象

参数:

struct dma_resv *obj: 预留对象

描述:

尝试锁定预留对象以进行独占访问和修改。注意,该锁仅针对其他写入者,读取者将与使用 RCU 的写入者并发运行。如果读取者与写入者发生冲突,则使用 seqlock 通知读取者。

另请注意,由于未提供上下文,因此无法进行死锁保护,而 trylock 也不需要死锁保护。

如果已获取锁,则返回 true,否则返回 false。


bool dma_resv_is_locked(struct dma_resv *obj

预留对象是否已锁定

参数:

struct dma_resv *obj: 预留对象

描述:

如果互斥锁已锁定,则返回 true;如果未锁定,则返回 false。


struct ww_acquire_ctx *dma_resv_locking_ctx(struct dma_resv *obj)

返回用于锁定对象的上下文

参数:

struct dma_resv *obj: 预留对象

描述:

返回用于锁定预留对象的上下文;如果未使用上下文或对象根本没有锁定,则返回 NULL。

警告:此接口非常糟糕,但 TTM 需要它,因为它不会在一些非常长的调用链中传递 struct ww_acquire_ctx。其他人只是用它来检查是否持有预留。


void dma_resv_unlock(struct dma_resv *obj)

解锁预留对象

参数:

struct dma_resv *obj: 预留对象

描述:

在独占访问后解锁预留对象。


三、DMA Fences

DMA 栅栏(struct dma_fence)是内核内部的同步原语,用于 DMA 操作,例如 GPU 渲染、视频编码/解码或在屏幕上显示缓冲区。

栅栏使用 dma_fence_init() 初始化,并使用 dma_fence_signal() 完成。栅栏与 context 上下文关联,通过 dma_fence_context_alloc() 分配,并且同一上下文中的所有栅栏都是完全有序的

由于栅栏的目的是促进跨设备和跨应用程序的同步,因此有多种使用方法:

(1) 单个栅栏可以作为 sync_file 公开,从用户空间以文件描述符的形式访问,并通过调用 sync_file_create() 创建。这称为显式栅栏,因为用户空间会传递显式的同步点。

(2) 某些子系统也有自己的显式栅栏原语,例如 drm_syncobj。与 sync_file 相比,drm_syncobj 允许更新底层栅栏。

(3) 此外,还有隐式隔离,同步点作为共享 dma_buf 实例的一部分隐式传递。此类隐式隔离通过 dma_buf.resv 指针存储在 struct dma_resv 中。


1. DMA Fence Cross-Driver Contract

由于 dma_fence 提供了跨驱动程序的契约,所有驱动程序都必须遵循相同的规则:

(1) 栅栏必须在合理的时间内完成。表示用户空间提交的内核和着色器的栅栏可能会永远运行,必须由超时和 GPU 挂起恢复代码支持。至少,该代码必须阻止进一步的命令提交,并强制完成所有正在运行的栅栏,例如,当驱动程序或硬件不支持 GPU 重置,或者 GPU 重置因某种原因失败时。理想情况下,驱动程序支持 GPU 恢复,这只会影响有问题的用户空间上下文,而不会影响其他用户空间提交。

(2) 不同驱动程序对“在合理时间内完成”的含义可能有不同的理解。有些挂起恢复代码使用固定的超时时间,而有些则混合使用观察前进进度和越来越严格的超时时间。驱动程序不应试图猜测其他驱动程序对栅栏的超时处理方式。

(3) 为了确保 dma_fence_wait() 不会与其他锁发生死锁,驱动程序应该使用 dma_fence_begin_signalling() 和 dma_fence_end_signalling() 注释到达 dma_fence_signal()(完成隔离)所需的所有代码。

(4) 驱动程序可以在持有 dma_resv_lock() 的同时调用 dma_fence_wait()。这意味着任何完成栅栏所需的代码都无法获取 dma_resv 锁。请注意,这还会拉入围绕 dma_resv_lock() 和 dma_resv_unlock() 建立的整个锁定层次结构。

(4) 驱动程序可以从其 shrinker 回调中调用 dma_fence_wait()。这意味着任何完成栅栏所需的代码都无法使用 GFP_KERNEL 分配内存。

(5) 驱动程序可以从其 mmu_notifier 或 mmu_interval_notifier 回调中调用 dma_fence_wait()。这意味着任何完成栅栏所需的代码都无法使用 GFP_NOFS 或 GFP_NOIO 分配内存。只允许使用 GFP_ATOMIC,但这可能会失败。

请注意,只有 GPU 驱动程序才有合理的理由同时要求 mmu_interval_notifier 和 shrinker 回调,以及必须使用 dma_fence 跟踪异步计算工作。drivers/gpu 之外的任何驱动程序都不应在这种情况下调用 dma_fence_wait()。


2. DMA Fence Signalling Annotations

通过代码审查和测试来证明 dma_fence 周围所有内核代码的正确性非常棘手,原因如下:

(1) 这是一个跨驱动程序的约定,因此所有驱动程序都必须遵循相同的锁嵌套顺序规则、各种函数的调用上下文以及其他任何对内核接口至关重要的规则。但是,在单台机器上测试所有驱动程序也是不可能的,因此不可能对所有组合进行强力的 N vs. N 测试。即使仅仅限制可能的组合也是不可行的。

(2) 这涉及大量的驱动程序代码。对于渲染驱动程序,需要处理命令提交的尾部代码、发布栅栏后的代码、调度程序代码、用于处理作业完成的中断和工作线程,以及超时、GPU 重置和 GPU 挂起恢复代码。此外,为了与核心 mm 集成,需要分别使用 mmu_notifier、mmu_interval_notifier 和 shrinker。对于模式设置驱动程序,在原子模式集的栅栏发布和相应的 vblank 完成之间需要提交尾部函数,包括任何中断处理和相关的工作线程。审计所有驱动程序中的所有代码是不可行的。

(3) 由于涉及众多其他子系统以及由此引入的锁定层次结构,驱动程序特定差异的调整空间非常小。dma_fence 通过页面错误处理程序 (dma_resv、dma_resv_lock() 和 dma_resv_unlock()) 与几乎所有核心​​内存处理进行交互。另一方面,它还通过 mmu_notifier 和 shrinker 与所有分配站点进行交互。

此外,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() 注释临界区时,需要遵守以下规则:

(1) 完成 dma_fence 所需的所有代码都必须进行注释,从栅栏可供其他线程访问的位置到调用 dma_fence_signal() 的位置。未注释的代码可能包含死锁问题,由于规则非常严格且存在许多极端情况,仅通过审查或常规压力测试无法发现这些问题。

(2) struct dma_resv 需要特别注意,因为读取器仅受 rcu 保护。这意味着信号临界区在新栅栏安装后立即启动,甚至在调用 dma_resv_unlock() 之前。

(3) 唯一的例外是快速路径和机会信号代码,它们调用 dma_fence_signal() 纯粹是为了优化,但并非保证 dma_fence 完成的必要条件。通常的例子是等待 IOCTL 调用 dma_fence_signal(),而强制完成路径则需要经过硬件中断和可能的作业完成工作线程。

(4) 为了提高代码的可组合性,只要整体锁定层次结构一致,注解就可以自由嵌套。注解在中断和进程上下文中均可工作。由于实现细节的原因,这要求调用者将一个不透明的 cookie 从 dma_fence_begin_signalling() 传递到 dma_fence_end_signalling()。

(5) 通过在启动时使用相关层次结构启动 lockdep 来实现对跨驱动程序契约的验证。这意味着即使只使用单个设备进行测试也足以验证驱动程序,至少就 dma_fence_wait() 与 dma_fence_signal() 的死锁而言。


3. DMA Fence Deadline Hints

理想情况下,可以充分流水线化工作负载,以便基于利用率的设备频率调节器能够达到满足用例要求的最低频率,从而最大限度地降低功耗。但在现实世界中,许多工作负载无法达到这一理想状态。例如(但不限于):

(1) 在设备和 CPU 之间来回切换的工作负载,CPU 等待设备,设备等待 CPU,交替出现。这会导致 devfreq 和 cpufreq 在各自的域中出现空闲时间,从而降低频率。

(2) 与周期性基于时间的截止时间交互的工作负载,例如双缓冲 GPU 渲染与vblank同步的页面翻转。在这种情况下,错过垂直空白截止时间会导致 GPU 的空闲时间增加(因为它必须等待额外的vblank周期),从而向 GPU 的 devfreq 发送信号以降低频率,而实际上所需的恰恰相反。

为此,可以通过 dma_fence_set_deadline 在 dma_fence 上设置截止时间提示。截止时间提示为等待的驱动程序或用户空间提供了一种向发出信号的驱动程序传达适当紧急感的方式。

截止时间提示以绝对 ktime 给出(面向用户空间的 API 为 CLOCK_MONOTONIC)。该时间可以是未来的某个时间点(例如,基于 vblank 的页面翻转截止时间,或合成器合成周期的开始时间),也可以是当前时间,以指示立即截止时间提示(即,在该栅栏发出信号之前无法继续前进)。

可以在给定栅栏上设置多个截止时间,甚至可以并行设置。请参阅 dma_fence_ops.set_deadline 的文档。

截止时间提示只是一个提示。创建栅栏的驱动程序可能会通过提高频率、做出不同的调度选择等方式做出反应,或者什么也不做。


4. DMA Fences Functions Reference


... //TODO: 以后再翻译


struct dma_fence

软件同步原语

struct dma_fence {
    spinlock_t *lock;
    const struct dma_fence_ops *ops;
    union {
        struct list_head cb_list;
        ktime_t timestamp;
        struct rcu_head rcu;
    };
    u64 context;
    u64 seqno;
    unsigned long flags;
    struct kref refcount;
    int error;
};

成员介绍:

lock: spin_lock_irqsave 用于锁定

ops: 与此栅栏关联的 dma_fence_ops

{unnamed_union}: 匿名

cb_list: 所有要调用的回调函数列表

timestamp: 栅栏发出信号时的时间戳。

rcu: 用于使用 kfree_rcu 释放栅栏

context: 此栅栏所属的执行上下文,由 dma_fence_context_alloc() 返回

seqno: 此栅栏在执行上下文中的序列号,可用于比较以确定稍后将向哪个栅栏发出信号。

flags: DMA_FENCE_FLAG_* 的掩码,定义如下

refcount: 此栅栏的引用计数

error: 可选,仅当 < 0 时有效,必须在调用 dma_fence_signal 之前设置,表示栅栏已完成并出现错误。

描述:

必须使用适当的原子操作 (bit_*) 来操作和读取 flags 成员,因此大多数情况下无需获取自旋锁。

DMA_FENCE_FLAG_SIGNALED_BIT - 栅栏已发出信号
DMA_FENCE_FLAG_TIMESTAMP_BIT - 用于栅栏信号发送的时间戳
DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT - enable_signaling 可能已被调用
DMA_FENCE_FLAG_USER_BITS - 未使用位的起始位,栅栏实现者可将其用于自身目的。不同的栅栏实现者可能会以不同的方式使用它,因此请勿依赖此位。

由于使用了原子位操作,因此无法保证情况确实如此。具体来说,如果该位已设置,但 dma_fence_signal 函数在设置该位之前被调用,那么它就能够在 enable_signaling 函数调用之前设置 DMA_FENCE_FLAG_SIGNALED_BIT。在设置 DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT 之后添加对 DMA_FENCE_FLAG_SIGNALED_BIT 的检查可以结束这场竞争,并确保在 dma_fence_signal 函数调用之后,任何 enable_signaling 函数调用要么已经完成,要么根本不会被调用。


struct dma_fence_cb

dma_fence_add_callback() 的回调.

struct dma_fence_cb {
    struct list_head node;
    dma_fence_func_t func;
};

成员介绍:

node: dma_fence_add_callback() 使用此节点将此结构体附加到 fence::cb_list

func: 用于调用的 dma_fence_func_t 函数

描述:

此结构体将由 dma_fence_add_callback() 初始化,可以通过将 dma_fence_cb 嵌入到另一个结构体中来传递其他数据。


struct dma_fence_ops

针对围栏实施的操作。

struct dma_fence_ops {
    bool use_64bit_seqno;
    const char * (*get_driver_name)(struct dma_fence *fence);
    const char * (*get_timeline_name)(struct dma_fence *fence);
    bool (*enable_signaling)(struct dma_fence *fence);
    bool (*signaled)(struct dma_fence *fence);
    signed long (*wait)(struct dma_fence *fence, bool intr, signed long timeout);
    void (*release)(struct dma_fence *fence);
    void (*fence_value_str)(struct dma_fence *fence, char *str, int size);
    void (*timeline_value_str)(struct dma_fence *fence, char *str, int size);
    void (*set_deadline)(struct dma_fence *fence, ktime_t deadline);
};

成员介绍:

use_64bit_seqno:

如果此 dma_fence 实现使用 64 位 seqno,则为 true,否则为 false。

get_driver_name:

返回驱动程序名称。这是一个回调函数,允许驱动程序在运行时计算名称,而无需为每个栅栏永久存储该名称,或构建某种缓存。

此回调函数是必需的。

get_timeline_name:

返回此栅栏所属上下文的名称。这是一个回调函数,允许驱动程序在运行时计算名称,而无需为每个栅栏永久存储该名称,或构建某种缓存。

此回调函数是必需的。

enable_signaling:

启用栅栏的软件信号。

对于支持 hw->hw 信号功能的栅栏实现,可以实现此操作来启用必要的中断,或将命令插入 cmdstream 等,以避免在通常只需要 hw->hw 同步的情况下执行这些开销高昂的操作。此操作在第一个 dma_fence_wait() 或 dma_fence_add_callback() 路径中调用,以告知栅栏实现有另一个驱动程序正在等待信号(即 hw->sw 的情况)。

此函数可以在原子上下文中调用,但不能在中断上下文中调用,因此可以使用普通的自旋锁。

返回值为 false 表示栅栏已通过,或者发生了某些故障导致无法启用信号。返回 true 表示启用成功。

可以在 enable_signaling 中设置 dma_fence.error,但仅在返回 false 时才有效。

由于许多实现即使在调用 enable_signaling 之前存在竞争窗口,也可以调用 dma_fence_signal(),此时 dma_fence_signal() 可能会导致最终的栅栏引用被释放,并释放其内存。为了避免这种情况,此回调的实现应该使用 dma_fence_get() 获取其自身的引用,并在栅栏收到信号(例如通过中断处理程序)时释放。

此回调是可选的。如果不存在此回调,则驱动程序必须始终启用信号。

signaled:

检测栅栏是否已发出信号,例如 dma_fence_wait() 或 dma_fence_add_callback() 的快速路径优化。请注意,此回调无需做出任何保证,除了栅栏一旦指示已发出信号,必须始终在此回调中返回 true 之外。即使栅栏已完成,此回调也可能返回 false,在这种情况下,信息尚未在系统中传播。另请参阅 dma_fence_is_signaled()。

如果返回 true,可以设置 dma_fence.error。

此回调是可选的。

wait:

自定义等待实现,如果未设置,则默认为 dma_fence_default_wait()。

已弃用,不应在新实现中使用。仅供需要对其硬件重置过程进行特殊处理的现有实现使用。

如果等待 intr = true 且等待被中断,则必须返回 -ERESTARTSYS;如果栅栏已发出信号,则返回剩余的 jiffies;如果等待超时,则返回 0。自定义实现中也可以返回其他错误值,这些错误值应被视为栅栏已发出信号。例如,可以这样报告硬件锁定。

release:

在栅栏销毁时调用,以释放额外资源。可以从中断上下文调用。此回调是可选的。如果为 NULL,则默认调用 dma_fence_free() 实现。

fence_value_str:

此回调用于填充特定于此栅栏的自由格式调试信息,例如序列号。

此回调是可选的。

timeline_value_str:

以字符串形式填充时间线的当前值,例如序列号。请注意,传递给此函数的特定栅栏无关紧要,驱动程序应仅使用它来查找相应的时间线结构。

set_deadline:

此回调允许栅栏等待器通知栅栏信号器即将到来的截止时间(例如 vblank),等待器希望在此截止时间点之前向栅栏信号器发出信号。这旨在向栅栏信号器提供反馈,以辅助电源管理决策,例如提高 GPU 频率。

此函数无需持有 dma_fence.lock 即可调用,并且可以在任何上下文中多次调用。如果被调用者需要管理某些状态,则锁定由被调用者自行决定。如果设置了多个截止时间,则预期跟踪最早的一个。如果截止时间早于当前时间,则应将其解释为立即截止时间。

此回调是可选的。


... //TODO: 之后再翻译


5. DMA Fence Arra


... //TODO: 之后再翻译


struct dma_fence_array_cb

围栏数组的回调助手

struct dma_fence_array_cb {
    struct dma_fence_cb cb;
    struct dma_fence_array *array;
};

成员:

cb: 用于信号传递的栅栏回调结构.

array: 指向父栅栏数组对象的引用.


struct dma_fence_array

fence 表示一组栅栏

struct dma_fence_array {
    struct dma_fence base;
    spinlock_t lock;
    unsigned num_fences;
    atomic_t num_pending;
    struct dma_fence **fences;
    struct irq_work work;
};

成员介绍:

base: 栅栏基类

lock: 用于栅栏处理的自旋锁

num_fences: 数组中的栅栏数量

num_pending: 数组中仍处于待处理状态的栅栏数量

fences: 栅栏数组

work: 内部 irq_work 函数


... //TODO: 以后再翻译


6. DMA Fence Chain

struct dma_fence_chain

栅栏表示栅栏链的一个节点

struct dma_fence_chain {
    struct dma_fence base;
    struct dma_fence __rcu *prev;
    u64 prev_seqno;
    struct dma_fence *fence;
    union {
        struct dma_fence_cb cb;
        struct irq_work work;
    };
    spinlock_t lock;
};

成员介绍:

base: 栅栏基类

prev: 栅栏链的上一个栅栏

prev_seqno: 垃圾回收前的原始序列号

fence: 封装栅栏

{unnamed_union}: 匿名

cb: 用于发送信号的回调函数

用于添加用于发送栅栏链完成信号的回调函数。切勿与中断工作同时使用。

work: 用于发送信号的irq work项

irq work结构体,允许我们添加回调函数而不会遇到锁反转。切勿与回调同时使用。

lock: 用于栅栏处理的自旋锁


... //TODO: 以后再翻译


7. DMA Fence unwrap

struct dma_fence_unwrap

光标进入容器结构

struct dma_fence_unwrap {
    struct dma_fence *chain;
    struct dma_fence *array;
    unsigned int index;
};

成员介绍:

chain: 潜在的 dma_fence_chain,但也可以是其他 fence

array: 潜在的 dma_fence_array,但也可以是其他 fence

index: 如果 array 确实是 dma_fence_array,则返回最后一个索引

说明:

应与 dma_fence_unwrap_for_each() 迭代器宏一起使用。


... //TODO: 以后再翻译


8. DMA Fence Sync File


... //TODO: 以后再翻译

struct sync_file

导出到用户空间的同步文件

struct sync_file {
    struct file             *file;
    char user_name[32];
#ifdef CONFIG_DEBUG_FS;
    struct list_head        sync_file_list;
#endif;
    wait_queue_head_t wq;
    unsigned long           flags;
    struct dma_fence        *fence;
    struct dma_fence_cb cb;
};

成员介绍:

file: 表示此栅栏的文件

user_name: 用户空间提供的同步文件的名称,用于合并的栅栏。否则,通过驱动程序回调生成(在这种情况下,整个数组为 0)。

sync_file_list: 全局文件列表的成员

wq: 栅栏信号的等待队列

flags: sync_file 的标志

fence: 包含 sync_file 中栅栏的栅栏

cb: 栅栏回调信息

说明:

flags:POLL_ENABLED:用户空间当前是否正在执行 poll()


9. DMA Fence Sync File uABI

struct sync_merge_data

SYNC_IOC_MERGE:合并两个栅栏

struct sync_merge_data {
    char name[32];
    __s32 fd2;
    __s32 fence;
    __u32 flags;
    __u32 pad;
};

成员介绍:

name: 新栅栏的名称

fd2: 第二个栅栏的文件描述符

fence: 将新栅栏的文件描述符返回给用户空间

flags: merge_data 标志

pad: 用于 64 位对齐的填充,应始终为零

描述:

创建一个新的栅栏,其中包含调用方文件描述符和 sync_merge_data.fd2 中 sync_pts 的副本。返回 sync_merge_data.fence 中新栅栏的文件描述符


struct sync_fence_info

详细的栅栏信息

struct sync_fence_info {
    char obj_name[32];
    char driver_name[32];
    __s32 status;
    __u32 flags;
    __u64 timestamp_ns;
};

成员介绍:

obj_name: 父级 sync_timeline 的名称

driver_name: 实现父级的驱动程序的名称

status: fence 的状态 0:active, 1:signaled, <0:error

flags: fence_info 标志

timestamp_ns: 状态变化的时间戳(纳秒)


struct sync_file_info

SYNC_IOC_FILE_INFO:获取有关 sync_file 的详细信息.

struct sync_file_info {
    char name[32];
    __s32 status;
    __u32 flags;
    __u32 num_fences;
    __u32 pad;
    __u64 sync_fence_info;
};

成员介绍:

name: 栅栏名称

status: 栅栏状态。1:已发出信号 0:活动 <0:错误

flags: sync_file_info 标志.

num_fences: sync_file 中的栅栏数量.

pad: 用于 64 位对齐的填充,应始终为零.

sync_fence_info: 指向 sync_fence_info 结构体数组的指针,包含 sync_file 中的所有栅栏.

描述:

接受一个 sync_file_info 结构体。如果 num_fences 为 0,则该字段将更新为实际的栅栏数量。如果 num_fences > 0,系统将使用 sync_fence_info 提供的指针返回最多 num_fences 个 sync_fence_info 结构体,并包含详细的栅栏信息。


10. Indefinite DMA Fences

在不同情况下,有人提出了 struct dma_fence,其时间不确定,直到 dma_fence_wait() 完成。示例包括:

(1) Future 栅栏,在 HWC1 中用于指示缓冲区不再被显示器使用,并在使缓冲区可见的屏幕更新时创建。此栅栏的完成时间完全由用户空间控制。

(2) 代理栅栏,建议用于处理尚未设置栅栏的 &drm_syncobj。用于异步延迟命令提交。

(3) 用户空间栅栏或 GPU futexes,是命令缓冲区内的细粒度锁定,用户空间使用它来进行跨引擎或与 CPU 的同步,然后将其作为 DMA 栅栏导入,以集成到现有的 Winsys 协议中。

(4) 长时间运行的计算命令缓冲区,仍然使用传统的批处理结束 DMA 栅栏进行内存管理,而不是上下文抢占 DMA 栅栏,后者在重新调度计算作业时会重新连接。

所有这些方案的共同点在于,用户空间控制这些栅栏的依赖关系,并控制它们的触发时间。将不确定栅栏与正常的内核 DMA 栅栏混合使用是行不通的,即使设置了回退超时以防范恶意用户空间攻击也是如此:

(1) 只有内核知道所有 DMA 栅栏依赖关系,用户空间无法感知由于内存管理或调度程序决策而注入的依赖关系。

(2) 只有用户空间知道不确定栅栏中的所有依赖关系以及它们何时完成,内核无法感知。

此外,内核必须能够根据内存管理需求延迟用户空间命令的提交,这意味着我们必须支持依赖于 DMA 栅栏的不确定栅栏。如果内核也像 DMA 栅栏一样支持内核中的不确定栅栏(就像上述任何提案一样),则可能会出现死锁。

图1

Indefinite Fencing Dependency Cycle

这意味着内核可能会通过用户空间不知情的内存管理依赖项意外创建死锁,从而随机挂起工作负载,直到超时。从用户空间的角度来看,工作负载不包含死锁。在这种混合隔离架构中,没有任何一个实体了解所有依赖项。因此,从内核内部阻止此类死锁是不可能的。

避免依赖项循环的唯一解决方案是不允许在内核中存在无限期的隔离。这意味着:

(1) 无论是否超时,都不允许将未来隔离、代理隔离或用户空间隔离导入为 DMA 隔离。

(2) 在允许用户空间使用用户空间隔离或长时间运行的计算工作负载的情况下,不允许使用 DMA 隔离来指示批处理缓冲区的结束。这也意味着在这些情况下,共享缓冲区不能进行隐式隔离。


11. Recoverable Hardware Page Faults Implications

现代硬件支持可恢复页面错误,这对 DMA 栅栏 (fence) 有很多影响。

首先,待处理的页面错误显然会阻碍加速器上运行的工作,通常需要分配内存来解决该错误。但是,内存分配不允许控制 DMA 栅栏的完成,这意味着任何使用可恢复页面错误的工作负载都无法使用 DMA 栅栏进行同步。必须使用由用户空间控制的同步栅栏。

这在 GPU 上带来了一个问题,因为 Linux 上当前的桌面合成器协议依赖于 DMA 栅栏,这意味着如果没有在用户空间栅栏之上构建全新的用户空间堆栈,它们就无法从可恢复页面错误中受益。具体来说,这意味着隐式同步将无法实现。例外情况是,页面错误仅用作迁移提示,而不是按需填充内存请求。目前,这意味着 GPU 上的可恢复页面错误仅限于纯计算工作负载。

此外,GPU 通常在 3D 渲染和计算端之间共享资源,例如计算单元或命令提交引擎。如果具有 DMA 栅栏的 3D 作业和使用可恢复页面错误的计算工作负载都处于待处理状态,则它们可能会死锁:

(1) 3D 工作负载可能需要等待计算作业完成并首先释放硬件资源。

(2) 计算工作负载可能会卡在页面错误中,因为内存分配正在等待 3D 工作负载的 DMA 栅栏完成。

有几种方法可以防止此问题,其中一种是驱动程序需要确保的:

(1) 即使页面错误处于待处理状态且尚未修复,计算工作负载也始终可以被抢占。并非所有硬件都支持此功能。

(2) DMA 栅栏工作负载和需要页面错误处理的工作负载拥有独立的硬件资源来保证向前推进。这可以通过例如专用引擎和为 DMA 栅栏工作负载预留最少的计算单元来实现。

(3) 预留方法可以进一步改进,仅在 DMA 栅栏工作负载正在执行时为其预留硬件资源。这必须涵盖从 DMA 栅栏对其他线程可见到栅栏通过 dma_fence_signal() 完成的时间。

(4) 作为最后的手段,如果硬件没有提供有用的预留机制,则在需要 DMA 栅栏的作业和需要页面错误处理的作业之间切换时,必须从 GPU 中清除所有工作负载:这意味着所有 DMA 栅栏必须完成,然后才能将需要页面错误处理的计算作业插入调度程序队列。反之亦然,在 DMA 栅栏在系统中可见之前,必须先抢占所有计算工作负载,以确保所有待处理的 GPU 页面错误都已清除。

(5) 目前唯一可行的方案是在分配内存以修复硬件页面错误时理清这些依赖关系,方法是通过单独的内存块或运行时跟踪所有 DMA 栅栏的完整依赖关系图。这会对内核造成非常广泛的影响,因为在 CPU 端解析页面本身就可能引发页面错误。将处理硬件页面错误的影响限制在特定驱动程序上更为可行且稳健。

请注意,在独立硬件(例如复制引擎或其他 GPU)上运行的工作负载不会产生任何影响。这使得我们即使在解决硬件页面错误时,也可以在内核内部继续使用 DMA 栅栏,例如,使用复制引擎清除或复制解决页面错误所需的内存。

从某种程度上来说,这个页面错误问题是无限 DMA 栅栏讨论的一个特例:来自计算工作负载的无限栅栏可以依赖于 DMA 栅栏,但反过来不行。甚至页面错误问题也并非新问题,因为用户空间中的其他 CPU 线程可能会遇到页面错误,从而阻塞用户空间栅栏 —— 在 GPU 上支持页面错误并非什么根本性的新问题。

 

posted on 2025-08-26 09:56  Hello-World3  阅读(94)  评论(0)    收藏  举报

导航