In diesem Artikel wird erläutert, wie das Audiosystem von Android versucht, eine Prioritätsumkehrung zu vermeiden. Außerdem werden Techniken beschrieben, 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 eine Audio-HAL implementieren. Die Implementierung dieser Techniken kann jedoch nicht garantieren, dass es nicht zu Störungen oder anderen Fehlern kommt, insbesondere wenn sie außerhalb des Audiokontexts verwendet werden. Ihre Ergebnisse können variieren. Sie sollten Ihre eigenen Bewertungen und Tests durchführen.
Hintergrund
Der Audioserver AudioFlinger und die Clientimplementierung von AudioTrack/AudioRecord werden neu gestaltet, um die Latenz zu reduzieren. Diese Arbeit begann mit Android 4.1 und wurde mit weiteren Verbesserungen in den Versionen 4.2, 4.3, 4.4 und 5.0 fortgesetzt.
Um diese niedrigere Latenz zu erreichen, waren viele Änderungen am gesamten System erforderlich. Eine wichtige Änderung besteht darin, zeitkritischen Threads CPU-Ressourcen mit einer besser vorhersehbaren Planungsrichtlinie zuzuweisen. Durch eine zuverlässige Planung können die Größe und Anzahl der Audio-Puffer reduziert werden, ohne Unter- und Überlauf zu verursachen.
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, während sie darauf wartet, dass eine Aufgabe mit niedrigerer Priorität eine Ressource freigibt, z. B. einen (durch einen Mutex geschützten) freigegebenen Zustand.
In einem Audiosystem äußert sich die Prioritätsumkehr in der Regel als Glitch (Klick, Pop, Aussetzer), wiederholte Audioinhalte, wenn zyklische Puffer verwendet werden, oder als Verzögerung bei der Reaktion auf einen Befehl.
Eine gängige Problemumgehung für die Prioritätsumkehr besteht darin, die Audiopuffergrößen zu erhöhen. Diese Methode erhöht jedoch die Latenz und verschleiert das Problem nur, anstatt es zu lösen. Es ist besser, die Prioritätsumkehrung zu verstehen und zu verhindern, wie unten dargestellt.
Bei der Android-Audioimplementierung tritt die Prioritätsumkehr am ehesten an diesen Stellen auf. Konzentrieren Sie sich daher auf Folgendes:
- zwischen dem normalen Mixer-Thread und dem schnellen 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 Rückruf-Thread der Anwendung für eine schnelle Audioaufnahme und dem Thread für die schnelle Aufnahme (ähnlich wie zuvor)
- in der Audio-Hardware-Abstraktionsschicht (HAL), z.B. für Telefonie oder Echounterdrückung
- im Audiotreiber im Kernel
- zwischen dem Rückruf-Thread von AudioTrack oder AudioRecord und anderen App-Threads (dies liegt nicht in unserer Kontrolle)
Gängige Lösungen
Zu den typischen Lösungen gehören:
- Unterbrechungen deaktivieren
- Mutexes mit Prioritätsübernahme
Das Deaktivieren von Unterbrechungen ist im Linux-Nutzerbereich nicht möglich und funktioniert nicht für symmetrische Multiprozessoren (SMP).
Prioritätsübertragungs-futexes (schnelle Mutexes im Userspace) werden im Audiosystem nicht verwendet, da sie relativ umfangreich sind und auf einen vertrauenswürdigen Client angewiesen sind.
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-Sperre. „Try lock“ und „lock with timeout“ funktionierten ziemlich gut, waren aber anfällig für einige unklare Fehlermodi: Der Server konnte nicht garantiert auf den freigegebenen Status zugreifen, wenn der Client gerade beschäftigt war, und die kumulative Zeitüberschreitung könnte zu lang sein, wenn es eine lange Sequenz nicht zusammenhängender Sperren gab, die alle eine Zeitüberschreitung hatten.
Außerdem verwenden wir atomare Vorgänge, z. B.:
- Erhöhen
- Bitweises „oder“
- Bitweises „AND“
Alle diese Funktionen geben den vorherigen Wert zurück und enthalten die erforderlichen SMP-Grenzwerte. Der Nachteil ist, dass sie unbegrenzte Wiederholungen erfordern können. In der Praxis haben wir festgestellt, dass die Wiederholungen kein Problem darstellen.
Hinweis:Atomare Vorgänge und ihre Interaktionen mit Speicherbarrieren werden häufig falsch verstanden und verwendet. Wir führen diese Methoden hier zur Vollständigkeit an, empfehlen aber, den Artikel SMP Primer for Android (Einführung in SMP für Android) zu lesen.
Die meisten der oben genannten Tools sind noch vorhanden und werden verwendet. Kürzlich haben wir diese Techniken hinzugefügt:
- Verwenden Sie für Daten nicht blockierende FIFO-Queues mit einem einzelnen Leser und einem einzelnen Schreiber.
- Versuchen Sie, den Status zwischen Modulen mit hoher und niedriger Priorität zu kopieren, anstatt ihn zu teilen.
- Wenn der Status geteilt werden muss, beschränken Sie ihn auf das Wort mit der maximalen Größe, auf das im Ein-Bus-Betrieb ohne Wiederholungen atomar zugegriffen werden kann.
- Verwenden Sie für komplexe Status mit mehreren Wörtern eine Statuswarteschlange. Eine Statuswarteschlange ist im Grunde nur eine nicht blockierende FIFO-Warteschlange mit einem Leser und einem Schreiber, die nicht für Daten, sondern für den Status verwendet wird. Der Schreiber reduziert jedoch benachbarte Pushes zu einem einzigen Push.
- Achten Sie auf Speicherbarrieren für die Korrektheit der SMP.
- Vertrauen, aber prüfen Wenn Sie den Status zwischen Prozessen teilen, gehen Sie nicht davon aus, dass der Status korrekt ist. Prüfen Sie beispielsweise, ob die Indizes innerhalb der zulässigen Grenzen liegen. Diese Überprüfung ist nicht erforderlich zwischen Threads im selben Prozess oder zwischen einander vertrauenden Prozessen (die in der Regel dieselbe UID haben). Sie ist auch für freigegebene Daten wie PCM-Audio unnötig, bei denen eine Beschädigung keine Auswirkungen hat.
Nicht blockierende Algorithmen
Nicht blockierende Algorithmen waren in letzter Zeit Gegenstand vieler Studien. Mit Ausnahme von FIFO-Warteschlangen mit nur einem Leser und einem Schreiber sind sie jedoch komplex und fehleranfällig.
Ab Android 4.2 finden Sie unsere nicht blockierenden Klassen mit einem einzelnen Leser/Schreiber an folgenden Stellen:
- frameworks/av/include/media/nbaio/
- frameworks/av/media/libnbaio/
- frameworks/av/services/audioflinger/StateQueue*
Sie wurden speziell für AudioFlinger entwickelt und sind nicht für allgemeine Zwecke geeignet. Nicht blockierende Algorithmen sind notorisch schwer zu debuggen. Sie können sich diesen Code als Modell ansehen. Es kann jedoch sein, dass es Fehler gibt und die Klassen nicht für andere Zwecke geeignet sind.
Entwickler sollten einige Teile des OpenSL ES-Anwendungscodes aktualisieren, um nicht blockierende Algorithmen zu verwenden oder auf eine Open-Source-Bibliothek zu verweisen, die nicht zu Android gehört.
Wir haben ein Beispiel für eine nicht blockierende FIFO-Implementierung veröffentlicht, die speziell für Anwendungscode entwickelt wurde. Sehen Sie sich die folgenden Dateien im Plattform-Quellverzeichnis frameworks/av/audio_utils
an:
Tools
Unseres Wissens gibt es keine automatischen Tools, mit denen sich eine Prioritätsumkehr erkennen lässt, insbesondere nicht, bevor sie eintritt. Einige Tools zur Analyse von statischem Code können Prioritätsumkehrungen finden, wenn sie auf die gesamte Codebasis zugreifen können. Wenn es sich um beliebigen Nutzercode handelt (wie hier bei der Anwendung) oder um eine große Codebasis (wie beim Linux-Kernel und bei Gerätetreibern), ist die statische Analyse möglicherweise nicht praktikabel. Das Wichtigste ist, den Code sehr sorgfältig zu lesen und sich ein gutes Bild vom gesamten System und den Interaktionen zu machen. Tools wie systrace und ps -t -p
sind nützlich, um eine Prioritätsumkehr nach dem Auftreten zu sehen, aber sie geben keine Vorwarnung.
Noch ein Wort zum Schluss
Nach all diesen Diskussionen sollten Sie keine Angst mehr vor Mutexen haben. Mutexes sind für die normale Verwendung geeignet, wenn sie richtig verwendet und in normalen, nicht zeitkritischen Anwendungsfällen implementiert werden. Zwischen Aufgaben mit hoher und niedriger Priorität und in zeitkritischen Systemen können Mutexe jedoch eher zu Problemen führen.