Evita la inversión de prioridad

En este artículo, se explica cómo el sistema de audio de Android intenta evitar la inversión de prioridad y se destacan las técnicas que también puedes usar.

Estas técnicas pueden ser útiles para los desarrolladores de apps de audio de alto rendimiento, OEMs y proveedores de SoC que implementan un HAL de audio. Ten en cuenta que no se garantiza que la implementación de estas técnicas evite fallas o errores, en especial, si se usan fuera del contexto de audio. Los resultados pueden variar, por lo que debes realizar tu propia evaluación y pruebas.

Información general

Se está rediseñando el servidor de audio AudioFlinger de Android y la implementación del cliente de AudioTrack/AudioRecord para reducir la latencia. Este trabajo comenzó en Android 4.1 y continuó con más mejoras en 4.2, 4.3, 4.4 y 5.0.

Para lograr esta latencia más baja, se necesitaron muchos cambios en todo el sistema. Un cambio importante es asignar recursos de la CPU a subprocesos urgentes con una política de programación más predecible. La programación confiable permite reducir los tamaños y recuentos de los búferes de audio y, al mismo tiempo, evitar subdesbordamientos y desbordamientos.

Inversión de prioridad

La inversión de prioridad es un modo de falla clásico de los sistemas en tiempo real, en el que una tarea de prioridad más alta se bloquea durante un tiempo ilimitado a la espera de que una tarea de prioridad más baja libere un recurso, como un mutex (estado compartido protegido por un mutex).

En un sistema de audio, la inversión de prioridad suele manifestarse como un error (clic, pop, desconexión), audio repetido cuando se usan búferes circulares o demora en responder a un comando.

Una solución alternativa común para la inversión de prioridad es aumentar los tamaños de los búferes de audio. Sin embargo, este método aumenta la latencia y solo oculta el problema en lugar de resolverlo. Es mejor comprender y evitar la inversión de prioridad, como se muestra a continuación.

En la implementación de audio de Android, es más probable que la inversión de prioridad se produzca en estos lugares. Por lo tanto, debes enfocar tu atención en lo siguiente:

  • entre el subproceso del mezclador normal y el subproceso del mezclador rápido en AudioFlinger
  • entre el subproceso de devolución de llamada de la aplicación para un AudioTrack rápido y un subproceso de mezclador rápido (ambos tienen prioridad elevada, pero prioridades ligeramente diferentes)
  • entre el subproceso de devolución de llamada de la aplicación para un AudioRecord rápido y un subproceso de captura rápido (similar al anterior)
  • dentro de la implementación de la capa de abstracción de hardware (HAL) de audio, p.ej., para telefonía o cancelación de eco
  • dentro del controlador de audio en el kernel
  • entre el subproceso de devolución de llamada de AudioTrack o AudioRecord y otros subprocesos de la app (esto está fuera de nuestro control)

Soluciones habituales

Entre las soluciones típicas, se incluyen las siguientes:

  • inhabilitar interrupciones
  • mutexes de herencia de prioridad

Inhabilitar las interrupciones no es factible en el espacio de usuario de Linux y no funciona para los multiprocesadores simétricos (SMP).

Los futexes de herencia de prioridad (mutexes de espacio de usuario rápido) no se usan en el sistema de audio porque son relativamente pesados y dependen de un cliente de confianza.

Técnicas que usa Android

Los experimentos se iniciaron con "try lock" y bloqueo con tiempo de espera. Estas son variantes de bloqueo no bloqueante y de bloqueo limitado de la operación de bloqueo de mutex. El bloqueo y el bloqueo con tiempo de espera funcionaron bastante bien, pero eran susceptibles a un par de modos de falla poco claros: no se garantizaba que el servidor pudiera acceder al estado compartido si el cliente estaba ocupado, y el tiempo de espera acumulativo podría ser demasiado largo si había una secuencia larga de bloqueos no relacionados que se agotaban.

