Evite a 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 apps de áudio de alto desempenho, OEMs e provedores de SoC que estão implementando um HAL de áudio. A implementação dessas técnicas não garante a prevenção de falhas ou outros erros, principalmente se usadas fora do contexto de áudio. Seus resultados podem variar, e você precisa realizar sua própria avaliação e teste.

Contexto

A implementação do cliente AudioTrack/AudioRecord e do servidor de áudio do Android AudioFlinger estão sendo reprojetados para reduzir a latência. Esse trabalho começou no Android 4.1 e continuou com outras melhorias nas versões 4.2, 4.3, 4.4 e 5.0.

Para alcançar essa latência menor, muitas mudanças foram necessárias em todo o sistema. Uma mudança importante é atribuir recursos de CPU a linhas de execução críticas com uma política de programação mais previsível. A programação confiável permite que os tamanhos e as contagens do buffer de áudio sejam reduzidos, 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).

Em um sistema de áudio, a inversão de prioridade normalmente se manifesta como um erro (clique, pop, perda), á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 de buffer 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 maior probabilidade de ocorrer nesses locais. 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 uma linha de execução de AudioTrack rápida e de mixer rápida (ambas têm prioridade elevada, mas prioridades ligeiramente diferentes)
  • entre a linha de execução de callback do aplicativo para uma linha de execução de AudioRecord rápida e 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:

  • Desativando interrupções
  • mutexes de herança de prioridade

A desativação de 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 é usada no sistema de áudio porque elas são relativamente pesadas e dependem de um cliente confiável.

Técnicas usadas pelo Android

Os experimentos começaram com "tentar bloquear" e bloqueio com tempo limite. Essas são variantes de bloqueio não bloqueantes e limitadas da operação de bloqueio do mutex. O bloqueio com e sem tempo limite funcionou bem, mas era suscetível a alguns modos de falha obscuros: não havia garantia de que o servidor pudesse 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 excederam o tempo limite.

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

  • aumentar
  • Bit a bit "ou"
  • Bit a bit "e"

Todos eles retornam o valor anterior e incluem as barreiras necessárias do SMP. A desvantagem é que elas podem exigir reexecuções 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 mal compreendidas e usadas incorretamente. Incluímos esses métodos aqui para que você tenha todas as informações, mas recomendamos que você também leia o artigo Introdução à SMP para Android para 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 escritor único não bloqueantes para dados.
  • Tente copiar o estado em vez de compartilhar o estado entre módulos de alta e baixa prioridade.
  • Quando o estado precisar ser compartilhado, limite-o ao tamanho máximo da palavra que pode ser acessada atomicamente em uma operação de um barramento sem novas tentativas.
  • Para estados complexos com várias palavras, use uma fila de estados. Uma fila de estado é basicamente uma fila FIFO de leitor único e gravação única não bloqueante usada para o estado em vez de dados, exceto que o gravador agrupa os envios adjacentes em um único envio.
  • Preste atenção nas barreiras de memória para verificar a correção do SMP.
  • Confie, mas verifique. Ao compartilhar estados entre processos, não suponha que o estado está bem formado. Por exemplo, verifique se os índices estão dentro dos limites. Essa verificação não é necessária entre linhas de execução no mesmo processo, entre processos de confiança mútua (que normalmente têm o mesmo UID). Ele também não é necessário para dados compartilhados, como áudio PCM, em que a corrupção é irrelevante.

Algoritmos não bloqueadores

Os algoritmos não bloqueadores foram o assunto de muitos estudos recentes. No entanto, com exceção das filas FIFO de um único leitor e um único gravador, elas são complexas e propensas a erros.

A partir do Android 4.2, você pode encontrar nossas classes de leitor/gravador não bloqueantes em:

  • 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. Os algoritmos não bloqueadores são conhecidos por serem difíceis de depurar. Você pode usar este 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, alguns dos exemplos de código de aplicativo do OpenSL ES precisam ser atualizados para usar algoritmos não bloqueadores ou fazer referência a uma biblioteca de código aberto que não seja do Android.

Publicamos um exemplo de implementação FIFO não bloqueante, que foi projetada especificamente para códigos de aplicativos. Confira estes arquivos localizados no diretório de origem da plataforma frameworks/av/audio_utils:

Ferramentas

Até onde sabemos, não há ferramentas automáticas para encontrar a 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 puderem acessar toda a base de código. É claro que, se um código de usuário arbitrário estiver envolvido (como no caso do aplicativo) ou se for uma base de código grande (como o kernel do Linux e os drivers de dispositivo), a análise estática pode ser impraticável. O mais importante é ler o código com muito cuidado e ter uma boa compreensão de todo o sistema e das interações. Ferramentas como systrace e ps -t -p são úteis para conferir a inversão de prioridade depois que ela ocorre, mas não informam com antecedência.

Uma palavra final

Depois de toda essa discussão, não tenha medo de mutexes. Os mutexes são seus amigos para uso comum, quando usados e implementados corretamente em casos de uso comuns que não são críticos para o tempo. No entanto, entre tarefas de alta e baixa prioridade e em sistemas sensíveis ao tempo, os mutexes têm mais chances de causar problemas.