
1. 问题现场与核心链路梳理1.1 卡死现象与现场代码定位先说这个问题的典型场景。H7S7 这类平台瑞萨 RZ 系列里的一个 SoC内部集成了 PowerVR GPU带独立的 2D 渲染引擎跑 GPU 2D 加速任务时用户态程序通过 NemaGFX 这套库提交渲染命令最终调用到nema_wait_irq_cl这个函数。名字拆开看就很有意思nema_是 NemaGFX 用户态驱动的接口前缀wait_irq是等中断cl大概率是 command list 的缩写。所以这个函数干的事情很简单——提交完一坨命令后傻等 GPU 的中断信号回来告诉用户态“这单活干完了”。正常情况下这个等待应该持续几百微秒到几毫秒就返回。但如果它一直不返回现象通常是应用进程卡死在nema_wait_irq_cl里的等待变量上top或者ps看进程状态是D不可中断睡眠或者R一直自旋取决于驱动是睡得还是死等。如果开了CONFIG_DEBUG_ATOMIC_SLEEP之类内核调试选项可能连内核栈都能打出来栈顶就停在等待中断完成的那个等待队列上。如果加了超时机制nema_wait_irq_cl最终会因为超时返回错误码应用侧打出一条类似“GPU timeout”的日志。仅凭“stuck at nema_wait_irq_cl”这句话其实信息量不大因为它只告诉我们结果——在等中断这一步卡住了。真正的排查难点是中断为什么没来或者说GPU 根本没干完活还是干完了但中断信号丢了这两者的根因和修法完全不同。1.2 nema_wait_irq_cl 在调用链中的位置要搞懂这个卡死点得先捋清一条完整的 GPU 命令提交链路。以典型的三层架构为例应用层调用 NemaGFX 的nema_cmdlist_acquire、nema_cmdlist_commit这类 API把你的 2D 绘制指令画矩形、做 BLIT、填色等编码进一张命令链表command list。用户态驱动层nema_cmdlist_commit内部会走 ioctl 下发到内核同时会把这个 command list 关联到一个 fence信号量上。nema_wait_irq_cl就是在用户态等这个 fence 或者等内核的某个同步对象。内核态驱动层收到 ioctl 后把命令链表提交到 GPU 的硬件队列写相关的寄存器触发 GPU 开始执行。GPU 执行完后会往中断控制器发一个中断内核驱动在中断处理里更新 fence 状态、唤醒等待的进程。所以nema_wait_irq_cl卡死本质上是这条链路的某一环断了要么命令根本没进 GPU要么 GPU 开始执行了但没做完要么做完了但中断没送达 CPU要么中断到了但驱动没有正确唤醒用户态。顺着这个链路一层层排除比盯着nema_wait_irq_cl本身瞎猜要高效得多。1.3 H7S7 平台 GPU2D 任务的执行路径H7S7 集成的 PowerVR GPU 在 2D 场景下的执行路径和 3D 渲染路径有一个显著差异2D 任务的命令提交往往更频繁、单次任务更短。这意味着什么意味着驱动的性能压力集中在“提交-中断”这个往返的损耗上很多平台为了省去内核态和用户态的切换开销会做sync类的优化甚至让某个 CPU 核专门轮询 GPU 的某个寄存器来判断完成状态而不一定真等中断。但这恰恰是最容易出问题的点。我在实际项目里见过好几种卡死现场都是在“轮询还是中断”这个取舍上翻了车。比如GPU 完成了作业但写完成标记的内存地址是 cacheable 的CPU 这边轮询时一直读到旧值。中断被注册成IRQF_SHARED但中断处理函数里没有正确判断是否是自己的中断号导致中断被其他设备消费掉。内核驱动在进入 suspend/低功耗模式前没有正确等待 GPU 排空队列唤醒后 GPU 状态错乱命令卡在队列里永远不执行。H7S7 这类 SoC 还有个特点GPU 的电源域power domain和时钟clock经常挂在 PM 框架里如果驱动的 runtime PM 逻辑有问题在 GPU 还在忙的时候把时钟关了那 GPU 就直接“冻”在那里中断自然永远不来。所以排查方向必须拉宽别只盯着nema_wait_irq_cl这一个函数。它只是一个报警器真正的问题藏在报警器背后那整条链路中。2. 从现象反推根因先判断卡在哪一层2.1 第一步确认是不是真死锁接到这个卡死问题第一件事不是看 GPU 寄存器也不是重编驱动而是先确认这个卡死是“永久性”的还是“偶发性”的是单进程还是全局性的。最简单的判断方法在这个卡死的同时去 shell 里执行一条命令看系统还通不通。如果系统整体还活着只是卡住的那个进程出不来说明 GPU 的中断子系统基本还能工作问题大概率出在命令提交侧或者 fence 管理。如果连cat /proc/interrupts都卡住那系统已经处于比较严重的状态可能是中断被风暴打挂、自旋锁死锁这类更底层的问题。接下来要确认是单次命令卡死还是后续所有命令都卡死。操作方式杀掉卡死的进程重新跑一个最简单的 2D 测试程序。如果简单测试能正常跑通说明 GPU 硬件本身没有完全锁死卡的是特定命令或者特定的资源状态如果简单测试也卡死说明 GPU 的全局状态已经被污染了需要重点排查是不是驱动有资源泄漏、fence 没有回收、或者 GPU 掉进了某个硬件错误状态。这两个问题的排查思路差别很大。前者更像是命令参数错误或者 MMU 页错误后者更像是中断控制器配置错误、电源管理异常、或者有一笔未完成的 DMA 把 GPU 总线地址空间搞乱。2.2 第二步中断到底有没有来等中断卡死最核心的问题是中断到底有没有来。判断方法分两层先看硬件层再看软件层。硬件层最简单的方式是cat /proc/interrupts找到 GPU 对应的中断号连续执行两次看中断计数有没有变化。如果你在卡死状态下触发一次新的 2D 任务然后立刻看中断计数如果计数动了但应用还是卡着说明中断已经到 CPU 了问题在驱动的中断处理或者 fence 唤醒逻辑。如果中断计数完全不动再分两种情况。一是 GPU 压根没产生中断。二是 GPU 产生了中断但被中断控制器屏蔽或者路由错了。区分这两种情况通常需要去读 GPU 的中断状态寄存器。PowerVR 系列的寄存器布局在 NemaGFX 的内核补丁里一般都会有一份头文件定义比如PVR_GPU_IRQ_STATUS、PVR_GPU_IRQ_ENABLE这类寄存器。我建议的做法是先把GPU_IRQ_ENABLE读出来确认该使能的中断源没有被意外关掉。再读GPU_IRQ_STATUS看硬件层面有没有 pending 的中断没有被清掉。读完之后手动向GPU_IRQ_CLEAR写入清除位然后等几毫秒再读一次STATUS如果清除后又立刻重新置位说明 GPU 还在不停地报同一个中断事件可能是 GPU 端根本没认为自己处理完这个任务。这里有个实际踩过坑的细节很多 SoC 的中断控制器会把 GPU 中断配成 level 触发型而不是 edge 触发型。如果驱动在中断处理里没有把 GPU 侧的中断状态清干净就返回了level 型中断会立刻再次触发导致中断风暴看起来像“GPU 一直在中断”但应用侧的 fence 又一直没更新于是大家各卡各的很迷惑。2.3 第三步从 GPU 侧寄存器判断作业状态如果中断确实没有产生下一步就是确认 GPU 到底干没干活。这需要看 GPU 侧的硬件状态寄存器。PowerVR 系列一般有一个GPU_IDLE或者GPU_STATUS之类的寄存器能反映 GPU 的当前状态。操作流程是这样的先读一次GPU_STATUS看 GPU 是 busy 还是 idle。如果 busy说明 GPU 手上还有活要么是命令没执行完要么是卡在某个渲染操作上。如果 idle说明 GPU 已经空闲了但中断没发出来问题就在中断路由、使能或者中断控制器的配置上。如果 busy再去看有没有GPU_FAULT_STATUS一类的寄存器。MMU 页错误、非法访问这类硬件异常GPU 会记录在 fault 状态寄存器里同时把状态置为错误。我在调试一个 2D 旋转命令卡死时就碰到过这种组合GPU_STATUS 显示 idle但 fault 寄存器里躺着一条 MMU page fault指向一个已经被用户态释放的 buffer 地址。这种情况下 GPU 早就放弃治疗了但中断没触发因为 fault 的中断源没有在ENABLE寄存器里打开。这种问题从软件上根本防不住只有把 fault 中断打开让错误在第一时间暴露出来才不至于默默卡死在nema_wait_irq_cl。所以我的习惯是在开发阶段把所有 GPU 错误中断源全部打开包括 MMU fault、page fault、GPU exception 等。虽然会多出一些“噪音”但比起一个完全不可见地卡死早期暴露问题要高效得多。3. 按根因逐项排查和修复方法3.1 GPU MMU 页错误导致的中断异常这是我在实际项目里遇到最多的一类根因。NemaGFX 在用户态分配 buffer、提交给 GPU 时需要把这些 buffer 的物理页映射到 GPU 的 MMU 地址空间。如果应用在 GPU 还在读这块 buffer 的时候就把 buffer 释放了或者 buffer 的映射没有正确刷新 TLBGPU 在取命令或者访问资源时就会触发 MMU 页错误。MMU 页错误的表现形式不是唯一固定的我在调试时发现有两种主要表现第一种是 GPU 直接停摆。GPU 检测到非法访问后置位 fault 状态并暂停当前的命令执行队列这种是“硬故障”接下来所有新提交的命令都不会被执行nema_wait_irq_cl必然卡住而且后续任务也会跟着卡。第二种是 GPU 跳过当前命令继续执行下一条但中间错过的那个 fence 永远不会被 signal结果只有等那个特定 fence 的进程卡死其他任务看似正常。排查方法先读 GPU fault 状态寄存器确认识别到的是哪一种。然后根据 fault 地址去反查是哪个 buffer 的地址再对照用户态的 buffer 生命周期看是不是存在 use-after-free。修复上如果问题出现在应用层需要 NemaGFX 的调用方保证 buffer 的生命周期覆盖到 GPU 任务完成之后。这里提供一个我验证过有效的思路在用户态用一个“pending 队列”管理所有提交给 GPU、但还没收到完成中断的 buffer每次新提交命令前先检查这个 pending 队列把已经完成的 buffer 对应的 fence 回收之后再提交新的能大大降低这类 Use-After-Free 的概率。还有一种情况是用户态驱动和内核态驱动的地址空间不一致导致缓冲区描述符写错了地址。这种问题通常出现在驱动版本不匹配时H7S7 平台如果用了内核里的旧 GPU 驱动补丁和新版 NemaGFX容易出现这个问题。排查时可以先降级到配套版本看看故障是否消失。3.2 中断注册与共享中断配置问题H7S7 这类 SoC 上GPU 中断经常和多个设备共享一个中断线。/proc/interrupts里可以看到类似GIC-0 123 GPU, VPU, ISP这样的行。共享中断本来不是问题但驱动处理的规范是中断处理函数开头必须判断中断状态寄存器确认本次中断是不是自己设备发出的不是就直接返回 IRQ_NONE。如果驱动没做这个判断或者判断条件写错了会出现一种很隐蔽的故障GPU 和 VPU 同时挂在一条中断线上VPU 触发中断时GPU 的驱动也跑去读自己的状态寄存器读出来没有 pending但因为没有正确返回 IRQ_NONE中断子系统认为这个中断已经被“消费”了。而 GPU 自己的中断也同时到了结果两个设备的中断互相干扰GPU 的中断处理函数被调用但状态被误判fence 永远没 signal。排查方法很简单cat /proc/interrupts看中断计数然后去读 GPU 的中断状态寄存器。如果发现中断计数在涨、但 GPU 的 pending 状态寄存器一直是 0就要高度怀疑共享中断处理逻辑有 bug。修复上我建议在 GPU 内核驱动的中断处理函数里把 “读状态寄存器 → 判断是否为本次设备的中断” 作为第一道关卡一旦判断不是自己的中断就立即返回 IRQ_NONE。同时确认中断号的申请方式如果该中断线已经注册为共享GPIO 请求和 IRQ 请求时都要带IRQF_SHARED否则另一个设备去申请的时候会失败或者报错。3.3 命令缓冲区与 fence 生命周期管理问题再讲一个和nema_wait_irq_cl直接相关的场景fence 生命周期管理的 bug。NemaGFX 的 fence 机制和 Linux 内核的dma_fence类似但实现上更轻量。用户态提交一个 command list 之后会拿到一个 fence 对象nema_wait_irq_cl就是在这个 fence 对象上等待。如果内核驱动在命令完成中断里没有正确 signal 对应的 fence或者 signal 错了对象用户态就会一直等下去。这种问题通常出在两个地方一是中断处理里获取 fence 的方式不对。比如内核驱动把 fence 保存在一个全局数组里用“命令 ID”索引但命令 ID 在某种竞争下被覆盖了导致中断时拿到的 fence 是另一个任务的当前任务永远等不到 signal。二是 fence 的引用计数管理有问题。如果dma_fence_wait之前没有正确增加引用计数而 GPU 完成中断来得异常快命令很短GPU 在用户态还没进入等待时就完成了中断处理里可能已经把 fence signal 并且释放了用户态去等一个已经被释放的 fence 对象轻则立即返回错误重则踩到野指针。我排查过一个非常隐蔽的 bug用户在低负载时一切正常高负载时偶尔卡死一次而且是偶发性的。最后发现是驱动里 fence 的 active list 没有加锁中断上下文和提交线程同时操作这个 list导致链表节点丢失fence 对应的回调函数没有被调用。这类问题没有一步到位的修复方法只能通过代码审查和压力测试逐步逼近。我的建议是在所有 fence 相关操作上尽量使用内核提供的标准dma_fence接口不要自己造轮子尤其是 signal 和 wait 的路径上必须保证线程安全。4. 实操排查工具与复现流程4.1 最小复现用例的写法思路排查这种 GPU 卡死问题最忌讳的就是在完整应用里乱试所有因素耦合在一起很难定位根因。正确做法是先写一个最小复现用例只做一件事分配一块 buffer → 填充像素数据 → 触发一次最简单的 BLIT 操作 → 等待 fence。我习惯写一个循环 1000 次的小程序每次提交一次 2D 命令然后等它完成。如果这个用例能稳定复现卡死恭喜你已经拿到了一个价值极高的调试工具。如果复现不了再逐步叠加条件加上旋转、加缩略图生成、加多线程并发提交……直到能稳定复现为止。复现用例里我建议加一个关键日志每次提交后打印这次提交的 job ID、fence 的地址值和时间戳。一旦卡死立刻能从这个日志里看出卡的是哪一单任务和之前有没有哪次提交的异常有关联。很多 GPU 驱动的卡死根本不是第一单卡住而是前面某个任务留下了“脏状态”后面正常的任务才撞上去。如果没有这种任务时间线日志定位会非常痛苦。4.2 常用调试命令和 trace 方法内核侧排查时我常用的工具和命令大致有这几类中断状态cat /proc/interrupts连续读两遍判断中断计数变化。内核日志dmesg -w或者dmesg | tail -n 100重点看有没有 GPU fault、MMU fault 相关的打印。H7S7 平台如果驱动打出了类似于PVR: GPU Fault: 0x0000xxxx address 0xyyyyyyyy的日志那基本可以直接顺着地址去查是哪个 buffer 了。CPU 状态如果进程卡死ps -eLo pid,tid,state,comm,wchan可以看进程的等待点wchan如果显示在nema_wait_irq_cl可以顺藤摸瓜看具体栈。寄存器读取如果驱动里有 debugfs 接口优先用debugfs读取 GPU 寄存器。没有的话devmem也是可以的但需要先确认 GPU 寄存器所在物理地址基址。H7S7 的 PowerVR GPU 地址可以从设备树里查一般在reg属性里定义了。内核态的 ftrace如果怀疑是内核驱动的某个函数没被调用可以开function_graph追踪重点 tracenema_*相关内核函数和中断处理函数的调用情况。如果中断处理函数压根没被调进来说明问题在更早的硬件到软件入口这一段如果调用进来了但 fence 没 signal那就往 fence 管理的方向查。4.3 我这边确认过有效的一组排查清单基于我的经验整理一个排查清单按顺序执行大部分nema_wait_irq_cl卡死问题都能定位到 80% 以上确认系统整体健康排除死锁和中断风暴。确认 GPU 中断计数在持续增加说明中断确实到达了 CPU。读 GPU 的中断状态寄存器确认 GPU 侧信号确实发出。读 GPU 的 fault 状态寄存器确认没有 MMU 页错误、总线错误等硬件异常。确认 GPU IDLE/STATUS判断 GPU 是否真的完成了作业。在内核驱动中断处理函数入口加打印确认中断处理是否有被调用。在 fence signal 路径加打印确认中断时是否找到了正确的 fence 对象。对比频繁卡死任务的前后日志确认第一次报错时前面到底跑过什么命令。这个清单执行完基本可以把问题定位到“硬件中断没发出来”“中断发了但驱动没收到”“驱动收到了但没 signal 对 fence”这三类中的某一类然后再针对性去修效率会高很多。4.4 实操心法先关 cache 局部修改再谈正确性最后分享一个我个人的排错习惯。遇到 GPU 2D 卡死的问题我从来不在原版驱动上直接猜而是会先做一个“减少优化”的 hack 版调试驱动把 GPU 中断线改成非共享模式如果硬件允许的话。把用户态 NemaGFX 的 buffer 分配接口改成禁止 cache 的类型也就是 buffer 用 uncached 属性。在内核驱动里把 fence signal 从“中断上下文”改成“工作队列上下文”虽然增加了延迟但能排除中断上下文的一些并发问题。这套“降级”组合拳打完之后再跑复现用例。如果问题消失就逐个把改动挪回去哪一步挪回去后问题复现了哪一步就是关键嫌疑点。这个方法虽然不够“优雅”但在现场调试时非常高效尤其适合线上紧急、又不想完全从头看代码的场景。这背后有一个朴素的逻辑很多 GPU 卡死问题都是“协议层正确但时序层错误”。把 cache 关了内存一致性问题自然消失把中断改成同步信号中断路由问题自然暴露把 signal 改成工作队列中断上下文竞争问题自然规避。一层层剥离冰山底下的真实根因通常就藏在最不起眼的那一环里。5. 经验总结与后续扩展方向处理这种问题的经验说到底就一句话等中断卡死永远不要只盯着nema_wait_irq_cl这一个函数。它是结果不是原因。真正有价值的是把“用户态提交 → 内核命令队列 → GPU 执行 → 中断产生 → 驱动唤醒”这条链路完整打通每一环节都能用工具和日志观测到问题定位就是时间问题。我个人的建议H7S7 这种平台如果要做 2D 加速产品的量产有两件事值得提前做一是在驱动 debugfs 里暴露完整的 GPU 寄存器读写接口方便现场快速读状态而不是每次都对着设备树查物理地址然后抱着 device 手册翻寄存器偏移二是把 NemaGFX 用户态库的 fence 等待路径加上超时和错误恢复机制超时后主动做一次 GPU 软复位哪怕丢了当前一帧也比整机卡死强得多。后续如果这个卡死问题还伴随着系统休眠唤醒、多进程并发提交、动态调频调压这些场景排查维度还会更多。但核心思路是不变的链路可见化、异常可观测、边界状态可恢复。把这三点做到位GPU 相关的疑难杂症基本都能在可控范围内解决。