Éviter l'inversion de priorité

Cet article explique comment le système audio d'Android tente d'éviter l'inversion de priorité et présente les techniques que vous pouvez également utiliser.

Ces techniques peuvent être utiles aux développeurs d'applications audio hautes performances, aux OEM et aux fournisseurs de SoC qui implémentent une HAL audio. Veuillez noter que l'implémentation de ces techniques ne garantit pas la prévention des problèmes ou autres échecs, en particulier si elles sont utilisées en dehors du contexte audio. Vos résultats peuvent varier. Vous devez effectuer votre propre évaluation et vos propres tests.

Arrière-plan

L'architecture du serveur audio AudioFlinger Android et de l'implémentation du client AudioTrack/AudioRecord est en cours de refonte pour réduire la latence. Ce travail a commencé dans Android 4.1 et s'est poursuivi avec d'autres améliorations dans les versions 4.2, 4.3, 4.4 et 5.0.

Pour atteindre cette latence plus faible, de nombreuses modifications ont été nécessaires dans l'ensemble du système. Un changement important consiste à attribuer des ressources de processeur aux threads critiques en termes de temps avec une stratégie de planification plus prévisible. La planification fiable permet de réduire la taille et le nombre de tampons audio tout en évitant les sous-utilisations et les surutilisations.

Inversion de priorité

L'inversion de priorité est un mode de défaillance classique des systèmes en temps réel, dans lequel une tâche de priorité plus élevée est bloquée pendant une durée illimitée en attendant qu'une tâche de priorité inférieure libère une ressource telle qu'un mutex (état partagé protégé par un mutex).

Dans un système audio, l'inversion de priorité se manifeste généralement par un problème (clic, bruit sec, interruption), une répétition audio lorsque des tampons circulaires sont utilisés ou un retard dans la réponse à une commande.

Une solution de contournement courante pour l'inversion de priorité consiste à augmenter la taille des mémoires tampons audio. Toutefois, cette méthode augmente la latence et ne fait que masquer le problème au lieu de le résoudre. Il est préférable de comprendre et d'éviter l'inversion de priorité, comme indiqué ci-dessous.

Dans l'implémentation audio Android, l'inversion de priorité est plus susceptible de se produire dans les cas suivants. Vous devez donc vous concentrer sur les éléments suivants :

  • entre le thread de mixeur normal et le thread de mixeur rapide dans AudioFlinger
  • entre le thread de rappel d'application pour un AudioTrack rapide et le thread de mixeur rapide (ils ont tous deux une priorité élevée, mais légèrement différente).
  • entre le thread de rappel de l'application pour un AudioRecord rapide et le thread de capture rapide (similaire au précédent)
  • dans l'implémentation de la couche d'abstraction matérielle (HAL) audio, par exemple pour la téléphonie ou l'annulation d'écho
  • dans le pilote audio du noyau
  • entre le thread de rappel AudioTrack ou AudioRecord et les autres threads d'application (cela ne dépend pas de nous)

Solutions courantes

Voici quelques solutions typiques :

  • désactiver les interruptions ;
  • Mutex d'héritage de priorité

Il n'est pas possible de désactiver les interruptions dans l'espace utilisateur Linux, et cela ne fonctionne pas pour les multiprocesseurs symétriques (SMP).

L'héritage de priorité des futexes (mutex rapides de l'espace utilisateur) n'est pas utilisé dans le système audio, car ils sont relativement lourds et reposent sur un client de confiance.

Techniques utilisées par Android

Tests commencés avec "try lock" et verrouillage avec délai d'expiration. Il s'agit de variantes non bloquantes et bloquantes limitées de l'opération de verrouillage mutex. La méthode "lock and lock with timeout" (verrouiller et verrouiller avec délai d'expiration) fonctionnait plutôt bien, mais était sujette à quelques modes de défaillance obscurs : le serveur n'était pas garanti de pouvoir accéder à l'état partagé si le client était occupé, et le délai d'expiration cumulé pouvait être trop long en cas de longue séquence de verrous non liés qui ont tous expiré.

Nous utilisons également des opérations atomiques telles que :

  • augmenter
  • OR au niveau du bit
  • "and" au niveau du bit

