Questo articolo spiega come il sistema audio di Android tenta di evitare l'inversione di priorità e mette in evidenza le tecniche che puoi utilizzare anche tu.
Queste tecniche possono essere utili per sviluppatori di app audio ad alte prestazioni, OEM e provider di SoC che implementano un HAL audio. Tieni presente che l'implementazione di queste tecniche non è garantita per prevenire problemi o altri errori, in particolare se utilizzata al di fuori del contesto audio. I tuoi risultati possono variare e dovresti condurre la tua valutazione e test.
Sfondo
Il server audio Android AudioFlinger e l'implementazione del client AudioTrack/AudioRecord sono stati riprogettati per ridurre la latenza. Questo lavoro è iniziato in Android 4.1 ed è proseguito con ulteriori miglioramenti in 4.2, 4.3, 4.4 e 5.0.
Per ottenere questa latenza inferiore, sono state necessarie molte modifiche in tutto il sistema. Un cambiamento importante consiste nell'assegnare le risorse della CPU ai thread critici in termini di tempo con una politica di pianificazione più prevedibile. La pianificazione affidabile consente di ridurre le dimensioni e i conteggi del buffer audio, evitando al contempo underrun e overrun.
Inversione di priorità
L'inversione di priorità è una classica modalità di errore dei sistemi in tempo reale, in cui un'attività con priorità più alta viene bloccata per un tempo illimitato in attesa che un'attività con priorità inferiore rilasci una risorsa come (stato condiviso protetto da) un mutex .
In un sistema audio, l'inversione di priorità si manifesta in genere come un problema tecnico (clic, pop, abbandono), audio ripetuto quando vengono utilizzati buffer circolari o ritardo nella risposta a un comando.
Una soluzione comune per l'inversione di priorità consiste nell'aumentare le dimensioni del buffer audio. Tuttavia, questo metodo aumenta la latenza e nasconde semplicemente il problema invece di risolverlo. È meglio comprendere e prevenire l'inversione di priorità, come mostrato di seguito.
Nell'implementazione audio di Android, è più probabile che si verifichi l'inversione di priorità in questi luoghi. Quindi dovresti concentrare la tua attenzione qui:
- tra il thread del mixer normale e il thread del mixer veloce in AudioFlinger
- tra il thread di callback dell'applicazione per un AudioTrack veloce e il thread del mixer veloce (entrambi hanno priorità elevata, ma priorità leggermente diverse)
- tra il thread di callback dell'applicazione per un AudioRecord veloce e il thread di acquisizione rapida (simile al precedente)
- all'interno dell'implementazione audio Hardware Abstraction Layer (HAL), ad esempio per la telefonia o la cancellazione dell'eco
- all'interno del driver audio nel kernel
- tra il thread di richiamata AudioTrack o AudioRecord e altri thread dell'app (questo è fuori dal nostro controllo)
Soluzioni comuni
Le soluzioni tipiche includono:
- disabilitazione degli interrupt
- mutex di ereditarietà prioritaria
La disabilitazione degli interrupt non è fattibile nello spazio utente di Linux e non funziona per Symmetric Multi-Processors (SMP).
I futex dell'ereditarietà prioritaria (mutex veloci nello spazio utente) non vengono utilizzati nel sistema audio perché sono relativamente pesanti e perché si basano su un client affidabile.
Tecniche utilizzate da Android
Gli esperimenti sono iniziati con "prova blocco" e blocco con timeout. Si tratta di varianti non bloccanti e di blocco limitato dell'operazione di blocco mutex. Provare a bloccare e bloccare con timeout ha funzionato abbastanza bene ma era suscettibile di un paio di oscure modalità di errore: non era garantito che il server fosse in grado di accedere allo stato condiviso se il client fosse occupato e il timeout cumulativo potrebbe essere troppo lungo se c'è stata una lunga sequenza di blocchi non correlati che sono scaduti.
Usiamo anche operazioni atomiche come:
- incremento
- bit a bit "o"
- bit a bit "e"
Tutti questi restituiscono il valore precedente e includono le necessarie barriere SMP. Lo svantaggio è che possono richiedere tentativi illimitati. In pratica, abbiamo riscontrato che i tentativi non sono un problema.
Nota: le operazioni atomiche e le loro interazioni con le barriere di memoria sono notoriamente mal interpretate e utilizzate in modo errato. Includiamo questi metodi qui per completezza, ma ti consigliamo di leggere anche l'articolo SMP Primer per Android per ulteriori informazioni.
Abbiamo ancora e utilizziamo la maggior parte degli strumenti di cui sopra e abbiamo recentemente aggiunto queste tecniche:
- Utilizzare code FIFO a lettore singolo e scrittore singolo non bloccanti per i dati.
- Prova a copiare lo stato anziché condividere lo stato tra i moduli ad alta e bassa priorità.
- Quando è necessario condividere lo stato, limitare lo stato alla parola di dimensione massima a cui è possibile accedere atomicamente in un'operazione a bus senza tentativi.
- Per uno stato multi-parola complesso, utilizzare una coda di stato. Una coda di stato è fondamentalmente solo una coda FIFO a lettore singolo e a scrittore singolo non bloccante utilizzata per lo stato anziché per i dati, tranne per il fatto che il writer comprime i push adiacenti in un unico push.
- Prestare attenzione alle barriere di memoria per la correttezza SMP.
- Fidati, ma verifica . Quando si condivide lo stato tra processi, non dare per scontato che lo stato sia ben formato. Ad esempio, controlla che gli indici rientrino nei limiti. Questa verifica non è necessaria tra thread nello stesso processo, tra processi di fiducia reciproca (che in genere hanno lo stesso UID). Non è inoltre necessario per i dati condivisi come l'audio PCM in cui un danneggiamento è irrilevante.
Algoritmi non bloccanti
Gli algoritmi non bloccanti sono stati oggetto di studi molto recenti. Ma con l'eccezione delle code FIFO a lettore singolo e scrittore singolo, le abbiamo trovate complesse e soggette a errori.
A partire da Android 4.2, puoi trovare le nostre classi non bloccanti per lettore/scrittore singolo in queste posizioni:
- frameworks/av/include/media/nbaio/
- frameworks/av/media/libnbaio/
- frameworks/av/services/audioflinger/StateQueue*
Questi sono stati progettati specificamente per AudioFlinger e non sono generici. Gli algoritmi non bloccanti sono noti per essere difficili da eseguire il debug. Puoi guardare questo codice come un modello. Ma tieni presente che potrebbero esserci bug e non è garantito che le classi siano adatte per altri scopi.
Per gli sviluppatori, parte del codice dell'applicazione OpenSL ES di esempio deve essere aggiornato per utilizzare algoritmi non bloccanti o fare riferimento a una libreria open source non Android.
Abbiamo pubblicato un esempio di implementazione FIFO non bloccante progettata specificamente per il codice dell'applicazione. Guarda questi file che si trovano nella directory di origine della piattaforma frameworks/av/audio_utils
:
Strumenti
Per quanto ne sappiamo, non esistono strumenti automatici per trovare l'inversione di priorità, soprattutto prima che avvenga. Alcuni strumenti di analisi del codice statico di ricerca sono in grado di trovare inversioni di priorità se sono in grado di accedere all'intera codebase. Naturalmente, se è coinvolto codice utente arbitrario (come è qui per l'applicazione) o è una grande base di codice (come per il kernel Linux e i driver di dispositivo), l'analisi statica potrebbe non essere pratica. La cosa più importante è leggere il codice con molta attenzione e avere una buona conoscenza dell'intero sistema e delle interazioni. Strumenti come systrace e ps -t -p
sono utili per vedere l'inversione di priorità dopo che si è verificata, ma non te lo dicono in anticipo.
Un'ultima parola
Dopo tutta questa discussione, non aver paura dei mutex. I mutex sono tuoi amici per l'uso ordinario, se usati e implementati correttamente in casi d'uso ordinari non critici per il tempo. Ma tra le attività ad alta e bassa priorità e nei sistemi sensibili al tempo è più probabile che i mutex causino problemi.