この記事では、優先順位の逆転を回避するために Android のオーディオ システムで講じられている対策、および、ご自身でも利用できる手法について説明します。
紹介する手法は、高性能オーディオ アプリのデベロッパー、OEM、SoC プロバイダがオーディオ HAL を実装する際に利用できます。これらの手法を採用しても、不具合やその他の障害を完全に防げるわけではありません(特にオーディオ コンテキスト外でアプリが使用された場合など)。結果はケースに応じて異なる可能性があります。また、評価、テストはご自身で行ってください。
背景
Android AudioFlinger オーディオ サーバーと AudioTrack / AudioRecord クライアントの実装は、レイテンシを短縮するために再設計されています。この取り組みは Android 4.1 から始まり、4.2、4.3、4.4、5.0 でさらに改善されました。
低レイテンシを実現するために、システム全体で多くの変更が必要でした。重要な変更の 1 つに、予測可能性がより高いスケジューリング ポリシーを使用して、スピードが重視されるスレッドに CPU リソースを割り当てるようになったことが挙げられます。これにより安定したスケジューリングが実現し、オーディオ バッファのサイズや数を削減しながらも、アンダーランやオーバーランを回避できるようになりました。
優先順位の逆転
優先順位の逆転は、リアルタイム システムで昔から存在する障害モードです。この障害では、優先順位の低いタスクがミューテックスのような(共有状態がロックされた)リソースを解放するまで、優先順位の高いタスクが無期限にブロックされてしまいます。
オーディオ システムでは、優先順位の逆転は通常、不具合(クリック、ポップ、ドロップアウト)、音声の繰り返し(循環バッファ使用時)、コマンドへの応答の遅延として現れます。
優先順位の逆転は一般的に、オーディオのバッファサイズを増やすことで回避できます。 しかし、この方法ではレイテンシが増加してしまいます。また、単に問題を隠しているだけであって、根本的な解決にはなりません。下記を参照して、優先順位の逆転について理解し、防止することをおすすめします。
Android オーディオの実装時、優先順位の逆転が最も発生しやすいのは以下の場所です。特に注意してください。
- 通常のミキサー スレッドと AudioFlinger の高速ミキサー スレッドの間
- 高速 AudioTrack 用アプリのコールバック スレッドと高速ミキサー スレッドの間(両方とも優先順位が高いものの、優先順位はわずかに異なります)
- 高速 AudioRecord 用アプリのコールバック スレッドと高速キャプチャ スレッドの間(上記と同様)
- オーディオ ハードウェア抽象化レイヤ(HAL)の実装内(例: 電話、エコー キャンセラ)
- カーネルのオーディオ ドライバ内
- AudioTrack または AudioRecord のコールバック スレッドと他のアプリスレッドの間(Google では対処不可能)
一般的な解決策
一般的な解決策は以下のとおりです。
- 割り込みを無効にする
- 優先順位継承ミューテックス
Linux ユーザー空間で割り込みを無効にすることはできません。また、割り込みの無効化は対称型マルチ プロセッサ(SMP)では機能しません。
優先順位継承フューテックス(高速ユーザー空間ミューテックス)はオーディオ システムでは使用されていません。比較的重く、またクライアントへの信頼に依存しているためです。
Android で使用される手法
Google ではまず、「ロック試行」と、タイムアウトによるロックのテストから始めました。これらは、ミューテックスのロック操作を改善する、非ブロックタイプと制限付きブロックタイプのロック操作です。ロック試行と、タイムアウトによるロックはかなりうまくいきましたが、いくつかの不明瞭な障害モードが発生しやすくなりました。クライアントがビジー状態になったときに、サーバーが共有状態にアクセスできることが保証されませんでした。また、無関係のロックの長期間のシーケンスがすべてタイムアウトすると、累積タイムアウトが長くなりすぎる場合がありました。
その他、以下のようなアトミック操作を使用しています。
- インクリメント
- ビット演算「OR」
- ビット演算「AND」
これらはすべて前の値を返します。また、必要となる SMP バリアが含まれます。デメリットは、無限回の再試行が必要になる可能性があることです。 実際には、再試行は問題にならないとわかりました。
注: アトミック操作、およびアトミック操作とメモリバリアとの相互利用については、大きく誤解され、誤って使用されています。ここでは、すべて説明するためにこれらの手法を挙げていますが、詳細について、Android 向け SMP Primer をご覧いただくことをおすすめします。
Google は上記の方法のほとんどを現在も使用していますが、以下の手法も最近追加されました。
- 非ブロックのシングル リーダー / ライター FIFO キューをデータに使用する。
- 優先順位の高いモジュールと低いモジュールの間で状態を「共有」するのではなく、「コピー」を試みる。
- 状態を共有する必要がある場合は、1 バス操作で再試行せずにアトミックにアクセスできる最大サイズのワードに状態を制限する。
- 複雑なマルチワード状態の場合は、状態キューを使用する。状態キューは基本的に、データではなく状態に使用される、非ブロックのシングル リーダー / ライター FIFO キューとする。ただし、ライターは隣接するプッシュをシングル プッシュに折りたたむ。
- メモリバリアに注意して、SMP の正確性を確認する。
- 信ぜよ、されど確認せよ。 状態をプロセス間で共有する場合に、状態の形式が適切であると過信しない。たとえば、インデックスが境界内にあるかどうかを確認します。同じプロセス内のスレッド間、互いに信頼済みのプロセス間(通常同じ UID を持つ)では、この確認は必要ありません。また、破損が問題とならない PCM オーディオのような共有データでも不要です。
非ブロック アルゴリズム
非ブロック アルゴリズムは、かなり最近の研究テーマです。しかし、シングル リーダー / ライター FIFO キューを除き、複雑でエラーが発生しやすいことがわかりました。
Android 4.2 以降では、非ブロックのシングル リーダー / ライタークラスは以下の場所にあります。
- frameworks/av/include/media/nbaio/
- frameworks/av/media/libnbaio/
- frameworks/av/services/audioflinger/StateQueue*
これらは AudioFlinger 用に設計されたもので、汎用向けではありません。非ブロック アルゴリズムは、デバッグが難しいことで有名です。このコードはモデルとして参照できます。バグがある可能性があるので注意してください。また、このクラスが他の目的に適しているとは限りません。
非ブロック アルゴリズムを使用したり、Android 以外のオープンソース ライブラリを参照したりする場合、デベロッパーは OpenSL ES アプリのサンプルコードの一部を更新する必要があります。
アプリコード用に設計された非ブロック FIFO の実装例を公開しています。プラットフォームのソース ディレクトリ frameworks/av/audio_utils
にある以下のファイルをご覧ください。
ツール
Google で認識している限りにおいて、優先順位の逆転を自動で、特に事前に検出できるツールは存在しません。一部の研究用静的コード解析ツールは、コードベース全体にアクセスできる場合に限り、優先順位の逆転を検出できます。もちろん、任意のユーザーコードが関与している場合(ここで問題としているアプリの場合)や、コードベースが大規模である場合(Linux カーネルやデバイス ドライバ)、静的解析は実用的ではありません。最も重要なことは、コードを注意深く読み、システム全体と相互作用をよく把握することです。Systrace や ps -t -p
などのツールを使用すると、優先順位の逆転を発生後には確認できますが、事前に検出することはできません。
おわりに
以上のように検討してきましたが、ミューテックスを恐れる必要はありません。ミューテックスは、スピードが重視されないユースケースで正しく使用、実装されているのであれば、非常に便利なものです。ただし、優先順位の高いタスクと低いタスクの間や、スピードが重視されるシステムでは、ミューテックスが原因で問題が発生する可能性が高くなります。