Vermeiden Sie eine Prioritätsumkehr

In diesem Artikel wird erklärt, wie das Audiosystem von Android versucht, eine Prioritätsumkehr zu vermeiden, und es werden Techniken hervorgehoben, die auch Sie verwenden können.

Diese Techniken können für Entwickler von Hochleistungs-Audio-Apps, OEMs und SoC-Anbietern nützlich sein, die einen Audio-HAL implementieren. Bitte beachten Sie, dass die Implementierung dieser Techniken keine Garantie dafür bietet, Störungen oder andere Fehler zu verhindern, insbesondere wenn sie außerhalb des Audiokontexts verwendet werden. Ihre Ergebnisse können variieren und Sie sollten Ihre eigene Bewertung und Tests durchführen.

Hintergrund

Der Android AudioFlinger-Audioserver und die AudioTrack/AudioRecord-Client-Implementierung werden neu gestaltet, um die Latenz zu reduzieren. Diese Arbeit begann in 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, zeitkritischen Threads CPU-Ressourcen mit einer vorhersehbareren Planungsrichtlinie zuzuweisen. Durch eine zuverlässige Planung können die Größe und Anzahl der Audiopuffer reduziert und gleichzeitig Unter- und Überläufe vermieden werden.

Prioritätsumkehr

Die Prioritätsumkehr ist ein klassischer Fehlermodus von Echtzeitsystemen, bei dem eine Aufgabe mit höherer Priorität für eine unbegrenzte Zeit blockiert wird und darauf wartet, dass eine Aufgabe mit niedrigerer Priorität eine Ressource freigibt, z. B. einen Mutex (durch einen gemeinsam genutzten Zustand geschützt).

In einem Audiosystem manifestiert sich die Prioritätsumkehr typischerweise als Störung (Klicken, Knacken, Aussetzer), wiederholtes Audio , wenn Ringpuffer verwendet werden, oder als Verzögerung bei der Reaktion auf einen Befehl.

Eine übliche Problemumgehung für die Prioritätsumkehr besteht darin, die Größe des Audiopuffers zu erhöhen. Allerdings erhöht diese Methode die Latenz und verschleiert das Problem lediglich, anstatt es zu lösen. Es ist besser, die Prioritätsumkehr zu verstehen und zu verhindern, wie unten gezeigt.

In der Android-Audio-Implementierung kommt es an diesen Stellen am wahrscheinlichsten zu einer Prioritätsumkehr. Und deshalb sollten Sie Ihre Aufmerksamkeit hier richten:

  • zwischen normalem Mixer-Thread und schnellem Mixer-Thread in AudioFlinger
  • zwischen dem Anwendungs-Callback-Thread für einen schnellen AudioTrack und einem schnellen Mixer-Thread (beide haben eine erhöhte Priorität, aber leicht unterschiedliche Prioritäten)
  • zwischen Anwendungs-Callback-Thread für eine schnelle Audioaufzeichnung und einem schnellen Capture-Thread (ähnlich wie zuvor)
  • innerhalb der Audio Hardware Abstraction Layer (HAL)-Implementierung, z. B. für Telefonie oder Echounterdrückung
  • innerhalb des Audiotreibers im Kernel
  • zwischen AudioTrack- oder AudioRecord-Callback-Thread und anderen App-Threads (dies liegt außerhalb unserer Kontrolle)

Gemeinsame Lösungen

Zu den typischen Lösungen gehören:

  • Interrupts deaktivieren
  • Mutexe mit Prioritätsvererbung

Das Deaktivieren von Interrupts ist im Linux-Benutzerbereich nicht möglich und funktioniert nicht für symmetrische Multiprozessoren (SMP).

Futexe mit Prioritätsvererbung (schnelle User-Space-Mutexe) werden im Audiosystem nicht verwendet, da sie relativ schwergewichtig sind und auf einem vertrauenswürdigen Client basieren.

Von Android verwendete Techniken

Die Experimente begannen mit „try lock“ und „lock with timeout“. Dies sind nicht blockierende und begrenzt blockierende Varianten der Mutex-Sperroperation. Der Versuch „Sperren“ und „Sperren mit Zeitüberschreitung“ funktionierte ziemlich gut, war jedoch anfällig für einige obskure Fehlermodi: Es wurde nicht garantiert, dass der Server auf den freigegebenen Status zugreifen konnte, wenn der Client gerade beschäftigt war, und die kumulative Zeitüberschreitung könnte zu lang sein, wenn Es gab eine lange Folge unabhängiger Sperren, bei denen alle eine Zeitüberschreitung erlitten.

Wir verwenden auch atomare Operationen wie:

  • Zuwachs
  • bitweises „oder“
  • bitweises „und“

Alle diese geben den vorherigen Wert zurück und beinhalten die notwendigen SMP-Barrieren. Der Nachteil besteht darin, dass sie unbegrenzte Wiederholungsversuche erfordern können. In der Praxis haben wir festgestellt, dass die Wiederholungsversuche kein Problem darstellen.

