4. QTI HAL典型死锁场景1:Camera HAL3中Stream与Request Queue的锁竞争
在Qualcomm平台的Camera HAL3实现中,Stream锁与Request Queue锁的竞争是最常见、最难复现的死锁模式之一。该场景通常发生在多线程并发处理Buffer分配、Stream配置更新以及Request入队/出队操作时,由于锁获取顺序不一致导致循环等待。
4.1 场景概述与锁模型
Camera HAL3的核心数据通路涉及两个关键临界区:
- Stream Lock (mStreamLock):保护Stream的配置、Buffer队列和内部状态,通常在
configureStreams()、processCaptureRequest()以及Buffer回调中获取。 - Request Queue Lock (mRequestLock):保护待处理的Capture Request队列,在
processCaptureRequest()入队和后台Dequeue线程出队时获取。
死锁的典型触发路径如下表所示:
| 线程 | 持有锁 | 等待锁 | 操作 |
|---|---|---|---|
| App Thread (Binder) | mStreamLock | mRequestLock | 在processCaptureRequest()中先更新Stream状态,再向队列添加Request |
| Dequeue Thread | mRequestLock | mStreamLock | 从队列取出Request后,需要获取Stream锁来填充Buffer |
关键判断依据:当两个线程分别持有对方需要的锁,且都不释放已持有的锁时,即形成循环等待。在Perfetto trace中,如果看到App线程阻塞在
pthread_mutex_lock(mRequestLock),而Dequeue线程阻塞在pthread_mutex_lock(mStreamLock),即可确认该死锁模式。
4.2 死锁触发流程详解
以下步骤描述了从正常操作到死锁冻结的完整过程:
- App线程发起Capture Request:HAL Service调用
processCaptureRequest(),App线程获取mStreamLock以检查Stream状态是否就绪。 - Stream状态检查:App线程在持有
mStreamLock的情况下,遍历Stream列表,确认Buffer可用。 - Request入队:App线程尝试获取
mRequestLock,将Request插入待处理队列。此时若Dequeue线程已持有mRequestLock,App线程阻塞。 - Dequeue线程处理:后台Dequeue线程持有
mRequestLock从队列取出Request,随后尝试获取mStreamLock以将Buffer填充到Stream中。 - 循环等待形成:App线程持有
mStreamLock等待mRequestLock;Dequeue线程持有mRequestLock等待mStreamLock。两个线程均无法继续执行,系统进入死锁状态。
实战提示:在QTI平台上,该死锁的复现概率与Request吞吐量正相关。当Camera帧率超过30fps或同时开启多路Stream(如Preview+Video+Snapshot)时,锁竞争加剧,死锁更容易触发。
4.3 锁竞争关系SVG图
下图展示了Camera HAL3中Stream Lock与Request Queue Lock的竞争关系及死锁形成路径:
4.4 Perfetto Trace中的关键信号
在Perfetto分析中,识别该死锁需要关注以下Trace信号:
| Trace信号 | 出现位置 | 死锁指示 |
|---|---|---|
futex(0x... FUTEX_WAIT_PRIVATE) |
App线程的Binder调用栈 | App线程在等待mRequestLock对应的futex |
futex(0x... FUTEX_WAIT_PRIVATE) |
Dequeue线程的调用栈 | Dequeue线程在等待mStreamLock对应的futex |
pthread_mutex_lock + 函数名 |
两个线程的堆栈顶部 | 确认锁等待发生在processCaptureRequest和dequeueRequest路径 |
长时间无processCaptureResult回调 |
Camera HAL输出事件流 | 死锁导致HAL无法完成Request处理,无结果返回 |
Perfetto分析技巧:在Trace中搜索
futex系统调用,按等待时间降序排列。如果发现两个线程的futex等待地址不同但互相指向对方持有的锁,且等待时间超过500ms,基本可以判定为死锁。使用sched/sched_switch事件可以精确看到哪个线程在哪个CPU上阻塞。
4.5 锁获取顺序对比
死锁的根源在于锁获取顺序不一致。以下表格对比了正常情况与死锁情况下的锁获取顺序:
| 场景 | App Thread 锁顺序 | Dequeue Thread 锁顺序 | 结果 |
|---|---|---|---|
| 正常 | 先 mRequestLock → 后 mStreamLock | 先 mRequestLock → 后 mStreamLock | 顺序一致,无死锁 |
| 死锁 | 先 mStreamLock → 后 mRequestLock | 先 mRequestLock → 后 mStreamLock | 顺序相反,循环等待 |
4.6 代码级复现示例
以下伪代码展示了死锁的触发路径。注意processCaptureRequest中锁获取顺序的反转:
// App Thread: processCaptureRequest()
void processCaptureRequest(CaptureRequest& req) {
// 错误顺序:先获取 Stream Lock
mStreamLock.lock(); // 持有 mStreamLock
// 检查 Stream 状态
for (auto& stream : mStreams) {
if (!stream.isBufferAvailable()) {
// 等待 Buffer
}
}
// 然后尝试获取 Request Lock
mRequestLock.lock(); // 等待 mRequestLock(可能死锁)
mRequestQueue.push(req);
mRequestLock.unlock();
mStreamLock.unlock();
}
// Dequeue Thread: dequeueAndProcess()
void dequeueAndProcess() {
// 先获取 Request Lock
mRequestLock.lock(); // 持有 mRequestLock
CaptureRequest req = mRequestQueue.pop();
// 然后尝试获取 Stream Lock
mStreamLock.lock(); // 等待 mStreamLock(可能死锁)
// 填充 Buffer 到 Stream
fillBufferToStream(req);
mStreamLock.unlock();
mRequestLock.unlock();
}
修复方案:统一锁获取顺序,确保所有线程都按照先mRequestLock后mStreamLock的顺序获取锁。在
processCaptureRequest中,先释放mStreamLock再获取mRequestLock,或者使用try_lock机制避免长时间阻塞。
4.7 死锁检测与预防策略
在QTI平台上,针对该死锁场景的检测与预防措施包括:
- 静态分析:使用Clang Thread Safety Analysis注解锁获取顺序,在编译期检测不一致。
- 运行时检测:在关键锁操作前后添加
ATRACE_BEGIN/END,通过Perfetto trace分析锁持有时间。 - 锁排序规范:在代码注释中明确锁层级(Lock Order),所有线程必须遵守全局锁顺序表。
- 超时机制:对
pthread_mutex_timedlock设置超时(如100ms),超时后打印堆栈并恢复。 - 死锁检测器:在调试版本中启用
pthread_mutexattr_settype的死锁检测属性(仅Linux支持)。
QTI平台特有注意:Qualcomm的Camera HAL实现中,
mStreamLock可能被多个内部模块(如ISP、JPEG、Video)共享。在修改锁顺序时,需要确认所有模块的锁获取路径,避免引入新的死锁变体。