Toutes ces fonctions renvoient la valeur précédente et incluent les barrières SMP nécessaires. L'inconvénient est qu'elles peuvent nécessiter un nombre illimité de nouvelles tentatives. En pratique, nous avons constaté que les tentatives ne posent pas de problème.

Remarque : Les opérations atomiques et leurs interactions avec les barrières de mémoire sont notoirement mal comprises et utilisées de manière incorrecte. Nous incluons ces méthodes ici pour être exhaustifs, mais nous vous recommandons également de lire l'article Présentation de SMP pour Android pour en savoir plus.

Nous disposons toujours de la plupart des outils ci-dessus et les utilisons. Nous avons récemment ajouté les techniques suivantes :

  • Utilisez des files d'attente FIFO non bloquantes à lecteur unique et à écriture unique pour les données.
  • Essayez de copier l'état plutôt que de le partager entre les modules à priorité élevée et ceux à priorité faible.
  • Lorsque l'état doit être partagé, limitez-le au mot de taille maximale accessible de manière atomique en fonctionnement sur un bus sans nouvelle tentative.
  • Pour les états complexes à plusieurs mots, utilisez une file d'état. Une file d'état est simplement une file FIFO non bloquante à lecteur et écriveur uniques, utilisée pour l'état plutôt que pour les données, à l'exception de l'écriveur qui regroupe les insertions adjacentes en une seule insertion.
  • Faites attention aux barrières de mémoire pour la correction SMP.
  • La confiance n'exclut pas le contrôle. Lorsque vous partagez un état entre des processus, ne partez pas du principe que l'état est bien formé. Par exemple, vérifiez que les index sont dans les limites. Cette validation n'est pas nécessaire entre les threads d'un même processus, ni entre les processus de confiance mutuelle (qui ont généralement le même UID). Il est également inutile pour les données partagées, telles que l'audio PCM, où une corruption est sans conséquence.

Algorithmes non bloquants

Les algorithmes non bloquants ont fait l'objet de nombreuses études récentes. Toutefois, à l'exception des files d'attente FIFO à lecteur unique et à écriture unique, nous les avons trouvées complexes et sujettes aux erreurs.

À partir d'Android 4.2, vous trouverez nos classes de lecture/écriture uniques et non bloquantes aux emplacements suivants :

  • frameworks/av/include/media/nbaio/
  • frameworks/av/media/libnbaio/
  • frameworks/av/services/audioflinger/StateQueue*

Elles ont été conçues spécifiquement pour AudioFlinger et ne sont pas à usage général. Les algorithmes non bloquants sont connus pour être difficiles à déboguer. Vous pouvez considérer ce code comme un modèle. Toutefois, sachez qu'il peut y avoir des bugs et que les classes ne sont pas garanties d'être adaptées à d'autres fins.

Pour les développeurs, certains exemples de code d'application OpenSL ES doivent être mis à jour pour utiliser des algorithmes non bloquants ou faire référence à une bibliothèque Open Source non Android.

Nous avons publié un exemple d'implémentation FIFO non bloquante spécialement conçue pour le code d'application. Consultez ces fichiers situés dans le répertoire source de la plate-forme frameworks/av/audio_utils :

Outils

À notre connaissance, il n'existe aucun outil automatique permettant de détecter l'inversion de priorité, en particulier avant qu'elle ne se produise. Certains outils d'analyse statique du code de recherche sont capables de détecter les inversions de priorité s'ils ont accès à l'ensemble du codebase. Bien sûr, si du code utilisateur arbitraire est impliqué (comme c'est le cas ici pour l'application) ou s'il s'agit d'une grande base de code (comme pour le noyau Linux et les pilotes de périphériques), l'analyse statique peut être impraticable. Le plus important est de lire le code très attentivement et de bien comprendre l'ensemble du système et des interactions. Des outils tels que systrace et ps -t -p sont utiles pour identifier l'inversion de priorité après qu'elle s'est produite, mais ne vous préviennent pas à l'avance.

Un dernier mot

Après toutes ces explications, n'ayez pas peur des mutex. Les mutex sont vos amis pour une utilisation ordinaire, lorsqu'ils sont utilisés et implémentés correctement dans des cas d'utilisation ordinaires non critiques en termes de temps. Toutefois, les mutex sont plus susceptibles de poser problème entre les tâches à priorité élevée et à priorité faible, ainsi que dans les systèmes sensibles au temps.