避免优先级倒置

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

对于高性能音频应用的开发者、原始设备制造商 (OEM) 和要实现音频 HAL 的 SoC 提供商而言,这些技术可能很有用。请注意,实现这些技术不能保证一定不会出现错误或其他故障,尤其在音频环境之外使用时。使用不同的技术,获得的结果可能也会不同,您需要自己进行评估和测试。

背景

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)。

优先级继承 futex(快速用户空间互斥)在 Linux 内核中可用,但目前并未由 Android C 运行时库 Bionic 采用。由于它们的负载相对较重,且依赖于可信客户端,因此不用于音频系统中。

Android 使用的技术

实验从“尝试锁定”(try lock) 和超时锁定开始。它们是互斥锁定操作的非阻塞和有界阻塞变体。尝试锁定和超时锁定的效果相当不错,但容易受到几个罕见故障模式的影响:如果客户端繁忙,则不能保证服务器一定能够访问共享状态;如果一长系列不相关的锁定都已超时,则累积的超时时间可能会过长。

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

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

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

注意:原子操作及其与内存屏障的互动遭到非常严重的误解和误用。我们在此处提供这些方法,是为了提供完整的信息;不过,建议您同时阅读 SMP Primer for Android 这篇文章,以了解更多信息。

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

  • 针对数据使用非阻塞单读取器单写入器 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 等工具有助于在优先级倒置发生后及时发现,但并不会提前告知您。

总结

经过上述讨论后,请不要害怕互斥。一般情况下,互斥会是您的得力助手,例如,在时间不紧急的一般使用情形中正确使用和实现互斥时。但在高优先级任务和低优先级任务之间以及在时间敏感型系统中,互斥更有可能会导致出现问题。