ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

第六章:异步访问的同步 — 6.3 隐式同步:dma_resv

第六章:异步访问的同步 — 6.3 隐式同步:dma_resv 6.1 讲清楚了「单个同步对象」dma_fence6.2 讲清楚了隐式/显式两种同步策略。但一块 BO 往往被多方消费每一方都会留下一个代表自身操作的 fence于是 BO 身上挂着的是一组而非单个 fence且这些 fence 之间还存在读写依赖。谁来统一管理这组 fence这正是本节主角dma_resv要解决的问题。1. 为什么单个 dma_fence 不够1.1 共享带来的问题多消费者 读写依赖一块 BObuffer object通常并非被单一使用者独占。以一帧画面为例它可能同时被多方访问GPU 3D 引擎向其中写入渲染结果Display 控制器从中读取数据以完成扫描输出scanout另一个进程通过 DMA-BUF 将其导入同样需要读取内核在搬迁migrate/evict该 BO 时也会插入一次内核管理性质的访问。这些访问大多是异步的提交给硬件后 CPU 立即返回实际完成需等待硬件回调。dma_fence正是用于表示「某次异步操作何时完成」的凭证。问题在于一次访问对应一个 fence而一块 BO 上同时存在多个访问因而产生一组 fence。这组 fence 之间存在读写依赖一个新的写操作必须等之前所有的读和写都完成写要独占一个新的读操作只需等之前的写完成多个读之间可以并发。这套「写等所有、读只等写」的规则本质就是一个读写锁的语义只不过它作用在异步 fence上而不是同步的临界区上。1.2 隐式同步的诉求若每个访问者都需显式查询「前面还有哪些访问未完成、自己应等待哪几个 fence」跨驱动、跨进程协作将变得极其脆弱——生产者无法预知未来的消费者。隐式同步implicit sync追求的效果是访问者只需将代表自身操作的 fence 注册到 BO 上无需知道对方是谁下一个访问者在访问前只要按读写规则检查 BO 上「应等待的那些 fence」是否已 signal即可正确排队。这要求 BO 上存在一个统一的载体用于存放这一组 fence记录每个 fence 的访问类型读/写/内核/记账提供线程安全的增、删、查、等待操作。1.3 dma_resv 的定位BO 的「同步协调员」这个「统一的载体」就是dma_resvreservation object预留对象。核心定位dma_resv是一个自带锁的、线程安全的 dma_fence 容器它按访问类型usage组织一组 fence为共享对象实现隐式同步。换言之使用dma_resv的对象可被多方消费且消费者之间存在依赖。各访问者将「关联自身消费动作的 dma_fence」统一交由dma_resv托管。当某个消费者准备访问时先查询dma_resv按其读写身份需等待的 fence 是否均已 signal若均已完成即可执行否则等待。对应到 BO 上即dma_resv 是 BO 的「同步协调员」每个访问者把代表自己操作的 dma_fence 注册进来并标注读/写身份下一个访问者消费前先按读写规则检查相关 fence 是否 signalsignal 了才能开始从而保证「写等读写、读等写」的正确顺序。为什么不使用 mutex / semaphore因为传统同步原语用于「同步临界区」——线程进入临界区、执行、退出临界区全程 CPU 阻塞。而此处需要协调的是已提交给硬件、CPU 早已返回的异步操作所需的是「注册一组完成凭证、按依赖等待」的能力特性dma_resvmutexspinlockcompletionsemaphore基本用途异步跨设备同步互斥临界区保护一次性事件计数资源管理一组凭证✓✗✗✗✗支持异步 / 回调✓✗✗部分部分区分读/写依赖✓✗✗✗✗RCU 无锁读✓✗✗✗✗典型场景DMA-BUF、GPU、多媒体进程同步内核临界区事件通知资源管理这张表也解释了dma_resv内部为什么是「一把锁 一组 fence」的组合结构。2. dma_resv 的核心模型2.1 结构一把锁 一组 fencedma_resv的结构极简只有两个字段structdma_resv{structww_mutexlock;// 写侧锁改 fence 列表时持有structdma_resv_list__rcu*fences;// fence 数组RCU 保护支持无锁读};lockww_mutex所有修改fence 列表的操作都需先获取这把锁。它并非普通 mutex而是 wound/wait mutex——专为「一次锁定多个 BO」而设计见 §2.2 第二点。fencesRCU 指针实际存放 fence 的数组用 RCU 保护使读侧可不加锁地并发迭代这对性能敏感的查询路径例如 display 判断是否可翻页 page flip至关重要。数组中的每个 fence 都带一个usage 标签区分它是读、写、内核还是记账。整体关系如下2.2 三个关键设计维度围绕「一把锁 一组 fence」这个骨架dma_resv有三处需单独深入的设计① fence 分类读 / 写 / 内核 / 记账usage 层级fence 并非无差别地堆叠在一起而是按enum dma_resv_usage分为四级语义严格有优先级enumdma_resv_usage{DMA_RESV_USAGE_KERNEL,// 内核管理如页表更新、清零级别最高DMA_RESV_USAGE_WRITE,// 隐式写同步DMA_RESV_USAGE_READ,// 隐式读同步DMA_RESV_USAGE_BOOKKEEP,// 只记账不参与隐式同步};它决定了「等谁、被谁等」数值越小级别越高等待某一级时会连带等待所有更高级别。等待规则新的 WRITE等 KERNEL WRITE READ新的 READ等 KERNEL WRITEKERNEL 内核操作(最高必须等)WRITE 写READ 读BOOKKEEP 记账(不参与隐式同步)这套层级机制为什么是四级、升级/降级规则、怎么按 usage 迭代是 6.3 的核心之一 → 详见6.3.1 dma_resv_usage 层级机制详解。② 写侧加锁为什么是 ww_mutex 而不是普通 mutex修改 fence 列表需加锁这符合直觉。但为何使用ww_mutexwound/wait mutex这类开销更高的锁因为命令提交时经常需要同时锁住一批 BO见第 ③ 点多个线程以不同顺序锁定多个 BO 就会产生 ABBA 死锁。ww_mutex通过「上下文时间戳 wound/wait 回退」机制在检测到潜在死锁时让较晚者主动释放锁并重试从而无死锁地批量加锁。ww_mutex 的死锁避免原理 → 详见6.3.2 ww_mutex 多锁场景下的死锁避免机制。③ 多 BO 加锁命令提交要一次锁一批一次 GPU 命令提交command submission通常涉及几十上百个 BO需要将它们的dma_resv-lock全部锁住才能安全地校验、搬迁、加 fence。手工实现这套「批量加锁 死锁回退 出错解锁」既繁琐又易错因此内核抽象出drm_exec框架统一处理。drm_exec 如何封装批量加锁 → 详见6.3.3 drm_exec 多 BO 无死锁加锁框架。2.3 读写两条路径总览综合上述三个维度与dma_resv交互实际上只有两条路径API 全景如下读侧查询 / 等待RCU多为无锁dma_resv_iter_* / for_each_fence迭代 fencedma_resv_wait_timeout()按 usage 批量等待dma_resv_test_signaled()非阻塞测试写侧修改 fence 列表持 ww_mutexdma_resv_lock()dma_resv_reserve_fences()预留空间(可能失败)dma_resv_add_fence()加 fence(不会失败)dma_resv_unlock()几个关键约束如下加 fence 分两步、且第二步不可失败先dma_resv_reserve_fences()预留数组空间这一步可能因内存不足失败成功后dma_resv_add_fence()真正挂入——后者保证不失败这样命令提交的临界区内就不会因为「加 fence 失败」而陷入无法恢复的状态。读侧优先走 RCU 无锁迭代dma_resv_iter_*/dma_resv_for_each_fence_unlocked()允许在不持ww_mutex的情况下安全遍历配合迭代器的 restart 机制应对并发修改。等待按 usage 收敛dma_resv_wait_timeout(obj, usage, intr, timeout)会根据传入的 usage 自动等待「该级别及更高级别」的所有 fence把 §2.2① 的层级规则落到实处。常用 API 速览分类接口作用生命周期dma_resv_init()/dma_resv_fini()初始化 / 销毁写侧锁dma_resv_lock()/dma_resv_unlock()加 / 解写侧 ww_mutex加 fencedma_resv_reserve_fences()/dma_resv_add_fence()预留 / 挂入换 fencedma_resv_replace_fences()按 context 替换查询dma_resv_get_fences()/dma_resv_get_singleton()取全部 / 合成单个等待dma_resv_wait_timeout()/dma_resv_test_signaled()阻塞等 / 非阻塞测其他dma_resv_set_deadline()/dma_resv_describe()deadline 提示 / 调试打印3. 一次隐式同步的完整时序综合前述机制用一条最小链路将其串联为完整流程生产者GPU 渲染写→ 消费者Display 扫描读共享一块用 DMA-BUF 导出的 BO。消费者 Display(读)dma_resv (BO 上)生产者 GPU(写)消费者 Display(读)dma_resv (BO 上)生产者 GPU(写)生产者提交渲染CPU 立即返回GPU 异步渲染中消费者要 scanout先问同步协调员GPU 渲染完成安全消费dma_resv_lock()reserve_fences() add_fence(fence_W, USAGE_WRITE)dma_resv_unlock()dma_resv_wait_timeout(usageREAD)读需等写 → 阻塞在 fence_Wfence_W.signal()等待返回依赖已满足启动 scanout 读取 BO对应的最小写侧代码片段隐式同步的「注册」动作structdma_buf*dmabuf...;structdma_resv*resvdmabuf-resv;dma_resv_lock(resv,NULL);dma_resv_reserve_fences(resv,1);dma_resv_add_fence(resv,fence_W,DMA_RESV_USAGE_WRITE);dma_resv_unlock(resv);/* 之后任何消费者只需 dma_resv_wait_timeout(resv, DMA_RESV_USAGE_READ, ...) 即可正确排队 */4. 小节dma_resv的意义可以浓缩成一句话Synchronization of access is necessary precisely because the object is shared. 因为共享所以同步。而当共享对象被「多方、异步、有读写依赖」地访问时单个 fence 不够于是需要一个容器来管理一组fence——这就是dma_resv。本篇建立的是「一把 ww_mutex 一组带 usage 的 fence」这一骨架模型。骨架之下有三处设计支撑起它的全部语义也是后续三节要各自展开的主题usage 层级——fence 的读写身份如何决定「等谁、被谁等」ww_mutex——写侧锁为何采用 wound/waitdrm_exec——命令提交如何一次性无死锁地锁住一批 BO。三者最终在命令提交路径上汇合构成一次完整的隐式同步。
返回列表