Éviter l'inversion de priorité

Cet article explique comment le système audio d'Android tente d'éviter l'inversion de priorité et met en avant des 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 un HAL audio. Veuillez noter que l'implémentation de ces techniques ne garantit pas l'absence de glitchs ou d'autres défaillances, en particulier si elles sont utilisées en dehors du contexte audio. Vos résultats peuvent varier. Vous devez donc effectuer votre propre évaluation et vos propres tests.

Arrière-plan

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

Pour obtenir cette latence plus faible, de nombreuses modifications ont été nécessaires dans l'ensemble du système. L'un des changements importants consiste à attribuer des ressources de processeur aux threads critiques avec une stratégie de planification plus prévisible. Une planification fiable permet de réduire les tailles et les nombres de mémoires tampons audio tout en évitant les sous-utilisations et les dépassements.

Inversion de priorité

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

Dans un système audio, l'inversion de priorité se manifeste généralement par un dysfonctionnement (clic, pop, coupure), un son répété lorsque des tampons circulaires sont utilisés ou un retard de 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 illustré ci-dessous.

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

  • entre le thread de mélangeur normal et le thread de mélangeur rapide dans AudioFlinger
  • entre le thread de rappel de l'application pour un thread AudioTrack rapide et un thread de mixeur rapide (ils ont tous deux une priorité élevée, mais des priorités légèrement différentes)
  • entre le thread de rappel de l'application pour un thread AudioRecord rapide et un 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 la suppression de l'écho
  • dans le pilote audio du noyau
  • entre le thread de rappel AudioTrack ou AudioRecord et d'autres threads d'application (ce qui ne dépend pas de Google)

Solutions courantes

Voici quelques solutions typiques:

  • désactiver les interruptions
  • mutex d'héritage de priorité

La désactivation des interruptions n'est pas possible dans l'espace utilisateur Linux et ne fonctionne pas pour les multiprocesseurs symétriques (SMP).

Les futexes (mutexes rapides dans l'espace utilisateur) ne sont pas utilisés dans le système audio, car ils sont relativement lourds et s'appuient sur un client de confiance.

Techniques utilisées par Android

Les tests ont commencé avec "try lock" et le verrouillage avec délai avant expiration. Il s'agit de variantes non bloquantes et bloquantes limitées de l'opération de verrouillage de mutex. Les méthodes TryLock et Lock avec délai avant expiration fonctionnaient plutôt bien, mais étaient sujettes à 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 avant expiration cumulé pouvait être trop long en cas de longue séquence de verrous sans rapport qui ont tous expiré.

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

  • augmenter
  • Opérateur "or" au niveau du bit
  • Opérateur "and" au niveau du bit

Tous ces éléments renvoient la valeur précédente et incluent les barrières SMP nécessaires. L'inconvénient est qu'ils peuvent nécessiter des tentatives illimitées. En pratique, nous avons constaté que les nouvelles tentatives ne posaient pas de problème.

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

Nous utilisons toujours la plupart des outils ci-dessus et nous avons récemment ajouté ces techniques:

  • Utilisez des files d'attente FIFO non bloquantes à un seul lecteur et un seul éditeur pour les données.
  • Essayez de copier l'état plutôt que de le partager entre les modules à priorité élevée et à priorité faible.
  • Lorsque l'état doit être partagé, limitez-le à la mot de taille maximale auquel vous pouvez accéder de manière atomique dans une opération à un bus sans nouvelle tentative.
  • Pour un état complexe à plusieurs mots, utilisez une file d'attente d'état. Une file d'attente d'état est essentiellement une file d'attente FIFO à un seul lecteur et un seul écrivain non bloquante utilisée pour l'état plutôt que pour les données, sauf que l'écrivain réduit les transferts adjacents en un seul transfert.
  • Portez une attention particulière aux barrières de mémoire pour la correction SMP.
  • Faites confiance, mais vérifiez. Lorsque vous partagez l'état entre les processus, ne partez pas du principe qu'il est bien formé. Par exemple, vérifiez que les indices sont dans les limites. Cette vérification n'est pas nécessaire entre les threads du même processus, entre les processus de confiance mutuelle (qui ont généralement le même UID). Il n'est pas non plus nécessaire 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 à un seul lecteur et un seul écrivain, nous avons constaté qu'elles étaient complexes et sujettes aux erreurs.

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

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

Ils ont été conçus spécifiquement pour AudioFlinger et ne sont pas destinés à un usage général. Les algorithmes non bloquants sont réputés 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 nécessairement 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 autre qu'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 une inversion de priorité, en particulier avant qu'elle ne se produise. Certains outils d'analyse du code statique de recherche sont capables de détecter des inversions de priorité s'ils peuvent accéder à l'ensemble du codebase. Bien entendu, si un code utilisateur arbitraire est impliqué (comme c'est le cas ici pour l'application) ou s'il s'agit d'un grand codebase (comme pour le noyau Linux et les pilotes d'appareils), l'analyse statique peut s'avérer peu pratique. Le plus important est de lire le code très attentivement et de bien comprendre l'ensemble du système et les interactions. Des outils tels que systrace et ps -t -p sont utiles pour détecter l'inversion de priorité après qu'elle s'est produite, mais ne vous en informent pas à l'avance.

Un dernier mot

Après toute cette discussion, n'ayez pas peur des mutex. Les mutex sont utiles pour une utilisation ordinaire, lorsqu'ils sont utilisés et implémentés correctement dans des cas d'utilisation ordinaires non critiques. Toutefois, entre les tâches à priorité élevée et celles à priorité faible, et dans les systèmes sensibles au temps, les mutexes sont plus susceptibles de causer des problèmes.