避免优先级反转

本文介绍了 Android 音频系统如何尝试避免优先级反转,并重点介绍了您也可以使用的技术。

这些技术可能对高性能音频应用程序的开发者、OEM 和 SoC 供应商(他们正在实现音频 HAL)非常有用。请注意,实施这些技术并不能保证防止故障或其他失败,尤其是在音频上下文之外使用时。您的结果可能会有所不同,您应该进行自己的评估和测试。

背景

Android AudioFlinger 音频服务器和 AudioTrack/AudioRecord 客户端实现正在进行重新架构,以减少延迟。这项工作始于 Android 4.1,并在 4.2、4.3、4.4 和 5.0 中继续改进。

为了实现更低的延迟,整个系统需要进行许多更改。一个重要的更改是使用更可预测的调度策略为时间关键型线程分配 CPU 资源。可靠的调度允许在仍然避免下溢和上溢的情况下减小音频缓冲区大小和计数。

优先级反转

优先级反转是实时系统的一种经典故障模式,其中较高优先级的任务会被无限期地阻塞,等待较低优先级的任务释放资源,例如(受 互斥锁 保护的)共享状态。

在音频系统中,优先级反转通常表现为爆音(咔哒声、砰砰声、断音)、使用循环缓冲区时的重复音频或命令响应延迟。

优先级反转的常见解决方法是增加音频缓冲区大小。然而,这种方法会增加延迟,并且仅仅是掩盖问题而不是解决问题。最好理解并防止优先级反转,如下所示。

在 Android 音频实现中,优先级反转最有可能发生在以下位置。因此您应该关注此处

  • AudioFlinger 中普通混音器线程和快速混音器线程之间
  • 快速 AudioTrack 的应用程序回调线程和快速混音器线程之间(它们都具有提升的优先级,但优先级略有不同)
  • 快速 AudioRecord 的应用程序回调线程和快速捕获线程之间(与之前类似)
  • 音频硬件抽象层 (HAL) 实现内部,例如用于电话或回声消除
  • 内核中的音频驱动程序内部
  • AudioTrack 或 AudioRecord 回调线程与其他应用程序线程之间(这超出了我们的控制范围)

常用解决方案

典型的解决方案包括

  • 禁用中断
  • 优先级继承互斥锁

在 Linux 用户空间中禁用中断是不可行的,并且不适用于对称多处理器 (SMP)。

优先级继承 futexes(快速用户空间互斥锁)未在音频系统中使用,因为它们相对而言比较重量级,并且因为它们依赖于受信任的客户端。

Android 使用的技术

实验开始于“尝试锁定”和带超时的锁定。这些是互斥锁操作的非阻塞和有界阻塞变体。“尝试锁定”和带超时的锁定效果相当好,但容易受到一些晦涩的故障模式的影响:如果客户端恰好很忙,则无法保证服务器能够访问共享状态,并且如果存在一长串不相关的锁都超时,则累积超时时间可能太长。

我们还使用原子操作,例如

  • 递增
  • 按位“或”
  • 按位“与”

所有这些都返回先前的值,并包括必要的 SMP 屏障。缺点是它们可能需要无限次的重试。在实践中,我们发现重试不是问题。

注意:原子操作及其与内存屏障的交互作用因其经常被误解和错误使用而臭名昭著。我们在此处包含这些方法是为了完整性,但也建议您阅读文章 Android SMP 入门 以获取更多信息。

我们仍然拥有并使用上述大多数工具,并且最近添加了以下技术

  • 对数据使用非阻塞单读单写 FIFO 队列
  • 尝试复制状态,而不是在高优先级和低优先级模块之间共享状态。
  • 当确实需要共享状态时,将状态限制为可以在一次总线操作中原子访问的最大大小,而无需重试。
  • 对于复杂的多字状态,请使用状态队列。状态队列基本上只是一个用于状态而不是数据的非阻塞单读单写 FIFO 队列,只是写入器将相邻的推送折叠为单个推送。
  • 注意 SMP 正确性的内存屏障
  • 信任,但要验证。在进程之间共享状态时,不要假设状态是格式良好的。例如,检查索引是否在界限内。同一进程中的线程之间、相互信任的进程之间(通常具有相同的 UID)不需要此验证。对于共享的数据(例如 PCM 音频),这种损坏也是无关紧要的。

非阻塞算法

非阻塞算法一直是最近研究的课题。但除了单读单写 FIFO 队列外,我们发现它们复杂且容易出错。

从 Android 4.2 开始,您可以在以下位置找到我们的非阻塞单读/写类

  • frameworks/av/include/media/nbaio/
  • frameworks/av/media/libnbaio/
  • frameworks/av/services/audioflinger/StateQueue*

这些是专门为 AudioFlinger 设计的,并非通用用途。非阻塞算法以难以调试而闻名。您可以将此代码作为模型进行参考。但请注意,可能存在错误,并且不能保证这些类适用于其他用途。

对于开发者来说,一些示例 OpenSL ES 应用程序代码应该更新为使用非阻塞算法或引用非 Android 开源库。

我们发布了一个示例非阻塞 FIFO 实现,它是专门为应用程序代码设计的。请参阅平台源代码目录 frameworks/av/audio_utils 中的以下文件

工具

据我们所知,没有自动工具可以查找优先级反转,尤其是在它发生之前。一些研究静态代码分析工具如果能够访问整个代码库,则能够找到优先级反转。当然,如果涉及到任意用户代码(就像应用程序的情况一样)或是一个大型代码库(就像 Linux 内核和设备驱动程序的情况一样),静态分析可能是不切实际的。最重要的是仔细阅读代码,并很好地掌握整个系统及其交互。诸如 systraceps -t -p 之类的工具对于在优先级反转发生后查看它很有用,但不会提前告诉您。

最后的话

经过所有这些讨论,不要害怕互斥锁。互斥锁是您在普通非时间关键用例中正确使用和实现时的朋友。但在高优先级和低优先级任务之间以及在时间敏感的系统中,互斥锁更可能引起麻烦。