También usamos operaciones atómicas, como las siguientes:

  • incremento
  • "or" a nivel de bits
  • "and" a nivel de bits

Todas estas opciones muestran el valor anterior y las barreras de SMP necesarias. La desventaja es que pueden requerir reintentos ilimitados. En la práctica, descubrimos que las reintentos no son un problema.

Nota: Las operaciones atómicas y sus interacciones con las barreras de memoria son muy mal entendidas y se usan de forma incorrecta. Incluimos estos métodos aquí para que tengas toda la información, pero te recomendamos que también leas el artículo Introducción a SMP para Android para obtener más información.

Aún tenemos y usamos la mayoría de las herramientas anteriores y, recientemente, añadimos estas técnicas:

  • Usa colas FIFO de un solo lector y un solo escritor no bloqueantes para los datos.
  • Intenta copiar el estado en lugar de compartirlo entre módulos de alta y prioridad baja.
  • Cuando se necesite compartir el estado, limítalo a la palabra de tamaño máximo a la que se puede acceder de forma atómica en la operación de un solo bus sin reintentos.
  • Para estados complejos de varias palabras, usa una cola de estado. Una cola de estado es, básicamente, una cola FIFO de un solo lector y un solo escritor no bloqueante que se usa para el estado en lugar de los datos, excepto que el escritor contrae los envíos adyacentes en un solo envío.
  • Presta atención a las barreras de memoria para que el SMP sea correcto.
  • Confía, pero verifica. Cuando compartas el estado entre procesos, no des por sentado que el estado está bien formado. Por ejemplo, verifica que los índices estén dentro de los límites. No se necesita esta verificación entre subprocesos en el mismo proceso, entre procesos de confianza mutua (que suelen tener el mismo UID). Tampoco es necesario para los datos compartidos, como el audio PCM, en el que un daño es irrelevante.

Algoritmos no bloqueantes

Los algoritmos no bloqueantes se han estudiado mucho en los últimos tiempos. Sin embargo, con la excepción de las colas FIFO de un solo lector y un solo escritor, descubrimos que son complejas y propensas a errores.

A partir de Android 4.2, puedes encontrar nuestras clases de un solo lector/escritor que no bloquean en estas ubicaciones:

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

Se diseñaron específicamente para AudioFlinger y no son de uso general. Los algoritmos no bloqueantes son conocidos por ser difíciles de depurar. Puedes considerar este código como un modelo. Sin embargo, ten en cuenta que puede haber errores y que no se garantiza que las clases sean adecuadas para otros fines.

En el caso de los desarrolladores, se debe actualizar parte del código de muestra de la aplicación de OpenSL ES para usar algoritmos no bloqueantes o hacer referencia a una biblioteca de código abierto que no sea de Android.

Publicamos un ejemplo de implementación de FIFO no bloqueante que está diseñado específicamente para el código de la aplicación. Consulta estos archivos ubicados en el directorio fuente de la plataforma frameworks/av/audio_utils:

Herramientas

Según nuestro conocimiento, no existen herramientas automáticas para encontrar la inversión de prioridad, especialmente antes de que ocurra. Algunas herramientas de investigación de análisis de código estático son capaces de encontrar inversiones de prioridad si pueden acceder a toda la base de código. Por supuesto, si está involucrado un código de usuario arbitrario (como es el caso de la aplicación) o es una base de código grande (como en el caso del kernel de Linux y los controladores de dispositivos), el análisis estático puede no ser práctico. Lo más importante es leer el código con mucho cuidado y comprender bien todo el sistema y las interacciones. Las herramientas como systrace y ps -t -p son útiles para ver la inversión de prioridad después de que ocurre, pero no te lo informan con anticipación.

Una última palabra

Después de toda esta discusión, no tengas miedo de los mutex. Los mutex son tus aliados para el uso general, cuando se usan e implementan correctamente en casos de uso normales que no son urgentes. Sin embargo, entre las tareas de prioridad alta y baja, y en los sistemas sensibles al tiempo, es más probable que los mutexes causen problemas.