In diesem Artikel wird erläutert, wie das Audiosystem von Android versucht, Prioritätsinversionen 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 eine Audio-HAL implementieren. Die Implementierung dieser Techniken garantiert jedoch nicht, dass Fehler oder andere Probleme vermieden werden können, insbesondere wenn sie außerhalb des Audiokontexts verwendet werden. Die Ergebnisse können variieren. Sie sollten daher eigene Tests und Bewertungen durchführen.
Hintergrund
Der Android AudioFlinger-Audioserver und die AudioTrack/AudioRecord-Clientimplementierung werden neu entwickelt, um die Latenz zu reduzieren. Diese Arbeiten begannen in Android 4.1 und wurden 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 zeitkritischen Threads mit einer besser vorhersagbaren Planungsrichtlinie zuzuweisen. Durch die zuverlässige Planung können die Größen und Anzahl der Audio-Puffer reduziert werden, ohne dass es zu Unterläufen und Überläufen kommt.
Prioritätsinversion
Prioritätsinversion ist ein klassischer Fehlermodus von Echtzeitsystemen, bei dem eine Aufgabe mit höherer Priorität für unbegrenzte Zeit blockiert wird, weil sie darauf wartet, dass eine Aufgabe mit niedrigerer Priorität eine Ressource freigibt, z. B. einen Mutex (gemeinsamer Status, der durch einen Mutex geschützt ist). Mutex.
In einem Audiosystem äußert sich die Prioritätsinversion in der Regel als ein Fehler (Klicken, Knacken, Aussetzer), wiederholte Audioausgabe bei Verwendung von Ringpuffern oder Verzögerung bei der Reaktion auf einen Befehl.
Eine gängige Problemumgehung für die Prioritätsinversion besteht darin, die Größen der Audio-Puffer zu erhöhen. Diese Methode erhöht jedoch die Latenz und verdeckt das Problem nur, anstatt es zu lösen. Es ist besser, die Prioritätsinversion zu verstehen und zu verhindern, wie unten beschrieben.
In der Android-Audioimplementierung tritt die Prioritätsinversion am wahrscheinlichsten an diesen Stellen auf. Sie sollten sich daher auf Folgendes konzentrieren:
- 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 Anwendungs-Callback-Thread für einen schnellen AudioRecord und dem schnellen Aufnahmethread (ähnlich wie oben)
- in der Audio-Hardwareabstraktionsschicht-Implementierung (HAL), z.B. für Telefonie oder Echounterdrückung
- im Audiotreiber im Kernel
- zwischen dem AudioTrack- oder AudioRecord-Callback-Thread und anderen App-Threads (dies liegt außerhalb unserer Kontrolle)
Gängige Lösungen
Zu den typischen Lösungen gehören:
- Unterbrechungen deaktivieren
- Mutexe mit Prioritätsvererbung
Das Deaktivieren von Unterbrechungen ist im Linux-Benutzerbereich nicht möglich und funktioniert nicht für symmetrische Multiprozessoren (SMP).
Futexe mit Prioritätsvererbung (schnelle Mutexe im Benutzerbereich) werden im Audiosystem nicht verwendet, da sie relativ schwergewichtig sind und auf einem vertrauenswürdigen Client basieren.
Von Android verwendete Techniken
Die Tests begannen mit „try lock“ und „lock with timeout“. Dies sind nicht blockierende und begrenzt blockierende Varianten des Mutex-Sperrvorgangs. „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 gemeinsamen Status zugreifen, wenn der Client beschäftigt war, und das kumulative Timeout konnte zu lang sein, wenn es eine lange Sequenz nicht zusammenhängender Sperren gab, bei denen alle ein Timeout hatten.
Wir verwenden auch atomare Operationen wie:
- increment
- bitweises OR
- bitweises AND
Alle diese Operationen 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 Wiederholungen kein Problem darstellen.
Hinweis:Atomare Operationen und ihre Interaktionen mit Speicherbarrieren werden häufig falsch verstanden und falsch 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 kürzlich die folgenden Techniken hinzugefügt:
- Verwenden Sie nicht blockierende FIFO-Warteschlangen mit einem Leser und einem Schreiber für Daten.
- Versuchen Sie, den Status zu kopieren , anstatt ihn zwischen Modulen mit hoher und niedriger Priorität freizugeben.
- Wenn der Status freigegeben werden muss, beschränken Sie ihn auf das Wort mit der maximalen Größe , auf das atomar in einem Busvorgang ohne Wiederholungen zugegriffen werden kann.
- Verwenden Sie für komplexe mehrwortige Status eine Statuswarteschlange. Eine Statuswarteschlange ist im Grunde nur eine nicht blockierende FIFO-Warteschlange mit einem Leser und einem Schreiber, die für den Status anstelle von Daten verwendet wird. Der Schreiber fasst jedoch benachbarte Pushs zu einem einzigen Push zusammen.
- Achten Sie auf Speicherbarrieren für die SMP-Korrektheit.
- Vertrauen, aber prüfen. Wenn Sie den Status zwischen Prozessen freigeben, gehen Sie nicht davon aus, dass der Status wohlgeformt ist. Prüfen Sie beispielsweise, ob die Indizes innerhalb der Grenzen liegen. Diese Überprüfung ist nicht zwischen Threads im selben Prozess und zwischen Prozessen erforderlich, die sich gegenseitig vertrauen (die in der Regel dieselbe UID haben). Sie ist auch für freigegebene Daten wie PCM-Audio nicht erforderlich, bei denen eine Beschädigung keine Folgen hat.
Nicht blockierende Algorithmen
Nicht blockierende Algorithmen sind in letzter Zeit Gegenstand vieler Studien gewesen. Mit Ausnahme von FIFO-Warteschlangen mit einem Leser und einem Schreiber haben wir jedoch festgestellt, dass sie komplex und fehleranfällig sind.
Ab Android 4.2 finden Sie unsere nicht blockierenden Klassen mit einem Leser/Schreiber an folgenden Orten:
- frameworks/av/include/media/nbaio/
- frameworks/av/media/libnbaio/
- frameworks/av/services/audioflinger/StateQueue*
Diese wurden speziell für AudioFlinger entwickelt und sind nicht für allgemeine Zwecke geeignet. Nicht blockierende Algorithmen sind bekanntermaßen schwer zu debuggen. Sie können diesen Code als Modell verwenden. Es können jedoch Fehler auftreten und die Klassen sind möglicherweise nicht für andere Zwecke geeignet.
Für Entwickler sollte ein Teil des OpenSL ES-Beispielanwendungscodes aktualisiert werden, um nicht blockierende Algorithmen zu verwenden oder auf eine Open-Source-Bibliothek zu verweisen, die nicht von Android stammt.
Wir haben eine Beispielimplementierung einer nicht blockierenden FIFO-Warteschlange veröffentlicht, die speziell für Anwendungscode entwickelt wurde. Diese Dateien finden Sie im Quellverzeichnis der Plattform unter frameworks/av/audio_utils:
Tools
Unseres Wissens nach gibt es keine automatischen Tools, mit denen Prioritätsinversionen gefunden werden können, insbesondere bevor sie auftreten. Einige Tools zur statischen Codeanalyse können Prioritätsinversionen finden, wenn sie auf die gesamte Codebasis zugreifen können. Wenn jedoch beliebiger Nutzercode beteiligt ist (wie hier für die Anwendung) oder es sich um eine große Codebasis handelt (wie für den Linux-Kernel und Gerätetreiber), ist die statische Analyse möglicherweise nicht praktikabel. 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 Prioritätsinversionen nach ihrem Auftreten zu erkennen, informieren Sie aber
nicht im Voraus.
Abschließende Bemerkungen
Nach all dieser Diskussion sollten Sie keine Angst vor Mutexen haben. Mutexe sind bei normaler Verwendung nützlich, wenn sie in normalen, nicht zeitkritischen Anwendungsfällen korrekt verwendet und implementiert werden. Zwischen Aufgaben mit hoher und niedriger Priorität und in zeitkritischen Systemen verursachen Mutexe jedoch eher Probleme.