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 gli sviluppatori di app audio ad alte prestazioni, gli OEM e i fornitori di SoC che implementano un HAL audio. Tieni presente che l'implementazione di queste tecniche non garantisce la prevenzione di problemi o altri errori, in particolare se utilizzate al di fuori del contesto audio. I risultati possono variare e devi eseguire la tua valutazione e i tuoi test.
Sfondo
Il server audio AudioFlinger di Android e l'implementazione del client AudioTrack/AudioRecord vengono riprogettati per ridurre la latenza. Questo lavoro è iniziato in Android 4.1 ed è proseguito con ulteriori miglioramenti nelle versioni 4.2, 4.3, 4.4 e 5.0.
Per ottenere questa latenza inferiore, sono state necessarie molte modifiche in tutto il sistema. Una modifica importante consiste nell'assegnare risorse CPU ai thread sensibili al tempo con una policy di pianificazione più prevedibile. La pianificazione affidabile consente di ridurre le dimensioni e i conteggi del buffer audio evitando comunque sottoproduzioni e sovrapproduzioni.
Inversione di priorità
L'inversione di priorità è una modalità di errore classica 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à più bassa 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 (clic, pop, interruzione), audio ripetuto quando vengono utilizzati buffer circolari o ritardo nella risposta a un comando.
Una soluzione alternativa comune per l'inversione di priorità è aumentare le dimensioni dei buffer audio. Tuttavia, questo metodo aumenta la latenza e nasconde semplicemente il problema anziché risolverlo. È meglio comprendere e prevenire l'inversione di priorità, come mostrato di seguito.
Nell'implementazione audio di Android, l'inversione di priorità si verifica più probabilmente in questi punti. Pertanto, 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 una traccia AudioTrack veloce e il thread del mixer veloce (entrambi hanno una priorità elevata, ma leggermente diverse)
- tra il thread di callback dell'applicazione per un AudioRecord veloce e il thread di acquisizione veloce (simile al precedente)
- all'interno dell'implementazione dell'Hardware Abstraction Layer (HAL) audio, ad es. per la telefonia o la cancellazione dell'eco
- all'interno del driver audio nel kernel
- tra il thread di callback AudioTrack o AudioRecord e altri thread dell'app (non è sotto il nostro controllo)
Soluzioni comuni
Le soluzioni tipiche includono:
- disattivazione degli interrupt
- mutex di ereditarietà della priorità
La disattivazione degli interrupt non è fattibile nello spazio utente Linux e non funziona per i processori multiprocessore simmetrici (SMP).
L'ereditarietà della priorità futexes (mutex veloci nello spazio utente) non vengono utilizzati nel sistema audio perché sono relativamente pesanti e perché si basano su un client attendibile.
Tecniche utilizzate da Android
Gli esperimenti sono iniziati con "prova chiusura" e chiusura con timeout. Queste sono varianti non bloccanti e bloccanti limitate dell'operazione di blocco mutex. I tentativi di blocco e blocco con timeout hanno funzionato abbastanza bene, ma erano suscettibili a un paio di modalità di errore oscure: il server non aveva la garanzia di poter accedere allo stato condiviso se il client era occupato e il timeout cumulativo poteva essere troppo lungo se c'era una lunga sequenza di blocchi non correlati che andavano tutti in timeout.
Utilizziamo anche operazioni atomiche come:
- aumenta
- "or" bit a bit
- AND bit a bit
Tutte queste funzioni restituiscono il valore precedente e includono le barriere SMP necessarie. Lo svantaggio è che possono richiedere tentativi illimitati. In pratica, abbiamo riscontrato che i tentativi non rappresentano un problema.
Nota:le operazioni atomiche e le loro interazioni con le barriere di memoria sono notoriamente mal comprese e utilizzate in modo errato. Abbiamo incluso questi metodi qui per completezza, ma ti consigliamo di leggere anche l'articolo SMP Primer per Android per ulteriori informazioni.
Continuiamo a utilizzare la maggior parte degli strumenti sopra indicati e di recente abbiamo aggiunto queste tecniche:
- Utilizza code FIFO single-reader single-writer 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, limita lo stato alla parola di dimensione massima a cui è possibile accedere in modo atomico in un'operazione di un bus senza nuovi tentativi.
- Per stati complessi composti da più parole, utilizza una coda di stati. Una coda di stato è fondamentalmente una coda FIFO non bloccante con un singolo lettore e un singolo writer utilizzata per lo stato anziché per i dati, tranne per il fatto che il writer comprime i push adiacenti in un unico push.
- Presta attenzione alle barriere di memoria per la correttezza SMP.
- Fidati, ma verifica. Quando condividi lo stato tra i processi, non presupporre che lo stato sia ben formato. Ad esempio, controlla che gli indici siano entro i limiti. Questa verifica non è necessaria tra i thread nello stesso processo, tra processi di fiducia reciproca (che in genere hanno lo stesso UID). Inoltre, non è 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 molti studi recenti. Tuttavia, ad eccezione delle code FIFO con un singolo lettore e un singolo writer, le abbiamo trovate complesse e soggette a errori.
A partire da Android 4.2, puoi trovare le nostre classi di lettura/scrittura non bloccanti e singole nelle seguenti posizioni:
- frameworks/av/include/media/nbaio/
- frameworks/av/media/libnbaio/
- frameworks/av/services/audioflinger/StateQueue*
Questi sono stati progettati appositamente per AudioFlinger e non sono di uso generale. Gli algoritmi non bloccanti sono noti per essere difficili da eseguire il debug. Puoi considerare questo codice come un modello. Tieni presente che potrebbero esserci bug e che le classi non sono garantite per altri scopi.
Per gli sviluppatori, parte del codice dell'applicazione OpenSL ES di esempio deve essere aggiornata per utilizzare algoritmi non bloccanti o fare riferimento a una libreria open source non Android.
Abbiamo pubblicato un'implementazione FIFO non bloccante di esempio progettata specificamente per
il codice dell'applicazione. Visualizza questi file nella directory di origine della piattaforma
frameworks/av/audio_utils
:
Strumenti
Al meglio delle nostre conoscenze, non esistono strumenti automatici per
trovare l'inversione di priorità, soprattutto prima che si verifichi. Alcuni
strumenti di analisi statica del codice di ricerca sono in grado di trovare inversioni di priorità
se sono in grado di accedere all'intero codebase. Naturalmente, se
è coinvolto codice utente arbitrario (come in questo caso per l'applicazione)
o se si tratta di una base di codice di grandi dimensioni (come per il kernel Linux e i driver di dispositivo),
l'analisi statica potrebbe non essere praticabile. La cosa più importante è leggere
il codice con molta attenzione e comprendere bene l'intero
sistema e le interazioni. Strumenti come
systrace
e
ps -t -p
sono utili per visualizzare l'inversione di priorità dopo che si è verificata, ma non
ti avvisano in anticipo.
Un'ultima parola
Dopo tutta questa discussione, non aver paura dei mutex. I mutex sono utili per l'uso ordinario, se utilizzati e implementati correttamente in casi d'uso ordinari non sensibili al tempo. Tuttavia, tra le attività ad alta e bassa priorità e nei sistemi sensibili al tempo, i mutex hanno maggiori probabilità di causare problemi.