blob: f3dd9595239e551d2ff2a6177479bf7ee0fc0d0e [file] [log] [blame]
<html devsite><head>
<title>避免优先级倒置</title>
<meta name="project_path" value="/_project.yaml"/>
<meta name="book_path" value="/_book.yaml"/>
</head>
<body>
<!--
Copyright 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<p>
本文介绍了 Android 的音频系统如何尝试避免优先级倒置,还重点介绍了您可以使用的技术。
</p>
<p>
对于高性能音频应用的开发者、原始设备制造商 (OEM) 和要实现音频 HAL 的 SoC 提供商而言,这些技术可能很有用。请注意,实现这些技术不能保证一定不会出现错误或其他故障,尤其在音频环境之外使用时。使用不同的技术,获得的结果可能也会不同,您需要自己进行评估和测试。
</p>
<h2 id="background">背景</h2>
<p>
Android AudioFlinger 音频服务器和 AudioTrack/AudioRecord 客户端实现正在进行架构调整,以缩短延迟时间。这项工作从 Android 4.1 开始,在 4.2、4.3、4.4 和 5.0 中得到了进一步改进。
</p>
<p>
为了缩短延迟时间,需要对整个系统进行大量更改。其中一项重要更改是采用预测性更高的调度策略将 CPU 资源分配给对时间要求严格的线程。可靠的调度可减小音频缓冲区的大小和数目,同时仍可避免欠载和溢出。
</p>
<h2 id="priorityInversion">优先级倒置</h2>
<p>
<a href="http://en.wikipedia.org/wiki/Priority_inversion">优先级倒置</a>是实时系统的一种典型故障模式,在这种模式下,优先级较高的任务会因等待优先级较低的任务释放<a href="http://en.wikipedia.org/wiki/Mutual_exclusion">互斥</a>(保护的共享状态)等资源而无限时受阻。
</p>
<p>
在音频系统中,如果使用环形缓冲区或其在响应命令时延迟,优先级倒置通常表现为音频<a href="http://en.wikipedia.org/wiki/Glitch">错误</a>(咔嗒声、爆裂声、音频丢失)和<a href="http://en.wikipedia.org/wiki/Max_Headroom_(character)">音频重复</a>
</p>
<p>
优先级倒置的常见解决方法是增加音频缓冲区大小。不过,这种方法会增加延迟时间,仅仅将问题隐藏,而非解决问题。最好的方法是了解并防止优先级倒置,如下所示。
</p>
<p>
在 Android 音频实现中,优先级倒置最有可能发生在以下位置。因此,您应该重点关注这些位置:
</p>
<ul>
<li>
AudioFlinger 中的常规混合器线程和快速混合器线程之间
</li>
<li>
快速 AudioTrack 的应用回调线程和快速混合器线程之间(它们的优先级都较高,但略有不同)
</li>
<li>
快速 AudioRecord 的应用回调线程和快速捕获线程之间(与上一种情况类似)
</li>
<li>
在音频硬件抽象层 (HAL) 实现(例如用于电话或回声消除的实现)中
</li>
<li>
在内核的音频驱动程序中
</li>
<li>
AudioTrack 或 AudioRecord 回调线程和其他应用线程(这不在我们的控制范围内)之间
</li>
</ul>
<h2 id="commonSolutions">常用解决方法</h2>
<p>
一般采用的解决方法包括:
</p>
<ul>
<li>
停用中断
</li>
<li>
优先级继承互斥
</li>
</ul>
<p>
停用中断在 Linux 用户空间中不可行,且不适用于对称多处理器 (SMP)。
</p>
<p>
优先级继承 <a href="http://en.wikipedia.org/wiki/Futex">futex</a>(快速用户空间互斥)在 Linux 内核中可用,但目前并未由 Android C 运行时库 <a href="http://en.wikipedia.org/wiki/Bionic_(software)">Bionic</a> 采用。由于它们的负载相对较重,且依赖于可信客户端,因此不用于音频系统中。
</p>
<h2 id="androidTechniques">Android 使用的技术</h2>
<p>
实验从“尝试锁定”(try lock) 和超时锁定开始。它们是互斥锁定操作的非阻塞和有界阻塞变体。尝试锁定和超时锁定的效果相当不错,但容易受到几个罕见故障模式的影响:如果客户端繁忙,则不能保证服务器一定能够访问共享状态;如果一长系列不相关的锁定都已超时,则累积的超时时间可能会过长。
</p>
<p>
我们还使用<a href="http://en.wikipedia.org/wiki/Linearizability">原子操作</a>,例如:
</p>
<ul>
<li>递增</li>
<li>按位“或”</li>
<li>按位“和”</li>
</ul>
<p>
所有这些操作均返回之前的值,并包含必要的 SMP 屏障。缺点在于它们可能需要无限次重试。在实践中,我们发现重试并不是问题。
</p>
<p class="note"><strong>注意</strong>:原子操作及其与内存屏障的互动遭到非常严重的误解和误用。我们在此处提供这些方法,是为了提供完整的信息;不过,建议您同时阅读 <a href="https://developer.android.com/training/articles/smp.html">SMP Primer for Android</a> 这篇文章,以了解更多信息。
</p>
<p>
我们仍然保有和使用上述大多数工具,并在最近添加了以下技术:
</p>
<ul>
<li>
针对数据使用非阻塞单读取器单写入器 <a href="http://en.wikipedia.org/wiki/Circular_buffer">FIFO 队列</a>
</li>
<li>
在高优先级和低优先级模块之间尝试“复制”状态而非“共享”状态。<i></i><i></i>
</li>
<li>
如果确实需要共享状态,请将状态的大小上限设为一个<a href="http://en.wikipedia.org/wiki/Word_(computer_architecture)"></a>,在不重试的情况下,可以在单个总线操作中通过原子方式访问该状态。
</li>
<li>
对于复杂的多字状态,请使用状态队列。状态队列基本上只是用于状态(而非数据)的非阻塞单读取器单写入器 FIFO 队列,写入器将相邻推送收成单个推送这一情况除外。
</li>
<li>
注意<a href="http://en.wikipedia.org/wiki/Memory_barrier">内存屏障</a>,以保证 SMP 的准确性。
</li>
<li>
<a href="http://en.wikipedia.org/wiki/Trust,_but_verify">信任,但要验证</a>。在进程之间共享“状态”时,请勿假定状态的格式正确无误。<i></i>例如,检查索引是否在范围内。在同一个进程中的线程之间,以及在相互信任的进程(通常具有相同的 UID)之间,不需要进行验证。此外,也无需验证共享的“数据”,例如出现非继发性损坏的 PCM 音频。<i></i>
</li>
</ul>
<h2 id="nonBlockingAlgorithms">非阻塞算法</h2>
<p>
<a href="http://en.wikipedia.org/wiki/Non-blocking_algorithm">非阻塞算法</a>是我们最近一直在研究的一项内容。不过,除了单读取器单写入器 FIFO 队列之外,我们发现此类算法复杂且容易出错。
</p>
<p>
从 Android 4.2 开始,您可以在以下位置找到我们的非阻塞单读取器/写入器类:
</p>
<ul>
<li>
frameworks/av/include/media/nbaio/
</li>
<li>
frameworks/av/media/libnbaio/
</li>
<li>
frameworks/av/services/audioflinger/StateQueue*
</li>
</ul>
<p>
它们是专为 AudioFlinger 设计的,并不通用。非阻塞算法因其难以调试而臭名昭著。您可以将此代码视为一种模型。不过请注意,非阻塞算法可能会出现错误,且不能保证这些类一定适合用于其他用途。
</p>
<p>
对于开发者来说,应该更新部分示例 OpenSL ES 应用代码,以使用非阻塞算法或参照非 Android 开放源代码库。
</p>
<p>
我们发布了一个示例非阻塞 FIFO 实现,该实现专为应用代码设计。请在平台源目录 <code>frameworks/av/audio_utils</code> 中查看以下文件:
</p>
<ul>
<li><a href="https://android.googlesource.com/platform/system/media/+/master/audio_utils/include/audio_utils/fifo.h">include/audio_utils/fifo.h</a></li>
<li><a href="https://android.googlesource.com/platform/system/media/+/master/audio_utils/fifo.c">fifo.c</a></li>
<li><a href="https://android.googlesource.com/platform/system/media/+/master/audio_utils/include/audio_utils/roundup.h">include/audio_utils/roundup.h</a></li>
<li><a href="https://android.googlesource.com/platform/system/media/+/master/audio_utils/roundup.c">roundup.c</a></li>
</ul>
<h2 id="tools">工具</h2>
<p>
据我们所知,目前没有用于找出优先级倒置(尤其是在优先级倒置发生之前)的自动工具。一些研究型静态代码分析工具如果能够访问整个代码库,就能找出优先级倒置。当然,如果涉及到任意用户代码(这里指应用)或用户代码是一个大型代码库(如 Linux 内核和设备驱动程序),则进行静态分析可能会不切实际。最重要的是,请务必认真阅读代码,并充分理解整个系统和各种交互操作。<a href="http://developer.android.com/tools/help/systrace.html">systrace</a><code>ps -t -p</code> 等工具有助于在优先级倒置发生后及时发现,但并不会提前告知您。
</p>
<h2 id="aFinalWord">总结</h2>
<p>
经过上述讨论后,请不要害怕互斥。一般情况下,互斥会是您的得力助手,例如,在时间不紧急的一般使用情形中正确使用和实现互斥时。但在高优先级任务和低优先级任务之间以及在时间敏感型系统中,互斥更有可能会导致出现问题。
</p>
</body></html>