Este artigo explica como o sistema de áudio do Android tenta evitar a inversão de prioridade e destaca técnicas que você também pode usar.
Essas técnicas podem ser úteis para desenvolvedores de apps de áudio de alto desempenho, OEMs e provedores de SoC que estão implementando uma HAL de áudio. A implementação dessas técnicas não garante a prevenção de falhas ou outros erros, principalmente se forem usadas fora do contexto de áudio. Seus resultados podem variar, e você precisa fazer sua própria avaliação e teste.
Contexto
O servidor de áudio AudioFlinger do Android e a implementação do cliente AudioTrack/AudioRecord estão sendo reprojetados para reduzir a latência. Esse trabalho começou no Android 4.1 e continuou com mais melhorias nas versões 4.2, 4.3, 4.4 e 5.0.
Para alcançar essa latência menor, foram necessárias muitas mudanças em todo o sistema. Uma mudança importante é atribuir recursos de CPU a linhas de execução com restrição de tempo usando uma política de programação mais previsível. O agendamento confiável permite reduzir os tamanhos e as contagens de buffer de áudio, evitando underruns e overruns.
Inversão de prioridade
A inversão de prioridade é um modo de falha clássico de sistemas em tempo real, em que uma tarefa de maior prioridade é bloqueada por um tempo ilimitado aguardando que uma tarefa de menor prioridade libere um recurso, como um mutex (estado compartilhado protegido por).
Em um sistema de áudio, a inversão de prioridade geralmente se manifesta como uma falha (clique, estalo, interrupção), áudio repetido quando buffers circulares são usados ou atraso na resposta a um comando.
Uma solução alternativa comum para a inversão de prioridade é aumentar os tamanhos dos buffers de áudio. No entanto, esse método aumenta a latência e apenas oculta o problema em vez de resolvê-lo. É melhor entender e evitar a inversão de prioridade, como mostrado abaixo.
Na implementação de áudio do Android, a inversão de prioridade tem mais chances de ocorrer nesses lugares. Portanto, concentre sua atenção aqui:
- entre a linha de execução do mixer normal e a linha de execução do mixer rápido no AudioFlinger
- entre a linha de execução de callback do aplicativo para um AudioTrack rápido e uma linha de execução de mixer rápida (ambas têm prioridade elevada, mas um pouco diferente).
- entre a linha de execução de callback do aplicativo para um AudioRecord rápido e uma linha de execução de captura rápida (semelhante à anterior)
- na implementação da camada de abstração de hardware (HAL) de áudio, por exemplo, para telefonia ou cancelamento de eco
- no driver de áudio no kernel
- entre a linha de execução de callback do AudioTrack ou AudioRecord e outras linhas de execução do app (isso está fora do nosso controle)
Soluções comuns
As soluções típicas incluem:
- desativar interrupções
- mutexes de herança de prioridade
Desativar interrupções não é viável no espaço do usuário do Linux e não funciona para multiprocessadores simétricos (SMP).
A herança de prioridade futexes (mutexes rápidos do espaço do usuário) não são usados no sistema de áudio porque são relativamente pesados e dependem de um cliente confiável.
Técnicas usadas pelo Android
Os experimentos começaram com "try lock" e bloqueio com tempo limite. Essas são variantes não bloqueadoras e limitadas de bloqueio de mutex da operação. O bloqueio e o bloqueio com tempo limite funcionavam bem, mas eram suscetíveis a alguns modos de falha obscuros: não havia garantia de que o servidor conseguiria acessar o estado compartilhado se o cliente estivesse ocupado, e o tempo limite cumulativo poderia ser muito longo se houvesse uma longa sequência de bloqueios não relacionados que expirassem.
Também usamos operações atômicas, como:
- aumentar
- "ou" bit a bit
- "e" bit a bit
Todos eles retornam o valor anterior e incluem as barreiras de SMP necessárias. A desvantagem é que elas podem exigir novas tentativas ilimitadas. Na prática, descobrimos que as novas tentativas não são um problema.
Observação:as operações atômicas e as interações delas com barreiras de memória são notoriamente mal compreendidas e usadas incorretamente. Incluímos esses métodos aqui para fins de integridade, mas recomendamos que você também leia o artigo Cartilha de SMP para Android para mais informações.
Ainda temos e usamos a maioria das ferramentas acima e adicionamos recentemente estas técnicas:
- Use filas FIFO de leitura/gravação única não bloqueadoras para dados.
- Tente copiar o estado em vez de compartilhar o estado entre módulos de alta e baixa prioridade.
- Quando o estado precisa ser compartilhado, limite-o à palavra de tamanho máximo que pode ser acessada atomicamente em uma operação de um barramento sem novas tentativas.
- Para estados complexos de várias palavras, use uma fila de estados. Uma fila de estado é basicamente uma fila FIFO de leitura única e gravação única não bloqueadora usada para estado em vez de dados, exceto que o gravador recolhe envios adjacentes em um único envio.
- Preste atenção às barreiras de memória para a correção do SMP.
- Confie, mas verifique. Ao compartilhar estado entre processos, não presuma que o estado esteja bem formado. Por exemplo, verifique se os índices estão dentro dos limites. Essa verificação não é necessária entre threads no mesmo processo, entre processos de confiança mútua (que normalmente têm o mesmo UID). Também é desnecessário para dados compartilhados, como áudio PCM, em que uma corrupção é insignificante.
Algoritmos sem bloqueio
Algoritmos sem bloqueio são um tema de muitos estudos recentes. Mas, com exceção das filas FIFO de leitura única e gravação única, elas são complexas e propensas a erros.
A partir do Android 4.2, você pode encontrar nossas classes de leitura/gravação única e não bloqueadoras nestes locais:
- frameworks/av/include/media/nbaio/
- frameworks/av/media/libnbaio/
- frameworks/av/services/audioflinger/StateQueue*
Eles foram projetados especificamente para o AudioFlinger e não são de uso geral. Algoritmos não bloqueadores são conhecidos por serem difíceis de depurar. Você pode usar esse código como modelo. No entanto, pode haver bugs, e não há garantia de que as classes sejam adequadas para outras finalidades.
Para desenvolvedores, parte do código de aplicativo de exemplo do OpenSL ES precisa ser atualizada para usar algoritmos não bloqueadores ou referenciar uma biblioteca de código aberto que não seja do Android.
Publicamos um exemplo de implementação FIFO não bloqueadora projetada especificamente para
código de aplicativo. Confira esses arquivos no diretório de origem da plataforma
frameworks/av/audio_utils
:
Ferramentas
Até onde sabemos, não há ferramentas automáticas para encontrar inversão de prioridade, principalmente antes que ela aconteça. Algumas ferramentas de análise de código estático de pesquisa conseguem encontrar inversões de prioridade se tiverem acesso a toda a base de código. É claro que, se um código de usuário arbitrário estiver envolvido (como é o caso aqui para o aplicativo) ou se for uma base de código grande (como para o kernel do Linux e drivers de dispositivo), a análise estática poderá ser impraticável. O mais importante é ler o código com muito cuidado e entender bem todo o sistema e as interações. Ferramentas como
systrace
e
ps -t -p
são úteis para ver a inversão de prioridade depois que ela ocorre, mas não
informam com antecedência.
Uma última palavra
Depois de toda essa discussão, não tenha medo de mutexes. Mutexes são seus amigos para uso comum, quando usados e implementados corretamente em casos de uso comuns não sensíveis ao tempo. Mas entre tarefas de alta e baixa prioridade e em sistemas sensíveis ao tempo, os mutexes têm mais chances de causar problemas.