Cet article explique comment le système audio d'Android tente d'éviter l'inversion de priorité et met en évidence 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 la mise en œuvre de ces techniques ne garantit pas la prévention des problèmes ou autres défaillances, 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
Le serveur audio Android AudioFlinger et l'implémentation du client AudioTrack/AudioRecord sont 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 4.2, 4.3, 4.4 et 5.0.
Pour atteindre cette latence plus faible, de nombreux changements ont été nécessaires dans tout le système. Un changement important consiste à affecter des ressources CPU à des threads critiques 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 prioritaire
L'inversion de priorité est un mode de défaillance classique des systèmes en temps réel, où une tâche de priorité supérieure est bloquée pendant un temps illimité en attendant qu'une tâche de priorité inférieure libère une ressource telle que (é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, décrochage), 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 de priorité consiste à augmenter la taille des tampons audio. Cependant, cette méthode augmente la latence et masque simplement le problème au lieu de le résoudre. Il vaut mieux comprendre et prévenir l'inversion de priorité, comme on le voit ci-dessous.
Dans l'implémentation audio d'Android, l'inversion de priorité est plus susceptible de se produire à ces endroits. Et donc vous devriez concentrer votre attention ici :
- entre le thread de mixage normal et le thread de mixage rapide dans AudioFlinger
- entre le fil de rappel d'application pour un AudioTrack rapide et le fil 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 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 fil de rappel AudioTrack ou AudioRecord et d'autres fils d'application (cela est hors de notre contrôle)
Solutions courantes
Les solutions typiques incluent :
- désactivation des interruptions
- mutex d'héritage prioritaire
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 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 reposent sur un client de confiance.
Techniques utilisées par Android
Les expériences ont commencé avec "essayer de verrouiller" et verrouiller avec un délai d'attente. Ce sont des variantes non bloquantes et bloquantes limitées de l'opération de verrouillage mutex. Essayer de verrouiller et de verrouiller avec un délai d'attente fonctionnait assez bien, mais était susceptible de quelques modes d'échec obscurs : le serveur n'était pas assuré de pouvoir accéder à l'état partagé si le client était occupé, et le délai d'attente cumulé pouvait être trop long si il y avait une longue séquence de verrous non liés qui ont tous expiré.
Nous utilisons également des opérations atomiques telles que :
- incrément
- "ou" au niveau du bit
- "et" au niveau du bit
Tous 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 tentatives ne sont pas un 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 vous recommandons également de lire l'article SMP Primer pour Android pour plus d'informations.
Nous avons et utilisons toujours la plupart des outils ci-dessus, et avons récemment ajouté ces techniques :
- Utilisez des files d'attente FIFO à lecteur unique et à écriture unique non bloquantes pour les données.
- Essayez de copier l'état plutôt que de partager l'état entre les modules de haute et de basse priorité.
- Lorsque l'état doit être partagé, limitez l'état au mot de taille maximale accessible de manière atomique dans un fonctionnement à un bus sans nouvelles tentatives.
- Pour un état multimot complexe, utilisez une file d'attente d'état. Une file d'attente d'état est essentiellement une file d'attente FIFO non bloquante à lecteur unique et à écrivain unique utilisée pour l'état plutôt que pour les données, sauf que l'écrivain regroupe 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 . Lors du partage d'état entre 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 d'approbation mutuelle (qui ont généralement le même UID). C'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 à auteur unique, nous les avons trouvées complexes et sujettes aux erreurs.
À partir d'Android 4.2, vous pouvez trouver nos cours non bloquants à lecture/écriture 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 considérer ce code comme un modèle. Mais sachez qu'il peut y avoir des bogues 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. Voir 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 pour trouver l'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'ensemble de la base de code. Bien sûr, si un 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 le code très attentivement et d'avoir une bonne compréhension de l'ensemble du système et des interactions. Des outils tels que systrace et ps -t -p
sont utiles pour voir l'inversion de priorité après qu'elle se soit produite, mais ne vous le prévenez 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. Mais entre les tâches à haute et basse priorité et dans les systèmes sensibles au facteur temps, les mutex sont plus susceptibles de causer des problèmes.