Evitando 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 aplicativos de áudio de alto desempenho, OEMs e provedores 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 teste.

Fundo

O servidor de áudio Android AudioFlinger 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 outras melhorias nas versões 4.2, 4.3, 4.4 e 5.0.

Para atingir essa latência mais baixa, muitas mudanças foram necessárias 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 as contagens do buffer de áudio sejam reduzidos, ao mesmo tempo em que evita subexecuções e sobrecargas.

Inversão de prioridade

A inversão de prioridade é um modo de falha clássico de sistemas de tempo real, em que 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, dropout), áudio repetido quando buffers circulares são usados ​​ou atraso na resposta a um comando.

Uma solução comum para inversão de prioridade é aumentar os tamanhos do 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 visto abaixo.

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

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

Soluções comuns

As soluções típicas incluem:

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

Desabilitar interrupções não é viável no espaço de usuário do Linux e não funciona para Symmetric Multi-Processors (SMP).

Os futexes de herança de prioridade (mutexes rápidos de espaço do 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

As experiências começaram com "tentar bloqueio" e bloquear com tempo limite. Estas são variantes de bloqueio sem bloqueio e limitado da operação de bloqueio mutex. Tentar bloquear e bloquear com tempo limite funcionou muito bem, mas era suscetível a alguns modos de falha obscuros: o servidor não tinha garantia de poder 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 expiraram.

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 SMP necessárias. A desvantagem é que eles podem exigir 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 as 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 adicionamos recentemente estas técnicas:

  • Use filas FIFO de gravador único de leitor único sem bloqueio para dados.
  • Tente copiar o estado em vez de compartilhar o estado entre os módulos de alta e baixa prioridade.
  • Quando o estado precisar ser compartilhado, limite o estado à palavra de tamanho máximo que pode ser acessada atomicamente na operação de um barramento sem novas tentativas.
  • Para estado complexo de várias palavras, use uma fila de estado. Uma fila de estado é basicamente apenas uma fila FIFO de gravador único de leitor ú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 correção do SMP.
  • Confie, mas verifique . Ao compartilhar o estado entre processos, não assuma 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 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, onde uma corrupção é irrelevante.

Algoritmos não bloqueantes

Algoritmos não bloqueantes têm sido objeto de muitos estudos 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 não bloqueantes nestes locais:

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

Estes foram projetados especificamente para AudioFlinger e não são de uso geral. Algoritmos sem bloqueio são notórios por serem difíceis de depurar. Você pode olhar para 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 desenvolvedores, alguns 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 não Android.

Publicamos um exemplo de implementação de FIFO não bloqueante que foi projetada especificamente para o código do 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 a inversão de prioridade, especialmente antes que ela aconteça. Algumas ferramentas de análise de código estático de pesquisa são capazes de encontrar inversões de prioridade se puderem acessar toda a base de código. Claro, se um código de usuário arbitrário estiver envolvido (como é aqui para o aplicativo) ou é uma grande base de código (como para o kernel Linux e 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 ver a inversão de prioridade depois que ela ocorre, mas não informam antecipadamente.

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 sem tempo crítico. Mas entre tarefas de alta e baixa prioridade e em sistemas sensíveis ao tempo, os mutexes são mais propensos a causar problemas.