In diesem Artikel wird erläutert, wie das Audiosystem von Android versucht, Prioritätsinversion zu vermeiden. Außerdem werden Techniken vorgestellt, die Sie ebenfalls verwenden können.
Diese Techniken können für Entwickler von leistungsstarken Audio-Apps, OEMs und SoC-Anbieter nützlich sein, die ein Audio-HAL implementieren. Die Implementierung dieser Techniken kann jedoch nicht garantieren, dass Störungen oder andere Fehler verhindert werden, insbesondere wenn sie außerhalb des Audiokontexts verwendet werden. Ihre Ergebnisse können variieren. Sie sollten eigene Auswertungen und Tests durchführen.
Hintergrund
Der AudioFlinger-Audioserver und die AudioTrack-/AudioRecord-Clientimplementierung für Android werden neu konzipiert, um die Latenz zu verringern. Diese Arbeit begann mit Android 4.1 und wurde mit weiteren Verbesserungen in 4.2, 4.3, 4.4 und 5.0 fortgesetzt.
Um diese geringere Latenz zu erreichen, waren viele Änderungen im gesamten System erforderlich. Eine wichtige Änderung besteht darin, CPU-Ressourcen für zeitkritische Threads mit einer besser vorhersagbaren Scheduling-Richtlinie zuzuweisen. Durch zuverlässige Planung können die Größe und Anzahl der Audio-Puffer reduziert werden, ohne dass es zu Unter- und Überläufen kommt.
Prioritätsumkehr
Die Prioritätsinversion ist ein klassischer Fehlermodus von Echtzeitsystemen, bei dem eine Aufgabe mit höherer Priorität für eine unbegrenzte Zeit blockiert wird, da sie darauf wartet, dass eine Aufgabe mit niedrigerer Priorität eine Ressource wie einen (durch einen) Mutex geschützten gemeinsamen Status freigibt.
In einem Audiosystem äußert sich eine Prioritätsinversion in der Regel als Störgeräusch (Klicken, Knacken, Aussetzer), wiederholte Audioausgabe bei Verwendung von Ringpuffern oder als Verzögerung bei der Reaktion auf einen Befehl.
Eine gängige Problemumgehung für die Prioritätsinversion besteht darin, die Größe der Audio-Puffer zu erhöhen. Diese Methode erhöht jedoch die Latenz und verbirgt das Problem nur, anstatt es zu lösen. Es ist besser, die Prioritätsumkehr zu verstehen und zu verhindern, wie unten beschrieben.
In der Android-Audioimplementierung tritt die Prioritätsumkehr am wahrscheinlichsten an diesen Stellen auf. Daher sollten Sie sich auf Folgendes konzentrieren:
- zwischen normalem Mixer-Thread und schnellem Mixer-Thread in AudioFlinger
- zwischen dem Anwendungs-Callback-Thread für einen schnellen AudioTrack und dem schnellen Mixer-Thread (beide haben eine erhöhte Priorität, aber leicht unterschiedliche Prioritäten)
- zwischen dem Anwendungs-Callback-Thread für einen schnellen AudioRecord und dem schnellen Aufnahmethread (ähnlich wie zuvor)
- in der Audio-HAL-Implementierung (Hardware Abstraction Layer), z.B. für Telefonie oder Echounterdrückung
- im Audio-Treiber im Kernel
- zwischen dem AudioTrack- oder AudioRecord-Callback-Thread und anderen App-Threads (darauf haben wir keinen Einfluss)
Gängige Lösungen
Zu den typischen Lösungen gehören:
- Unterbrechungen deaktivieren
- Mutexes mit Prioritätsvererbung
Das Deaktivieren von Interrupts ist im Linux-Nutzerbereich nicht möglich und funktioniert nicht für symmetrische Multiprozessoren (SMP).
Die Prioritätsvererbung von Futexes (Fast User-Space Mutexes) wird im Audiosystem nicht verwendet, da sie relativ schwergewichtig sind und auf einem vertrauenswürdigen Client basieren.
Von Android verwendete Techniken
Tests, die mit „try lock“ und „lock with timeout“ gestartet wurden. Dies sind nicht blockierende und begrenzt blockierende Varianten der Mutex-Sperroperation. „Try lock“ und „lock with timeout“ funktionierten recht gut, waren aber anfällig für einige obskure Fehlermodi: Der Server konnte nicht garantiert auf den freigegebenen Status zugreifen, wenn der Client beschäftigt war, und das kumulative Zeitlimit konnte zu lang sein, wenn es eine lange Sequenz nicht zusammenhängender Sperren gab, die alle ein Zeitlimit überschritten.
Wir verwenden auch atomare Vorgänge wie:
- Erhöhen
- Bitweises „OR“
- Bitweises AND
Alle geben den vorherigen Wert zurück und enthalten die erforderlichen SMP-Barrieren. Der Nachteil ist, dass sie unbegrenzte Wiederholungen erfordern können. In der Praxis haben wir festgestellt, dass die Wiederholungsversuche kein Problem darstellen.
Hinweis:Atomare Operationen und ihre Interaktionen mit Memory Barriers werden häufig falsch verstanden und verwendet. Wir haben diese Methoden hier der Vollständigkeit halber aufgeführt, empfehlen Ihnen aber, auch den Artikel SMP Primer for Android zu lesen, um weitere Informationen zu erhalten.
Wir verwenden die meisten der oben genannten Tools weiterhin und haben vor Kurzem die folgenden Techniken hinzugefügt:
- Verwenden Sie nicht blockierende FIFO-Warteschlangen mit einem einzelnen Leser und einem einzelnen Schreiber für Daten.
- Kopieren Sie den Status anstatt ihn zwischen Modulen mit hoher und niedriger Priorität zu teilen.
- Wenn der Status freigegeben werden muss, beschränken Sie ihn auf das Wort mit der maximalen Größe, auf das in einem Busvorgang ohne Wiederholungen atomar zugegriffen werden kann.
- Verwenden Sie für komplexe mehrteilige Status eine Statuswarteschlange. Eine Statuswarteschlange ist im Grunde nur eine nicht blockierende FIFO-Warteschlange mit einem Leser und einem Writer, die für den Status anstelle von Daten verwendet wird. Der Writer fasst jedoch benachbarte Push-Vorgänge in einem einzigen Push-Vorgang zusammen.
- Achten Sie auf Speichersperren, um die SMP-Korrektheit zu gewährleisten.
- Vertrauen, aber prüfen: Wenn Sie Status zwischen Prozessen freigeben, gehen Sie nicht davon aus, dass der Status wohlgeformt ist. Prüfen Sie beispielsweise, ob die Indexe innerhalb der Grenzen liegen. Diese Bestätigung ist nicht zwischen Threads im selben Prozess und nicht zwischen Prozessen erforderlich, die sich gegenseitig vertrauen (die in der Regel dieselbe UID haben). Es ist auch nicht für gemeinsam genutzte Daten wie PCM-Audio erforderlich, bei denen eine Beschädigung keine Auswirkungen hat.
Nicht blockierende Algorithmen
Nicht blockierende Algorithmen sind in letzter Zeit Gegenstand vieler Studien gewesen. Mit Ausnahme von FIFO-Warteschlangen mit einem einzelnen Leser und einem einzelnen Writer haben wir festgestellt, dass sie komplex und fehleranfällig sind.
Ab Android 4.2 finden Sie unsere nicht blockierenden Single-Reader-/Writer-Klassen an folgenden Speicherorten:
- frameworks/av/include/media/nbaio/
- frameworks/av/media/libnbaio/
- frameworks/av/services/audioflinger/StateQueue*
Diese wurden speziell für AudioFlinger entwickelt und sind nicht universell einsetzbar. Nicht blockierende Algorithmen sind dafür bekannt, dass sie schwer zu debuggen sind. Sie können sich diesen Code als Vorlage ansehen. Es kann jedoch Fehler geben und die Klassen sind möglicherweise nicht für andere Zwecke geeignet.
Für Entwickler sollte ein Teil des OpenSL ES-Beispielanwendungscodes aktualisiert werden, damit nicht blockierende Algorithmen verwendet oder auf eine Open-Source-Bibliothek verwiesen wird, die nicht von Android stammt.
Wir haben eine Beispielimplementierung für einen nicht blockierenden FIFO veröffentlicht, die speziell für Anwendungscode entwickelt wurde. Die Dateien befinden sich im Quellverzeichnis der Plattform frameworks/av/audio_utils
:
Tools
Unseres Wissens nach gibt es keine automatischen Tools, um Prioritätsinversionen zu finden, insbesondere bevor sie auftreten. Einige Tools zur Analyse von statischem Code können Prioritätsinversionen erkennen, wenn sie auf die gesamte Codebasis zugreifen können. Wenn beliebiger Nutzercode beteiligt ist (wie hier bei der Anwendung) oder es sich um eine große Codebasis handelt (wie beim Linux-Kernel und bei Gerätetreibern), kann die statische Analyse natürlich unpraktisch sein. Am wichtigsten ist es, den Code sehr sorgfältig zu lesen und das gesamte System und die Interaktionen gut zu verstehen. Tools wie systrace und ps -t -p
sind nützlich, um eine Prioritätsinversion nach ihrem Auftreten zu erkennen, aber sie geben keine Vorabinformationen.
Ein letztes Wort
Nach all diesen Ausführungen sollten Sie keine Angst vor Mutexes haben. Mutexes sind für den normalen Gebrauch sehr nützlich, wenn sie in normalen, nicht zeitkritischen Anwendungsfällen richtig verwendet und implementiert werden. Bei Aufgaben mit hoher und niedriger Priorität und in zeitkritischen Systemen verursachen Mutexe jedoch eher Probleme.