1. 背景:BO 的并发访问与锁
amdgpu BO(Buffer Object)代表一块 GPU 可访问的内存。在实际运行中,同一个 BO 可能被多个路径并发访问——CPU 端的页表更新、GPU 端的渲染/计算、TTM 的内存迁移/eviction 等。如何保证这些并发操作的正确性?答案是锁。
那么 BO 的锁在哪里?每个 BO 内嵌了一个 struct dma_resv 对象,它包含一个 ww_mutex(wait/wound mutex)和一组 dma_fence。ww_mutex 提供互斥保护,dma_fence 追踪异步 GPU 操作的完成状态。访问 BO 的元数据(placement、backing pages)前需要获取这把锁,这个动作叫 “reserve”。
1.1 为什么用 ww_mutex 而不是普通 mutex?
GPU 操作经常需要同时锁多个 BO。一条渲染命令可能同时引用顶点缓冲、纹理、渲染目标等多个 BO,如果每个 BO 用普通 mutex,不同线程按不同顺序加锁就会产生 ABBA 死锁。ww_mutex(wait/wound mutex)专门解决这个问题:每次加锁操作关联一个全局递增的 ticket(ww_acquire_ctx),冲突时 ticket 小的等待,ticket 大的被 wound(返回 -EDEADLK)并必须释放已持有的锁后重试,从而保证无死锁。
dma_resv 不仅仅是一把锁,而是"锁 + 异步操作追踪 + 跨设备共享"的统一机制:
| 多 BO 无死锁加锁 | ✗ | ✓ (ww_acquire_ctx) |
| 跟踪异步 GPU 操作 | ✗ | ✓ (内嵌 dma_fence 列表) |
| 跨设备共享 | ✗ | ✓ (prime/dma-buf 共享同一个 dma_resv) |
第三点也是 resv 指针可以指向外部对象的原因——dma-buf 导入时,多个设备的 BO 共享同一个 dma_resv,任何一方挂上去的 fence 对方都能看到,实现隐式同步。
理解这个机制的关键在于区分两个容易混淆的概念:resv 指针和 “reserved” 状态。
2. 两个概念
| bo->tbo.base.resv | 指向 struct dma_resv 的指针 | BO 存在期间始终有效 |
| “reserved” 状态 | resv 内部 ww_mutex 被当前线程持有 | 动态获取/释放 |
struct drm_gem_object {
struct dma_resv _resv; // 内嵌的 dma_resv 实例
struct dma_resv *resv; // 指针,通常指向 &_resv
};
resv 指针有两种指向:
- 默认:drm_gem_object_init() 时设置为 &obj->_resv,即自己内嵌的实例(大多数 BO)
- 共享:通过 bp->resv 传入外部 dma_resv(典型场景:dma-buf 导入,多个设备的 BO 共享同一个 dma_resv)
设置后不再改变。
“Reserved” 是指通过 dma_resv_lock() / ttm_bo_reserve() 获取了该 dma_resv 内部的锁。
3. amdgpu_bo_create 中的时序
amdgpu_bo_create(adev, &bp, &bo)
{
// 1. 分配 BO,初始化 resv 指针
drm_gem_object_init() → bo->tbo.base.resv = &bo->tbo.base._resv
// 2. TTM 初始化时加锁
ttm_bo_init_reserved() → dma_resv_lock(bo->tbo.base.resv)
// 此时 BO 处于 "reserved" 状态
// 3. 可选:VRAM clear(需要 reserved 才能 add fence)
amdgpu_ttm_clear_buffer() → dma_resv_add_fence(resv, fence, KERNEL)
// 4. 根据 bp->resv 决定是否解锁
if (!bp->resv)
amdgpu_bo_unreserve(bo); → dma_resv_unlock(bo->tbo.base.resv)
// BO 不再 "reserved",但 resv 指针仍有效
return bo; // resv 指针有效,reserved 状态已释放
}
为什么 !bp->resv 时要 unreserve? 遵循"谁加锁谁解锁"原则:
| bp->resv != NULL | 调用者(外部已持锁) | 调用者(外部负责解锁) |
| bp->resv == NULL | ttm_bo_init_reserved(内部) | amdgpu_bo_create(内部 unreserve) |
传入外部 resv 时,调用者已经持有该锁,函数不能替调用者解锁——调用者可能还需要在锁保护下做后续操作(如把 BO 加入列表)。
没传入时,锁是函数内部加的,调用者对此一无所知,所以函数自己收尾,返回一个"干净"的 unlocked BO。
4. dma_resv API 的锁要求
dma_resv 内部使用 RCU 读写分离:写端(修改 fence 列表)持 ww_mutex 互斥,读端(遍历 fence 列表)走 RCU 无锁路径。判定方法:
- 函数内部调用了 dma_resv_assert_held() → 必须持锁
- 遍历用 dma_resv_for_each_fence_unlocked(RCU) → 无需持锁
| dma_resv_add_fence() | 写 | ✓ | dma_resv_assert_held |
| dma_resv_reserve_fences() | 写 | ✓ | dma_resv_assert_held |
| dma_resv_replace_fences() | 写 | ✓ | dma_resv_assert_held |
| dma_resv_copy_fences() | 写(目标) | ✓ | dma_resv_assert_held(dst) |
| dma_resv_reset_max_fences() | 写 | ✓ | dma_resv_assert_held |
| dma_resv_test_signaled() | 读 | ✗ | dma_resv_for_each_fence_unlocked |
| dma_resv_wait_timeout() | 读 | ✗ | dma_resv_for_each_fence_unlocked |
| dma_resv_get_fences() | 读 | ✗ | 注释 “without update side lock held” |
| dma_resv_get_singleton() | 读 | ✗ | 内部调用 dma_resv_get_fences() |
5. 注意事项
amdgpu_bo_reserve(bo, true) 可能失败——interruptible=true 时可返回 -EINTR,必须检查返回值。在 cleanup 等不可中断路径应传 false。
不要在持 resv 锁时做可能触发 eviction 的内存分配——TTM eviction 会试图 lock 被驱逐 BO 的 resv,如果该 BO 共享同一个 resv,会自锁死锁。
ww_mutex 是睡眠锁——不能在 atomic / interrupt / spinlock 上下文中调用 amdgpu_bo_reserve。
持锁时间应尽量短——resv lock 会阻塞该 BO 的 eviction、CS 提交等所有操作,只在真正需要的临界区内持锁。
6. 总结
- resv 指针:内嵌对象的地址,BO 生命周期内恒定
- “reserved”:运行时锁状态,需要显式 acquire/release
- 读 fence 列表(test_signaled、get_singleton、wait_timeout):RCU 无锁,只需 resv 指针有效
- 写 fence 列表(add_fence、reserve_fences、replace_fences):需要 reserved 状态
- amdgpu 中 bp.resv == NULL 导致创建后自动 unreserve,需要在使用前重新 reserve
![基于Vue的青少年足球夏令营管理系统[Vue]-计算机毕业设计源码+LW文档-171主机测评](https://www.171host.com/wp-content/uploads/2026/10/20261031080405-6ae5a0f50fda4-220x150.jpg)