Vermeidung von Prioritätsumkehr

Dieser Artikel erklärt, wie das Audiosystem von Android versucht, eine Prioritätsumkehr zu vermeiden, und hebt Techniken hervor, die Sie ebenfalls verwenden können.

Diese Techniken können für Entwickler von Hochleistungs-Audio-Apps, OEMs und SoC-Anbieter nützlich sein, die eine Audio-HAL implementieren. Bitte beachten Sie, dass die Implementierung dieser Techniken Störungen oder andere Fehler nicht garantiert verhindert, 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 niedrigere 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. Zuverlässiges Scheduling ermöglicht es, die Audiopuffergrößen und -zählwerte zu reduzieren, während gleichzeitig Unterschreitungen und Überschreitungen vermieden werden.

Prioritätsumkehr

Prioritätsumkehr 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 , wie z.

In einem Audiosystem manifestiert sich die Prioritätsumkehr typischerweise als ein Glitch (Klick, Pop, Dropout), wiederholtes Audio , wenn Ringpuffer verwendet werden, oder eine Verzögerung bei der Reaktion auf einen Befehl.

Eine gängige Problemumgehung für die Prioritätsumkehr besteht darin, die Audiopuffergröße zu erhöhen. Diese Methode erhöht jedoch die Latenz und verbirgt 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-Audioimplementierung tritt an diesen Stellen höchstwahrscheinlich eine Prioritätsumkehr auf. Und so sollten Sie Ihre Aufmerksamkeit hier konzentrieren:

  • zwischen normalem Mixer-Thread und schnellem Mixer-Thread in AudioFlinger
  • zwischen Anwendungs-Callback-Thread für einen schnellen AudioTrack und schnellen Mixer-Thread (beide haben eine erhöhte Priorität, aber leicht unterschiedliche Prioritäten)
  • zwischen Anwendungs-Callback-Thread für einen schnellen AudioRecord und schnellen Capture-Thread (ähnlich dem vorherigen)
  • innerhalb der HAL-Implementierung (Hardware Abstraction Layer) von Audio, 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:

  • Unterbrechungen deaktivieren
  • Prioritätsvererbungs-Mutexe

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

Prioritätsvererbungs- Futexe (schnelle User-Space-Mutexe) werden im Audiosystem nicht verwendet, da sie relativ schwergewichtig sind und sich auf einen vertrauenswürdigen Client verlassen.

Von Android verwendete Techniken

Experimente begannen mit "try lock" und lock with timeout. Dies sind nicht blockierende und begrenzt blockierende Varianten der Mutex-Lock-Operation. Try lock and lock with timeout funktionierte ziemlich gut, war aber anfällig für ein paar obskure Fehlermodi: Es war nicht garantiert, dass der Server auf den freigegebenen Zustand zugreifen konnte, wenn der Client beschäftigt war, und das kumulative Timeout könnte zu lang sein, wenn Es gab eine lange Folge nicht zusammenhängender Sperren, die alle abgelaufen waren.

Wir verwenden auch atomare Operationen wie:

  • Zuwachs
  • bitweises "oder"
  • bitweises "und"

All diese 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 Wechselwirkungen mit Speicherbarrieren werden notorisch stark missverstanden und falsch verwendet. Wir nehmen diese Methoden hier der Vollständigkeit halber auf, empfehlen Ihnen aber auch, den Artikel SMP-Grundlagen für Android für weitere Informationen zu lesen.

Wir haben und verwenden immer noch die meisten der oben genannten Tools und haben kürzlich diese Techniken hinzugefügt:

  • Verwenden Sie nicht blockierende Single-Reader-Single-Writer- FIFO-Warteschlangen für Daten.
  • Versuchen Sie, den Status zu kopieren , anstatt den Status zwischen Modulen mit hoher und niedriger Priorität zu teilen .
  • Wenn der Zustand geteilt werden muss, begrenzen Sie den Zustand auf das Wort mit der maximalen Größe, auf das im Ein-Bus-Betrieb ohne Wiederholungen atomar zugegriffen werden kann.
  • Verwenden Sie für einen komplexen Mehrwortzustand eine Zustandswarteschlange. Eine Zustandswarteschlange ist im Grunde nur eine nicht blockierende FIFO-Warteschlange mit einem Lesegerät und einem Schreiber, die eher für den Zustand als für Daten verwendet wird, außer dass der Schreiber benachbarte Pushs zu einem einzigen Push zusammenfasst.
  • Achten Sie auf Speicherbarrieren für die SMP-Korrektheit.
  • Vertraue, aber überprüfe . Wenn Sie den Zustand zwischen Prozessen teilen, gehen Sie nicht davon aus, dass der Zustand wohlgeformt ist. Überprüfen Sie beispielsweise, ob Indizes innerhalb der Grenzen liegen. Diese Überprüfung ist nicht erforderlich zwischen Threads im selben Prozess, zwischen gegenseitig vertrauenden Prozessen (die normalerweise dieselbe UID haben). Es ist auch unnötig für gemeinsam genutzte Daten wie PCM-Audio, wo eine Beschädigung belanglos ist.

Nicht blockierende Algorithmen

Nicht-blockierende Algorithmen sind Gegenstand zahlreicher neuerer Studien gewesen. Aber mit Ausnahme von Single-Reader-Single-Writer-FIFO-Warteschlangen 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 universell einsetzbar. Nicht blockierende Algorithmen sind dafür bekannt, dass sie schwer zu debuggen sind. Sie können sich diesen Code als Muster ansehen. Beachten Sie jedoch, dass Fehler auftreten können und dass die Klassen nicht garantiert für andere Zwecke geeignet sind.

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 eine beispielhafte nicht blockierende FIFO-Implementierung veröffentlicht, die speziell für Anwendungscode entwickelt wurde. Siehe diese Dateien im Quellverzeichnis der Plattform frameworks/av/audio_utils :

Werkzeug

Nach unserem besten Wissen gibt es keine automatischen Tools zum Auffinden der Prioritätsumkehr, insbesondere bevor sie auftritt. Einige statische Codeanalysetools für die Forschung sind in der Lage, Prioritätsumkehrungen zu finden, wenn sie auf die gesamte Codebasis zugreifen können. Wenn es sich natürlich um beliebigen Benutzercode handelt (wie hier für die Anwendung) oder um eine große Codebasis (wie beim Linux-Kernel und den Gerätetreibern), kann eine statische Analyse unpraktisch sein. Das Wichtigste ist, den Code sehr genau zu lesen und das gesamte System und die Interaktionen gut zu verstehen. Tools wie systrace und ps -t -p sind nützlich, um die Prioritätsumkehr zu sehen, nachdem sie aufgetreten ist, sagen es Ihnen aber 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.