Évitez l’inversion des priorités

Cet article explique comment le système audio d'Android tente d'éviter l'inversion des priorités et met en évidence 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 la mise en œuvre de ces techniques n'est pas garantie pour éviter les problèmes ou autres pannes, en particulier si elles sont utilisées en dehors du contexte audio. Vos résultats peuvent varier et vous devez effectuer votre propre évaluation et vos propres tests.

Arrière-plan

L'implémentation du serveur audio Android AudioFlinger et du client AudioTrack/AudioRecord est en cours de réorganisation pour réduire la latence. Ce travail a commencé avec 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 obtenir cette latence plus faible, de nombreux changements ont été nécessaires dans l’ensemble du système. Un changement important consiste à attribuer des ressources CPU aux threads à temps critique avec une politique de planification plus prévisible. Une planification fiable permet de réduire la taille et le nombre de 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 temps réel, dans lequel une tâche de priorité supérieure 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 (é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, pop, abandon), un son répété lorsque des tampons circulaires sont utilisés ou un retard dans la réponse à une commande.

Une solution de contournement courante pour l’inversion des priorités consiste à augmenter la taille des tampons audio. Cependant, 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 des priorités, comme indiqué ci-dessous.

Dans l’implémentation audio Android, l’inversion des priorités est plus susceptible de se produire à ces endroits. Vous devriez donc concentrer votre attention ici :

  • entre le fil de mixage normal et le fil de mixage rapide dans AudioFlinger
  • entre le thread de rappel d'application pour un AudioTrack rapide et le thread de mixage rapide (ils ont tous deux une priorité élevée, mais des priorités légèrement différentes)
  • entre le fil de rappel de l'application pour un enregistrement audio rapide et le fil de capture rapide (similaire au précédent)
  • dans l'implémentation audio de la couche d'abstraction matérielle (HAL), par exemple pour la téléphonie ou l'annulation d'écho
  • dans le pilote audio du noyau
  • entre le fil de rappel AudioTrack ou AudioRecord et d'autres fils d'application (cela est hors de notre contrôle)

Solutions communes

Les solutions typiques incluent :

  • désactiver les interruptions
  • mutex d'héritage prioritaire

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

Les futex d'héritage prioritaire (mutex rapides de l'espace utilisateur) ne sont pas utilisés dans le système audio car ils sont relativement lourds et parce qu'ils reposent sur un client de confiance.

Techniques utilisées par Android

Les expériences ont commencé avec « essayer le verrouillage » et le verrouillage avec délai d'attente. Il s'agit de variantes non bloquantes et à blocage limité de l'opération de verrouillage mutex. Les tentatives de verrouillage et de verrouillage avec délai d'attente fonctionnaient assez bien, mais étaient sensibles à quelques modes de défaillance obscurs : il n'était pas garanti que le serveur puisse accéder à l'état partagé si le client était occupé, et le délai d'attente cumulé pouvait être trop long si il y a eu une longue séquence de verrous sans rapport qui ont tous expiré.

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

  • incrément
  • au niveau du bit "ou"
  • au niveau du bit "et"

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 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 par souci d'exhaustivité, mais nous vous recommandons également de lire l'article SMP Primer pour Android pour plus d'informations.

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

  • 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 partager l'état entre les modules de priorité élevée et faible.
  • Lorsque l’état doit être partagé, limitez-le au mot de taille maximale accessible de manière atomique dans le cadre d’un fonctionnement sur un seul bus sans nouvelle tentative.
  • Pour un état complexe à plusieurs mots, utilisez une file d'attente d'état. Une file d'attente d'état est fondamentalement simplement une file d'attente FIFO non bloquante à lecteur unique et à écriture unique utilisée pour l'état plutôt que pour les données, sauf que l'écrivain réduit les poussées adjacentes en une seule poussée.
  • Faites attention aux barrières de mémoire pour l'exactitude du SMP.
  • Faites confiance, mais vérifiez . Lorsque vous partagez un état entre des processus, ne supposez pas que l'état 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). Cela 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. Mais à 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 pouvez trouver nos cours non bloquants à lecteur/écrivain unique aux emplacements suivants :

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

Ceux-ci ont été conçus 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 regarder ce code comme modèle. Mais sachez qu’il peut y avoir des bugs et qu’il n’est pas garanti que les cours soient adaptés à 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 plateforme frameworks/av/audio_utils :

Outils

À notre connaissance, il n’existe pas d’outils automatiques permettant de détecter une inversion de priorité, surtout avant qu’elle ne se produise. Certains outils d'analyse de code statique de recherche sont capables de trouver des inversions de priorité s'ils peuvent accéder à l'intégralité de la base de code. 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 base de code volumineuse (comme pour le noyau Linux et les pilotes de périphériques), l'analyse statique peut s'avérer peu pratique. Le plus important est de lire très attentivement le code et de bien appréhender l’ensemble du système et ses interactions. Des outils tels que systrace et ps -t -p sont utiles pour voir l'inversion des priorités après qu'elle se soit produite, mais ne vous le disent pas à l'avance.

Un dernier mot

Après toute cette discussion, 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. Mais entre les tâches à priorité élevée et faible et dans les systèmes sensibles au facteur temps, les mutex sont plus susceptibles de causer des problèmes.