Evite inversão de prioridade

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 aplicativos de áudio de alto desempenho, OEMs e fornecedores de SoC que estão implementando um HAL de áudio. Observe que a implementação dessas técnicas não garante a prevenção de falhas ou outras falhas, principalmente se usadas fora do contexto de áudio. Seus resultados podem variar e você deve realizar sua própria avaliação e testes.

Fundo

O servidor de áudio Android AudioFlinger e a implementação do cliente AudioTrack/AudioRecord estão sendo reprojetados para reduzir a latência. Este trabalho começou no Android 4.1 e continuou com melhorias adicionais no 4.2, 4.3, 4.4 e 5.0.

Para atingir essa latência mais baixa, foram necessárias muitas mudanças em todo o sistema. Uma mudança importante é atribuir recursos de CPU a threads de tempo crítico com uma política de agendamento mais previsível. O agendamento confiável permite que os tamanhos e contagens do buffer de áudio sejam reduzidos, evitando, ao mesmo tempo, execuções insuficientes e excessivas.

Inversão de prioridade

A inversão de prioridade é um modo de falha clássico de sistemas em tempo real, onde uma tarefa de prioridade mais alta é bloqueada por um tempo ilimitado aguardando que uma tarefa de prioridade mais baixa libere um recurso como (estado compartilhado protegido por) um mutex .

Em um sistema de áudio, a inversão de prioridade normalmente se manifesta como uma falha (clique, pop, abandono), á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 o tamanho do buffer de áudio. No entanto, este método aumenta a latência e apenas oculta o problema em vez de resolvê-lo. É melhor compreender e prevenir a inversão de prioridades, conforme visto a seguir.

Na implementação de áudio do Android, é mais provável que ocorra inversão de prioridade nesses locais. E então você deve concentrar sua atenção aqui:

  • entre thread de mixer normal e thread de mixer rápido no AudioFlinger
  • entre o thread de retorno de chamada do aplicativo para um AudioTrack rápido e o thread de mixagem rápido (ambos têm prioridade elevada, mas prioridades ligeiramente diferentes)
  • entre o thread de retorno de chamada do aplicativo para um AudioRecord rápido e o thread de captura rápida (semelhante ao anterior)
  • dentro da implementação da Camada de Abstração de Hardware (HAL) de áudio, por exemplo, para telefonia ou cancelamento de eco
  • dentro do driver de áudio no kernel
  • entre o thread de retorno de chamada AudioTrack ou AudioRecord e outros threads de aplicativos (isso está fora de nosso controle)

Soluções comuns

As soluções típicas incluem:

  • desabilitando interrupções
  • mutexes de herança prioritária

Desabilitar interrupções não é viável no espaço do usuário Linux e não funciona para Multiprocessadores Simétricos (SMP).

Futexes de herança prioritária (mutexes rápidos de espaço de usuário) não são usados ​​no sistema de áudio porque são relativamente pesados ​​e porque dependem de um cliente confiável.

Técnicas usadas pelo Android

Os experimentos começaram com "try lock" e lock with timeout. Estas são variantes de bloqueio limitado e sem bloqueio da operação de bloqueio mutex. Tentar bloquear e bloquear com tempo limite funcionou razoavelmente bem, mas era suscetível a alguns modos de falha obscuros: não era garantido que o servidor conseguiria acessar o estado compartilhado se o cliente estivesse ocupado, e o tempo limite cumulativo poderia ser muito longo se houve uma longa sequência de bloqueios não relacionados que expirou.

Também usamos operações atômicas como:

  • incremento
  • bit a bit "ou"
  • bit a bit "e"

Todos estes retornam o valor anterior e incluem as barreiras necessárias do SMP. A desvantagem é que eles podem exigir novas tentativas ilimitadas. Na prática, descobrimos que as novas tentativas não são um problema.

Nota: As operações atômicas e suas interações com barreiras de memória são notoriamente mal compreendidas e usadas incorretamente. Incluímos esses métodos aqui para completar, mas recomendamos que você também leia o artigo SMP Primer para Android para obter mais informações.

Ainda temos e usamos a maioria das ferramentas acima e recentemente adicionamos estas técnicas:

  • Use filas FIFO de leitor único e gravador único sem bloqueio para dados.
  • Tente copiar o estado em vez de compartilhá -lo entre módulos de alta e baixa prioridade.
  • Quando o estado precisar ser compartilhado, limite-o à palavra de tamanho máximo que pode ser acessada atomicamente na 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 apenas uma fila FIFO de leitor único e gravador único, sem bloqueio, usada para estado em vez de dados, exceto que o gravador recolhe pushes adjacentes em um único push.
  • Preste atenção às barreiras de memória para a correção do SMP.
  • Confie mas verifique . Ao compartilhar o 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 possuem o mesmo UID). Também é desnecessário para dados compartilhados, como áudio PCM, onde a corrupção é irrelevante.

Algoritmos sem bloqueio

Algoritmos sem bloqueio têm sido objeto de estudos muito recentes. Mas, com exceção das filas FIFO de leitor único e gravador único, descobrimos que elas são complexas e propensas a erros.

A partir do Android 4.2, você pode encontrar nossas classes de leitor/gravador único e sem bloqueio nestes locais:

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

Eles foram projetados especificamente para AudioFlinger e não são de uso geral. Algoritmos sem bloqueio são conhecidos por serem difíceis de depurar. Você pode ver este código como um modelo. Mas esteja ciente de que pode haver bugs e não há garantia de que as classes sejam adequadas para outros fins.

Para os desenvolvedores, alguns dos exemplos de código do aplicativo OpenSL ES devem ser atualizados para usar algoritmos sem bloqueio ou fazer referência a uma biblioteca de código aberto que não seja Android.

Publicamos um exemplo de implementação FIFO sem bloqueio que foi projetado especificamente para código de aplicativo. Veja estes arquivos localizados no diretório de origem da plataforma frameworks/av/audio_utils :

Ferramentas

Até onde sabemos, não existem ferramentas automáticas para encontrar inversão de prioridade, especialmente antes que ela aconteça. Algumas ferramentas de pesquisa de análise de código estático são capazes de encontrar inversões de prioridade se conseguirem acessar 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 houver uma grande base de código (como para o kernel Linux e drivers de dispositivo), a análise estática poderá ser impraticável. O mais importante é ler o código com muita atenção e ter uma boa noção de todo o sistema e das 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 avisam com antecedência.

Uma palavra final

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 que não exigem tempo crítico. Mas entre tarefas de alta e baixa prioridade e em sistemas sensíveis ao tempo, os mutexes têm maior probabilidade de causar problemas.