Evitare l'inversione di priorità

Questo articolo spiega come il sistema audio di Android tenta di evitare l'inversione di priorità ed evidenzia le tecniche che puoi utilizzare anche tu.

Queste tecniche possono essere utili agli sviluppatori di app audio ad alte prestazioni, agli OEM e ai fornitori di SoC che stanno implementando un HAL audio. Tieni presente che l'implementazione di queste tecniche non garantisce la prevenzione di anomalie o altri guasti, in particolare se utilizzate al di fuori del contesto audio. I risultati possono variare e dovresti condurre la tua valutazione e i tuoi test.

Sfondo

Il server audio Android AudioFlinger e l'implementazione del client AudioTrack/AudioRecord sono stati riprogettati per ridurre la latenza. Questo lavoro è iniziato con Android 4.1 ed è continuato con ulteriori miglioramenti in 4.2, 4.3, 4.4 e 5.0.

Per ottenere questa latenza inferiore, sono state necessarie numerose modifiche in tutto il sistema. Una modifica importante consiste nell'assegnare le risorse della CPU ai thread critici in termini di tempo con una politica di pianificazione più prevedibile. Una pianificazione affidabile consente di ridurre le dimensioni e i conteggi del buffer audio evitando al tempo stesso sottocarichi e sovraccarichi.

Inversione di priorità

L'inversione della 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 tipicamente come un glitch (clic, pop, dropout), audio ripetuto quando vengono utilizzati buffer circolari o ritardo nella risposta a un comando.

Una soluzione comune per l'inversione della priorità consiste nell'aumentare le dimensioni del buffer audio. Tuttavia, questo metodo aumenta la latenza e si limita a nascondere il problema invece di risolverlo. È meglio comprendere e prevenire l'inversione di priorità, come mostrato di seguito.

Nell'implementazione audio Android, è più probabile che si verifichi l'inversione di priorità in questi luoghi. E 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 una traccia audio veloce e il thread del mixer veloce (entrambi hanno priorità elevata, ma priorità leggermente diverse)
  • tra il thread di callback dell'applicazione per una registrazione audio veloce e il thread di acquisizione veloce (simile al precedente)
  • all'interno dell'implementazione audio dell'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 callback AudioTrack o AudioRecord e altri thread dell'app (questo è fuori dal nostro controllo)

Soluzioni comuni

Le soluzioni tipiche includono:

  • disabilitazione degli interrupt
  • mutex con ereditarietà prioritaria

La disabilitazione degli interrupt non è fattibile nello spazio utente Linux e non funziona per i multiprocessori simmetrici (SMP).

I futex con 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 con blocco limitato dell'operazione di blocco mutex. Provare il blocco e il blocco con timeout funzionava abbastanza bene ma era suscettibile a un paio di oscure modalità di errore: non era garantito che il server fosse in grado di accedere allo stato condiviso se il client era occupato e il timeout cumulativo poteva essere troppo lungo se si è verificata una lunga sequenza di blocchi non correlati che sono tutti scaduti.

Usiamo anche operazioni atomiche come:

  • incremento
  • bit per bit "o"
  • bit per bit "e"

Tutti questi restituiscono il valore precedente e includono le necessarie barriere SMP. Lo svantaggio è che possono richiedere tentativi illimitati. In pratica, abbiamo scoperto che i nuovi tentativi non sono un problema.

Nota: le operazioni atomiche e le loro interazioni con le barriere della memoria sono notoriamente fraintese 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 recentemente abbiamo aggiunto queste tecniche:

  • Utilizzare code FIFO a singolo lettore e scrittore singolo non bloccanti per i dati.
  • Prova a copiare lo stato anziché condividerlo tra 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 operazioni a bus singolo senza tentativi.
  • Per stati complessi composti da più parole, utilizzare una coda di stati. Una coda di stato è fondamentalmente solo una coda FIFO a singolo lettore e scrittore non bloccante utilizzata per lo stato anziché per i dati, tranne per il fatto che lo scrittore comprime i push adiacenti in un unico push.
  • Prestare attenzione alle barriere di memoria per la correttezza SMP.
  • Fidarsi ma verificare . 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 non ha conseguenze.

Algoritmi non bloccanti

Gli algoritmi non bloccanti sono stati oggetto di studi molto recenti. Ma con l'eccezione delle code FIFO a singolo lettore e singolo scrittore, abbiamo riscontrato che sono complesse e soggette a errori.

A partire da Android 4.2, puoi trovare i nostri corsi non bloccanti a lettore/scrittore singolo in queste posizioni:

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

Questi sono stati progettati specificamente 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. Ma tieni presente che potrebbero esserci dei bug e non è garantito che le classi siano adatte per altri scopi.

Per gli sviluppatori, parte del codice dell'applicazione OpenSL ES di esempio dovrebbe 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. Vedi questi file situati nella directory dei sorgenti della piattaforma frameworks/av/audio_utils :

Utensili

Per quanto ne sappiamo, non esistono strumenti automatici per individuare 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 base di codice. Naturalmente, se è coinvolto un codice utente arbitrario (come nel caso dell'applicazione) o si tratta di una base di codice di grandi dimensioni (come nel caso del kernel Linux e dei driver del dispositivo), l'analisi statica potrebbe essere poco pratica. La cosa più importante è leggere il codice con molta attenzione e acquisire 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 utilizzati e implementati correttamente in casi d'uso ordinari non critici in termini di tempo. Ma tra compiti ad alta e bassa priorità e nei sistemi sensibili al fattore tempo, i mutex hanno maggiori probabilità di causare problemi.