Hinweis: Atomare Operationen und ihre Wechselwirkungen mit Speicherbarrieren werden bekanntermaßen stark missverstanden und falsch verwendet. Wir führen diese Methoden hier der Vollständigkeit halber auf, empfehlen Ihnen jedoch, für weitere Informationen auch den Artikel „SMP Primer für Android“ zu lesen.

Wir verfügen immer noch über die meisten der oben genannten Tools und verwenden sie auch. Kürzlich haben wir diese Techniken hinzugefügt:

  • Verwenden Sie nicht blockierende Single-Reader-Single-Writer- FIFO-Warteschlangen für Daten.
  • Versuchen Sie, den Status zu kopieren , anstatt ihn zwischen Modulen mit hoher und niedriger Priorität zu teilen .
  • Wenn der Status gemeinsam genutzt werden muss, begrenzen Sie den Status auf das Wort mit der maximalen Größe, auf das atomar im Ein-Bus-Betrieb ohne Wiederholungsversuche zugegriffen werden kann.
  • Für komplexe Zustände mit mehreren Wörtern verwenden Sie eine Zustandswarteschlange. Eine Statuswarteschlange ist im Grunde nur eine nicht blockierende FIFO-Warteschlange mit einem Lesegerät und einem Schreiber, die eher für den Status als für Daten verwendet wird, mit der Ausnahme, dass der Schreiber benachbarte Pushs zu einem einzigen Push zusammenfasst.
  • Achten Sie auf Speicherbarrieren für die SMP-Korrektheit.
  • Vertrauen, aber überprüfen . Wenn Sie den Zustand zwischen Prozessen teilen, gehen Sie nicht davon aus, dass der Zustand wohlgeformt ist. Überprüfen Sie beispielsweise, ob die Indizes innerhalb der Grenzen liegen. Diese Überprüfung ist nicht zwischen Threads im selben Prozess oder zwischen gegenseitig vertrauenden Prozessen (die normalerweise dieselbe UID haben) erforderlich. Dies ist auch für gemeinsam genutzte Daten wie PCM-Audio nicht erforderlich, bei denen eine Beschädigung keine Rolle spielt.

Nicht blockierende Algorithmen

Nicht blockierende Algorithmen waren Gegenstand zahlreicher neuerer Studien. Aber mit Ausnahme der FIFO-Warteschlangen mit einem Leser und einem Schreiber haben wir festgestellt, dass sie komplex und fehleranfällig sind.

Ab Android 4.2 finden Sie unsere nicht blockierenden Single-Reader/Writer-Klassen an diesen 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 bestimmt. Nicht blockierende Algorithmen sind dafür bekannt, dass sie schwer zu debuggen sind. Sie können diesen Code als Modell betrachten. Beachten Sie jedoch, dass möglicherweise Fehler auftreten und die Eignung der Klassen für andere Zwecke nicht gewährleistet ist.

Für Entwickler sollte ein Teil des OpenSL ES-Beispielanwendungscodes aktualisiert werden, um nicht blockierende Algorithmen zu verwenden oder auf eine Nicht-Android-Open-Source-Bibliothek zu verweisen.

Wir haben ein Beispiel für eine nicht blockierende FIFO-Implementierung veröffentlicht, die speziell für Anwendungscode entwickelt wurde. Sehen Sie sich diese Dateien im Plattform-Quellverzeichnis frameworks/av/audio_utils an:

Werkzeuge

Nach unserem besten Wissen gibt es keine automatischen Tools, um die Prioritätsumkehr zu finden, insbesondere bevor sie geschieht. Einige Forschungstools für die statische Codeanalyse sind in der Lage, Prioritätsumkehrungen zu finden, wenn sie auf die gesamte Codebasis zugreifen können. Wenn natürlich beliebiger Benutzercode beteiligt ist (wie hier für die Anwendung) oder es sich um eine große Codebasis handelt (wie für den Linux-Kernel und die Gerätetreiber), kann eine statische Analyse unpraktisch sein. Das Wichtigste ist, den Code sehr sorgfältig zu lesen und einen guten Überblick über das gesamte System und die Interaktionen zu bekommen. Tools wie systrace und ps -t -p sind nützlich, um die Prioritätsumkehr nach dem Auftreten zu sehen, informieren Sie jedoch nicht im Voraus.

Ein letztes Wort

Haben Sie nach all dieser Diskussion keine Angst vor Mutexes. Mutexe sind Ihr Freund für den normalen Gebrauch, wenn sie in normalen, nicht zeitkritischen Anwendungsfällen korrekt verwendet und implementiert werden. Aber zwischen Aufgaben mit hoher und niedriger Priorität und in zeitkritischen Systemen verursachen Mutexe eher Probleme.