VM_BIND 锁定¶
本文档试图描述正确进行 VM_BIND 锁定所需的条件,包括 userptr mmu_notifier 锁定。它还讨论了一些优化,以消除最简单实现中需要的对所有 userptr 映射和外部/共享对象映射的循环。此外,还有一个章节描述了实现可恢复页面错误的 VM_BIND 锁定。
DRM GPUVM 助手集¶
有一组助手用于实现 VM_BIND 的驱动程序,这组助手实现了大部分(但不是全部)本文档中描述的锁定。特别是,它目前缺少 userptr 实现。本文档不打算详细描述 DRM GPUVM 实现,但它包含在 其自身的文档中。强烈建议任何实现 VM_BIND 的驱动程序使用 DRM GPUVM 助手,并在缺少通用功能时对其进行扩展。
命名约定¶
gpu_vm
:具有元数据的虚拟 GPU 地址空间的抽象。通常每个客户端(DRM 文件私有)一个,或者每个执行上下文一个。gpu_vma
:gpu_vm 中具有关联元数据的 GPU 地址范围的抽象。gpu_vma 的后备存储可以是 GEM 对象、匿名页面或页面缓存页面,这些页面也映射到进程的 CPU 地址空间中。gpu_vm_bo
:抽象了 GEM 对象和 VM 的关联。GEM 对象维护一个 gpu_vm_bo 列表,其中每个 gpu_vm_bo 维护一个 gpu_vma 列表。userptr gpu_vma 或者仅仅是 userptr
:一个 gpu_vma,其后备存储是上述的匿名页面或页面缓存页面。revalidating
:重新验证 gpu_vma 意味着使后备存储的最新版本驻留,并确保 gpu_vma 的页表条目指向该后备存储。dma_fence
:类似于结构 completion 的struct dma_fence
,用于跟踪 GPU 活动。当 GPU 活动完成时,dma_fence 会发出信号。请参考 dma-buf 文档的DMA Fences
部分。dma_resv
:一个struct dma_resv
(又名预留对象),用于以 gpu_vm 或 GEM 对象上的多个 dma_fence 的形式跟踪 GPU 活动。dma_resv 包含 dma_fence 的数组/列表和一个锁,该锁需要在将额外的 dma_fence 添加到 dma_resv 时持有。该锁的类型允许以任意顺序对多个 dma_resv 进行死锁安全锁定。请参考 dma-buf 文档的Reservation Objects
部分。exec 函数
:exec 函数是一个重新验证所有受影响的 gpu_vma、提交 GPU 命令批处理并将表示 GPU 命令活动的 dma_fence 注册到所有受影响的 dma_resv 的函数。为了完整起见,虽然本文档未涵盖,但值得一提的是,exec 函数也可能是某些驱动程序在计算/长时间运行模式下使用的重新验证工作者。local object
:一个仅在单个 VM 中映射的 GEM 对象。本地 GEM 对象共享 gpu_vm 的 dma_resv。external object
:又名共享对象:一个可以被多个 gpu_vm 共享并且其后备存储可以与其他驱动程序共享的 GEM 对象。
锁和锁定顺序¶
VM_BIND 的一个好处是,本地 GEM 对象共享 gpu_vm 的 dma_resv 对象,从而共享 dma_resv 锁。因此,即使有大量的本地 GEM 对象,也只需要一个锁即可使 exec 序列成为原子操作。
使用以下锁和锁定顺序
gpu_vm->lock
(可选,一个 rwsem)。保护 gpu_vm 的数据结构,该数据结构跟踪 gpu_vma。它还可以保护 gpu_vm 的 userptr gpu_vma 列表。如果用 CPU mm 类比,这将对应于 mmap_lock。rwsem 允许几个读者同时遍历 VM 树,但是该并发的好处很可能因驱动程序而异。userptr_seqlock
。在读取模式下,对于 gpu_vm 的 userptr 列表中的每个 userptr gpu_vma 都会获取此锁;在写入模式下,在 mmu 通知程序失效期间获取此锁。这不是真正的 seqlock,但在mm/mmu_notifier.c
中描述为“类似于 seqcount 的冲突重试读/写侧‘锁’。但是,这允许多个写侧同时持有它...”。读侧临界区由mmu_interval_read_begin() / mmu_interval_read_retry()
包围,如果持有写侧,则mmu_interval_read_begin()
会休眠。核心 mm 在调用 mmu 区间失效通知程序时持有写侧。gpu_vm->resv
锁。保护 gpu_vm 的需要重新绑定的 gpu_vma 列表,以及所有 gpu_vm 的本地 GEM 对象的驻留状态。此外,它通常保护 gpu_vm 的已逐出和外部 GEM 对象列表。gpu_vm->userptr_notifier_lock
。这是一个 rwsem,在 exec 期间以读取模式获取,在 mmu 通知程序失效期间以写入模式获取。userptr 通知程序锁是每个 gpu_vm 的。gem_object->gpuva_lock
此锁保护 GEM 对象的 gpu_vm_bo 列表。这通常与 GEM 对象的 dma_resv 锁相同,但是某些驱动程序以不同的方式保护此列表,请参见下文。gpu_vm 列表 自旋锁
。对于某些实现,需要它们才能更新 gpu_vm 的已逐出对象和外部对象列表。对于这些实现,在操作列表时会获取自旋锁。但是,为了避免与 dma_resv 锁发生锁定顺序冲突,在遍历列表时需要特殊的方案。
gpu_vm_bo 和 gpu_vma 的保护和生命周期¶
GEM 对象的 gpu_vm_bo 列表和 gpu_vm_bo 的 gpu_vma 列表受 gem_object->gpuva_lock
保护,它通常与 GEM 对象的 dma_resv 相同,但是如果驱动程序需要从 dma_fence 信号临界区内访问这些列表,它可以选择使用单独的锁来保护它,该锁可以从 dma_fence 信号临界区内锁定。然后,此类驱动程序需要额外注意从循环内获取哪些锁,以避免在遍历 gpu_vm_bo 和 gpu_vma 列表时发生锁定顺序冲突。
DRM GPUVM 助手集提供了 lockdep 断言,以确保在相关情况下持有此锁,并且还提供了一种方法,使其可以知道实际使用了哪个锁:drm_gem_gpuva_set_lock()
。
每个 gpu_vm_bo 都持有一个指向底层 GEM 对象的引用计数指针,并且每个 gpu_vma 都持有一个指向 gpu_vm_bo 的引用计数指针。在遍历 GEM 对象的 gpu_vm_bo 列表和 gpu_vm_bo 的 gpu_vma 列表时,不得释放 gem_object->gpuva_lock
,否则,附加到 gpu_vm_bo 的 gpu_vma 可能会在没有通知的情况下消失,因为它们不是引用计数的。驱动程序可以实现自己的方案来允许这样做,但代价是增加了复杂性,但这不在本文档的范围内。
在 DRM GPUVM 实现中,每个 gpu_vm_bo 和每个 gpu_vma 都持有对 gpu_vm 自身的引用计数。因此,并且为了避免循环引用计数,不得从 gpu_vm 的析构函数中完成 gpu_vm 的 gpu_vma 的清理。驱动程序通常实现一个 gpu_vm 关闭函数来进行此清理。gpu_vm 关闭函数将中止使用此 VM 的 gpu 执行,取消映射所有 gpu_vma 并释放页表内存。
本地对象的重新验证和驱逐¶
请注意,在下面给出的所有代码示例中,我们使用简化的伪代码。特别是,省略了 dma_resv 死锁避免算法以及为 dma_resv fences 保留内存。
重新验证¶
对于 VM_BIND,当 gpu 使用 gpu_vm 执行时,所有本地对象都需要驻留,并且这些对象需要设置有效的 gpu_vma 指向它们。因此,通常每个 gpu 命令缓冲区提交之前都有一个重新验证部分
dma_resv_lock(gpu_vm->resv);
// Validation section starts here.
for_each_gpu_vm_bo_on_evict_list(&gpu_vm->evict_list, &gpu_vm_bo) {
validate_gem_bo(&gpu_vm_bo->gem_bo);
// The following list iteration needs the Gem object's
// dma_resv to be held (it protects the gpu_vm_bo's list of
// gpu_vmas, but since local gem objects share the gpu_vm's
// dma_resv, it is already held at this point.
for_each_gpu_vma_of_gpu_vm_bo(&gpu_vm_bo, &gpu_vma)
move_gpu_vma_to_rebind_list(&gpu_vma, &gpu_vm->rebind_list);
}
for_each_gpu_vma_on_rebind_list(&gpu vm->rebind_list, &gpu_vma) {
rebind_gpu_vma(&gpu_vma);
remove_gpu_vma_from_rebind_list(&gpu_vma);
}
// Validation section ends here, and job submission starts.
add_dependencies(&gpu_job, &gpu_vm->resv);
job_dma_fence = gpu_submit(&gpu_job));
add_dma_fence(job_dma_fence, &gpu_vm->resv);
dma_resv_unlock(gpu_vm->resv);
拥有单独的 gpu_vm 重新绑定列表的原因是,可能存在未映射缓冲区对象的 userptr gpu_vma,也需要重新绑定。
驱逐¶
然后,驱逐这些本地对象之一将类似于以下内容
obj = get_object_from_lru();
dma_resv_lock(obj->resv);
for_each_gpu_vm_bo_of_obj(obj, &gpu_vm_bo);
add_gpu_vm_bo_to_evict_list(&gpu_vm_bo, &gpu_vm->evict_list);
add_dependencies(&eviction_job, &obj->resv);
job_dma_fence = gpu_submit(&eviction_job);
add_dma_fence(&obj->resv, job_dma_fence);
dma_resv_unlock(&obj->resv);
put_object(obj);
请注意,由于该对象是 gpu_vm 本地的,因此它将共享 gpu_vm 的 dma_resv 锁,例如 obj->resv == gpu_vm->resv
。标记为驱逐的 gpu_vm_bo 会放置在 gpu_vm 的驱逐列表上,该列表受 gpu_vm->resv
保护。在驱逐期间,所有本地对象都会锁定其 dma_resv,并且由于上述等式,也会锁定保护 gpu_vm 的驱逐列表的 gpu_vm 的 dma_resv。
对于 VM_BIND,无需在驱逐之前取消绑定 gpu_vma,因为驱动程序必须确保驱逐 blit 或 copy 将等待 GPU 空闲或依赖于所有先前的 GPU 活动。此外,GPU 随后尝试通过 gpu_vma 访问释放的内存的任何尝试都将以新的 exec 函数开头,其中包含重新验证部分,该部分将确保所有 gpu_vma 都已重新绑定。在重新验证时持有对象 dma_resv 的驱逐代码将确保新的 exec 函数不会与驱逐竞争。
驱动程序可以以这样一种方式实现:在每个 exec 函数上,仅选择 vma 的子集进行重新绑定。在这种情况下,所有未选择用于重新绑定的 vma 都必须在提交 exec 函数工作负载之前取消绑定。
使用外部缓冲区对象进行锁定¶
由于外部缓冲区对象可以被多个 gpu_vm 共享,因此它们无法与其单个 gpu_vm 共享其预留对象。相反,它们需要拥有自己的预留对象。因此,使用一个或多个 gpu_vma 绑定到 gpu_vm 的外部对象将放置在每个 gpu_vm 的列表上,该列表受 gpu_vm 的 dma_resv 锁或 gpu_vm 列表自旋锁之一保护。锁定 gpu_vm 的预留对象后,可以安全地遍历外部对象列表并锁定所有外部对象的 dma_resv。但是,如果改为使用列表自旋锁,则需要使用更精细的迭代方案。
在驱逐时,需要将外部对象绑定到的所有 gpu_vm 的 gpu_vm_bo 放置在其 gpu_vm 的驱逐列表上。但是,在驱逐外部对象时,通常不会持有该对象绑定到的 gpu_vm 的 dma_resv。只能保证持有该对象的私有 dma_resv。如果在驱逐时有 ww_acquire 上下文可用,我们可以获取这些 dma_resv,但这可能会导致昂贵的 ww_mutex 回滚。一个简单的选择是仅使用一个 evicted
bool 标记被驱逐的 gem 对象的 gpu_vm_bo,该 bool 在下次需要遍历相应的 gpu_vm 驱逐列表之前进行检查。例如,在遍历外部对象列表并锁定它们时。那时,将同时持有 gpu_vm 的 dma_resv 和该对象的 dma_resv,然后可以将标记为已驱逐的 gpu_vm_bo 添加到 gpu_vm 的已驱逐 gpu_vm_bo 列表中。evicted
bool 受该对象的 dma_resv 正式保护。
exec 函数变为
dma_resv_lock(gpu_vm->resv);
// External object list is protected by the gpu_vm->resv lock.
for_each_gpu_vm_bo_on_extobj_list(gpu_vm, &gpu_vm_bo) {
dma_resv_lock(gpu_vm_bo.gem_obj->resv);
if (gpu_vm_bo_marked_evicted(&gpu_vm_bo))
add_gpu_vm_bo_to_evict_list(&gpu_vm_bo, &gpu_vm->evict_list);
}
for_each_gpu_vm_bo_on_evict_list(&gpu_vm->evict_list, &gpu_vm_bo) {
validate_gem_bo(&gpu_vm_bo->gem_bo);
for_each_gpu_vma_of_gpu_vm_bo(&gpu_vm_bo, &gpu_vma)
move_gpu_vma_to_rebind_list(&gpu_vma, &gpu_vm->rebind_list);
}
for_each_gpu_vma_on_rebind_list(&gpu vm->rebind_list, &gpu_vma) {
rebind_gpu_vma(&gpu_vma);
remove_gpu_vma_from_rebind_list(&gpu_vma);
}
add_dependencies(&gpu_job, &gpu_vm->resv);
job_dma_fence = gpu_submit(&gpu_job));
add_dma_fence(job_dma_fence, &gpu_vm->resv);
for_each_external_obj(gpu_vm, &obj)
add_dma_fence(job_dma_fence, &obj->resv);
dma_resv_unlock_all_resv_locks();
相应的共享对象感知驱逐将如下所示
obj = get_object_from_lru();
dma_resv_lock(obj->resv);
for_each_gpu_vm_bo_of_obj(obj, &gpu_vm_bo)
if (object_is_vm_local(obj))
add_gpu_vm_bo_to_evict_list(&gpu_vm_bo, &gpu_vm->evict_list);
else
mark_gpu_vm_bo_evicted(&gpu_vm_bo);
add_dependencies(&eviction_job, &obj->resv);
job_dma_fence = gpu_submit(&eviction_job);
add_dma_fence(&obj->resv, job_dma_fence);
dma_resv_unlock(&obj->resv);
put_object(obj);
在未持有 dma_resv 锁的情况下访问 gpu_vm 的列表¶
某些驱动程序在访问 gpu_vm 的驱逐列表和外部对象列表时将持有 gpu_vm 的 dma_resv 锁。但是,有些驱动程序需要在未持有 dma_resv 锁的情况下访问这些列表,例如由于来自 dma_fence 信号临界区内的异步状态更新。在这种情况下,可以使用自旋锁来保护对列表的操作。但是,由于在遍历列表时需要为每个列表项获取更高级别的休眠锁,因此需要将已遍历的项临时移动到私有列表,并在处理每个项时释放自旋锁
由于额外的锁定和原子操作,可以避免在 dma_resv 锁外部访问 gpu_vm 的列表的驱动程序可能还希望避免使用此迭代方案。特别是,如果驱动程序预计会有大量的列表项。对于预计列表项数量较少、列表迭代不经常发生或者与每次迭代相关的额外成本非常高的列表,与此类迭代相关的原子操作开销很可能可以忽略不计。请注意,如果使用此方案,则必须确保此列表迭代受外部级别的锁或信号量保护,因为在迭代时会暂时从列表中删除列表项,并且还值得一提的是,本地列表 still_in_list
也应被视为受 gpu_vm->list_lock
保护,因此列表项也可能在与列表迭代同时从本地列表中删除。
请参考 DRM GPUVM 锁定部分及其内部的 get_next_vm_bo_from_list()
函数。
userptr gpu_vma¶
userptr gpu_vma 是一个 gpu_vma,它不是将缓冲区对象映射到 GPU 虚拟地址范围,而是直接映射 CPU mm 的匿名或文件页面缓存页面范围。一个非常简单的方法是在绑定时使用 pin_user_pages() 锁定页面,并在取消绑定时取消锁定它们,但是这会创建一个拒绝服务向量,因为单个用户空间进程将能够锁定所有系统内存,这是不可取的。(对于特殊用例并假设正确的核算,锁定可能仍然是理想的功能,尽管如此)。在一般情况下,我们需要做的是获取对所需页面的引用,确保在 CPU mm 取消映射页面之前使用 MMU 通知程序通知我们,如果这些页面未以只读方式映射到 GPU,则将它们标记为脏页,然后删除引用。当我们收到 MMU 通知程序通知 CPU mm 将要删除页面时,我们需要通过等待 MMU 通知程序中的 VM 空闲来停止 GPU 对页面的访问,并确保在 GPU 下次尝试访问 CPU mm 范围中当前存在的任何内容之前,我们从 GPU 页表中取消映射旧页面并重复获取新页面引用的过程。(请参见下面的 通知程序示例)。请注意,当核心 mm 决定清洗页面时,我们会收到这样的取消映射 MMU 通知,并且可以在下次 GPU 访问之前再次将页面标记为脏页。我们还收到类似的 NUMA 核算的 MMU 通知,GPU 驱动程序实际上不需要关心,但是到目前为止,事实证明很难排除某些通知。
在 pin_user_pages() 文档中描述了使用 MMU 通知程序进行设备 DMA(和其他方法)。
现在,不幸的是,不能在 dma_resv 锁下使用使用 get_user_pages() 获取 struct page 引用的方法,因为这会违反 dma_resv 锁与在解析 CPU 页面错误时获取的 mmap_lock 的锁定顺序。这意味着 gpu_vm 的 userptr gpu_vma 列表需要由外部锁保护,在我们的示例中,它是 gpu_vm->lock
。
userptr gpu_vma 的 MMU 区间 seqlock 的使用方式如下
// Exclusive locking mode here is strictly needed only if there are
// invalidated userptr gpu_vmas present, to avoid concurrent userptr
// revalidations of the same userptr gpu_vma.
down_write(&gpu_vm->lock);
retry:
// Note: mmu_interval_read_begin() blocks until there is no
// invalidation notifier running anymore.
seq = mmu_interval_read_begin(&gpu_vma->userptr_interval);
if (seq != gpu_vma->saved_seq) {
obtain_new_page_pointers(&gpu_vma);
dma_resv_lock(&gpu_vm->resv);
add_gpu_vma_to_revalidate_list(&gpu_vma, &gpu_vm);
dma_resv_unlock(&gpu_vm->resv);
gpu_vma->saved_seq = seq;
}
// The usual revalidation goes here.
// Final userptr sequence validation may not happen before the
// submission dma_fence is added to the gpu_vm's resv, from the POW
// of the MMU invalidation notifier. Hence the
// userptr_notifier_lock that will make them appear atomic.
add_dependencies(&gpu_job, &gpu_vm->resv);
down_read(&gpu_vm->userptr_notifier_lock);
if (mmu_interval_read_retry(&gpu_vma->userptr_interval, gpu_vma->saved_seq)) {
up_read(&gpu_vm->userptr_notifier_lock);
goto retry;
}
job_dma_fence = gpu_submit(&gpu_job));
add_dma_fence(job_dma_fence, &gpu_vm->resv);
for_each_external_obj(gpu_vm, &obj)
add_dma_fence(job_dma_fence, &obj->resv);
dma_resv_unlock_all_resv_locks();
up_read(&gpu_vm->userptr_notifier_lock);
up_write(&gpu_vm->lock);
mmu_interval_read_begin()
和 mmu_interval_read_retry()
之间的代码标记了我们所谓的 userptr_seqlock
的读侧临界区。实际上,gpu_vm 的 userptr gpu_vma 列表会被循环访问,并且会检查其所有的 userptr gpu_vma,尽管我们在此处仅显示一个。
可能会从回收上下文中调用 userptr gpu_vma MMU 失效通知程序,并且同样为了避免锁定顺序冲突,我们不能从中获取任何 dma_resv 锁或 gpu_vm->lock。
bool gpu_vma_userptr_invalidate(userptr_interval, cur_seq)
{
// Make sure the exec function either sees the new sequence
// and backs off or we wait for the dma-fence:
down_write(&gpu_vm->userptr_notifier_lock);
mmu_interval_set_seq(userptr_interval, cur_seq);
up_write(&gpu_vm->userptr_notifier_lock);
// At this point, the exec function can't succeed in
// submitting a new job, because cur_seq is an invalid
// sequence number and will always cause a retry. When all
// invalidation callbacks, the mmu notifier core will flip
// the sequence number to a valid one. However we need to
// stop gpu access to the old pages here.
dma_resv_wait_timeout(&gpu_vm->resv, DMA_RESV_USAGE_BOOKKEEP,
false, MAX_SCHEDULE_TIMEOUT);
return true;
}
当此失效通知程序返回时,GPU 不能再访问 userptr gpu_vma 的旧页面,并且需要在新的 GPU 提交成功之前重做页面绑定。
高效的 userptr gpu_vma exec_function 迭代¶
如果 gpu_vm 的 userptr gpu_vma 列表变得很大,则在每个 exec 函数上迭代整个 userptr 列表以检查每个 userptr gpu_vma 的已保存序列号是否过时效率低下。一种解决方案是将所有失效的 userptr gpu_vma 放置在单独的 gpu_vm 列表上,并且仅在每个 exec 函数上检查此列表上存在的 gpu_vma。然后,此列表非常适合于 自旋锁迭代部分中描述的自旋锁锁定方案,因为在 mmu 通知程序中,我们在其中将失效的 gpu_vma 添加到列表,无法获取任何外部锁,例如 gpu_vm->lock
或 gpu_vm->resv
锁。请注意,如该部分中所述,在迭代时仍然需要获取 gpu_vm->lock
以确保列表完整。
如果像这样使用失效的 userptr 列表,则 exec 函数中的重试检查会简单地变成检查失效列表是否为空。
绑定和取消绑定时的锁定¶
在绑定时,假设一个 GEM 对象支持的 gpu_vma,每个 gpu_vma 需要与一个 gpu_vm_bo 相关联,并且该 gpu_vm_bo 依次需要添加到 GEM 对象的 gpu_vm_bo 列表,并且可能添加到 gpu_vm 的外部对象列表。这被称为链接 gpu_vma,并且通常需要持有 gpu_vm->lock
和 gem_object->gpuva_lock
。取消链接 gpu_vma 时应持有相同的锁,这可确保在 gpu_vm->resv
或 GEM 对象的 dma_resv 下迭代 gpu_vma` 时,只要我们迭代的锁未释放,gpu_vma 就会保持活动状态。对于 userptr gpu_vma,同样需要在 vma 销毁期间持有外部 ``gpu_vm->lock
,因为否则在如上一节所述迭代失效的 userptr 列表时,没有任何东西可以使那些 userptr gpu_vma 保持活动状态。
可恢复页面错误页表更新的锁定¶
对于可恢复页面错误的锁定,我们需要确保两件事
当我们返回页面以供系统/分配器重用时,不应再有任何 GPU 映射,并且任何 GPU TLB 都必须已刷新。
gpu_vma 的取消映射和映射不得竞争。
由于 GPU pte 的取消映射(或清零)通常发生在难以甚至不可能获取任何外部级别锁定的地方,因此我们必须引入一个在映射和取消映射时都持有的新锁,或者查看我们在取消映射时确实持有的锁,并确保在映射时也持有它们。对于 userptr gpu_vma,在 mmu 失效通知程序中以写入模式持有 userptr_seqlock
,在此处进行清零。因此,如果在映射期间以读取模式持有 userptr_seqlock
以及 gpu_vm->userptr_notifier_lock
,则它不会与清零竞争。对于 GEM 对象支持的 gpu_vma,清零将在 GEM 对象的 dma_resv 下进行,并确保在为指向 GEM 对象的任何 gpu_vma 填充页表时也持有 dma_resv,这将同样确保我们没有竞争。
如果在释放这些锁的情况下在 dma-fence 下异步执行映射的任何部分,则清零需要在开始修改页表之前等待该 dma-fence 在相关锁下发出信号。
由于以释放页表内存的方式修改页表结构也可能需要外部级别的锁,因此 GPU pte 的清零通常仅专注于将页表或页面目录条目清零并刷新 TLB,而页表内存的释放会延迟到取消绑定或重新绑定时。