Evitare l'inversione della 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 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 ed è consigliabile eseguire una valutazione e un test autonomi.

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 le risorse della CPU ai thread time-critical con una policy di pianificazione più prevedibile. La pianificazione affidabile consente di ridurre le dimensioni e il numero dei buffer audio, evitando al contempo underflow e overflow.

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, ad esempio (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 del buffer audio. Tuttavia, questo metodo aumenta la latenza e nasconde semplicemente il problema anziché risolverlo. È meglio comprendere e prevenire l'inversione di priorità, come illustrato 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 un AudioTrack veloce e il thread del mixer veloce (entrambi hanno una priorità elevata, ma priorità leggermente diverse)
  • Tra il thread di callback dell'applicazione per un AudioRecord veloce e il thread di acquisizione veloce (simile al precedente)
  • Nell'implementazione dell'HAL (Hardware Abstraction Layer) audio, ad es. per la telefonia o la cancellazione dell'eco
  • All'interno del driver audio nel kernel
  • Tra il thread di callback di AudioTrack o AudioRecord e altri thread dell'app (questo è fuori dal 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 multiprocessori simmetrici (SMP).

I futex di ereditarietà della priorità (mutex veloci dello 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 "try lock" e lock con timeout. Queste sono varianti di blocco non bloccanti e limitate dell'operazione di blocco mutex. Try lock e lock con timeout hanno funzionato abbastanza bene, ma erano soggetti a un paio di modalità di errore oscure: non era garantito che il server potesse accedere allo stato condiviso se il client era occupato e il timeout cumulativo poteva essere troppo lungo se era presente una lunga sequenza di blocchi non correlati che hanno raggiunto il timeout.

Utilizziamo anche operazioni atomiche come:

  • aumenta
  • "or" bit a bit
  • "and" bit a bit

Tutte queste restituiscono il valore precedente e includono le barriere SMP necessarie. Lo svantaggio è che possono richiedere nuovi 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 di memoria sono notoriamente mal comprese e utilizzate in modo errato. Includiamo questi metodi qui per completezza, ma ti consigliamo di leggere anche l'articolo SMP Primer for Android per ulteriori informazioni.

Abbiamo ancora e utilizziamo la maggior parte degli strumenti sopra indicati e di recente abbiamo aggiunto queste tecniche:

  • Utilizza code FIFO (First In, First Out) non bloccanti a singolo lettore e singolo scrittore per i dati.
  • Prova a copiare lo stato anziché condividere lo stato tra i moduli con priorità alta e bassa.
  • Quando è necessario condividere lo stato, limitalo alla parola di dimensione massima a cui è possibile accedere in modo atomico in un'operazione bus senza nuovi tentativi.
  • Per lo stato complesso di più parole, utilizza una coda di stato. Una coda di stato è fondamentalmente una coda FIFO non bloccante a singolo lettore e singolo scrittore utilizzata per lo stato anziché per i dati, tranne per il fatto che lo scrittore comprime le push adiacenti in una singola push.
  • Presta attenzione alle barriere di memoria per la correttezza SMP.
  • Fidati, ma verifica. Quando condividi lo stato tra i processi, non dare per scontato 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 i processi di fiducia reciproca (che in genere hanno lo stesso UID). È anche inutile per i dati condivisi, ad esempio l'audio PCM, in cui una corruzione è irrilevante.

Algoritmi non bloccanti

Gli algoritmi non bloccanti sono stati oggetto di molti studi recenti. Tuttavia, ad eccezione delle code FIFO a singolo lettore e singolo scrittore, li abbiamo trovati complessi e soggetti a errori.

A partire da Android 4.2, puoi trovare le nostre classi non bloccanti a singolo lettore/scrittore in queste 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 generico. Gli algoritmi non bloccanti sono notoriamente difficili da eseguire il debug. Puoi considerare questo codice come un modello. Tuttavia, tieni presente che potrebbero esserci bug e che non è garantito che le classi siano adatte ad altri scopi.

Per gli sviluppatori, alcuni dei codici dell'applicazione OpenSL ES di esempio devono essere aggiornati 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 appositamente per il codice dell'applicazione. Consulta questi file che si trovano 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 possono accedere all'intero codebase. Naturalmente, se è coinvolto codice utente arbitrario (come in questo caso per l'applicazione) o se si tratta di un codebase di grandi dimensioni (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 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 normale, se utilizzati e implementati correttamente in casi d'uso normali non time-critical. Tuttavia, tra le attività con priorità alta e bassa e nei sistemi time-sensitive, i mutex hanno maggiori probabilità di causare problemi.