Este artículo explica cómo el sistema de audio de Android intenta evitar la inversión de prioridad y destaca técnicas que usted también puede utilizar.
Estas técnicas pueden resultar útiles para los desarrolladores de aplicaciones de audio de alto rendimiento, OEM y proveedores de SoC que están implementando un HAL de audio. Tenga en cuenta que no se garantiza que la implementación de estas técnicas evite fallas u otras fallas, especialmente si se usan fuera del contexto de audio. Sus resultados pueden variar y usted debe realizar su propia evaluación y pruebas.
Fondo
El servidor de audio Android AudioFlinger y la implementación del cliente AudioTrack/AudioRecord se están rediseñando para reducir la latencia. Este trabajo comenzó en Android 4.1 y continuó con mejoras adicionales en 4.2, 4.3, 4.4 y 5.0.
Para lograr esta menor latencia, se necesitaron muchos cambios en todo el sistema. Un cambio importante es asignar recursos de CPU a subprocesos en los que el tiempo es crítico con una política de programación más predecible. La programación confiable permite reducir los tamaños y recuentos del búfer de audio y, al mismo tiempo, evitar insuficiencias y desbordamientos.
Inversión de prioridad
La inversión de prioridad es un modo de falla clásico de los sistemas en tiempo real, donde una tarea de mayor prioridad se bloquea durante un tiempo ilimitado esperando que una tarea de menor prioridad libere un recurso como (estado compartido protegido por) un mutex .
En un sistema de audio, la inversión de prioridad generalmente se manifiesta como un fallo (clic, pop, abandono), audio repetido cuando se utilizan buffers circulares o retraso en la respuesta a un comando.
Una solución alternativa común para la inversión de prioridad es aumentar el tamaño del búfer de audio. Sin embargo, este método aumenta la latencia y simplemente oculta el problema en lugar de resolverlo. Es mejor comprender y prevenir la inversión de prioridad, como se ve a continuación.
En la implementación de audio de Android, es más probable que se produzca inversión de prioridad en estos lugares. Y entonces deberías centrar tu atención aquí:
- entre el hilo del mezclador normal y el hilo del mezclador rápido en AudioFlinger
- entre el hilo de devolución de llamada de la aplicación para una pista de audio rápida y el hilo del mezclador rápido (ambos tienen prioridad elevada, pero prioridades ligeramente diferentes)
- entre el hilo de devolución de llamada de la aplicación para un AudioRecord rápido y el hilo de captura rápida (similar al anterior)
- dentro de la implementación de la capa de abstracción de hardware (HAL) de audio, por ejemplo, para telefonía o cancelación de eco
- dentro del controlador de audio en el kernel
- entre el hilo de devolución de llamada AudioTrack o AudioRecord y otros hilos de la aplicación (esto está fuera de nuestro control)
Soluciones comunes
Las soluciones típicas incluyen:
- deshabilitar interrupciones
- exclusión mutua de herencia prioritaria
Deshabilitar las interrupciones no es factible en el espacio de usuario de Linux y no funciona para multiprocesadores simétricos (SMP).
Los futex de herencia prioritaria (mutex rápidos de espacio de usuario) no se utilizan en el sistema de audio porque son relativamente pesados y porque dependen de un cliente confiable.
Técnicas utilizadas por Android
Los experimentos comenzaron con "intento de bloqueo" y bloqueo con tiempo de espera. Estas son variantes sin bloqueo y con bloqueo limitado de la operación de bloqueo mutex. Intentar bloquear y bloquear con tiempo de espera funcionó bastante bien, pero era susceptible a un par de modos de falla oscuros: 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 hubo una larga secuencia de bloqueos no relacionados y todos expiraron.
También utilizamos operaciones atómicas como:
- incremento
- bit a bit "o"
- bit a bit "y"
Todos estos devuelven el valor anterior e incluyen las barreras SMP necesarias. La desventaja es que pueden requerir reintentos ilimitados. En la práctica, hemos descubierto que los reintentos no son un problema.
Nota: Las operaciones atómicas y sus interacciones con las barreras de la memoria son notoriamente mal entendidas y utilizadas incorrectamente. Incluimos estos métodos aquí para que estén completos, pero le recomendamos que lea también el artículo SMP Primer para Android para obtener más información.
Todavía tenemos y utilizamos la mayoría de las herramientas anteriores y recientemente hemos agregado estas técnicas:
- Utilice colas FIFO de un solo lector y un solo escritor sin bloqueo para los datos.
- Intente copiar el estado en lugar de compartirlo entre módulos de alta y baja prioridad.
- Cuando sea necesario compartir el estado, limítelo a la palabra de tamaño máximo a la que se pueda acceder atómicamente en operación de un solo bus sin reintentos.
- Para estados complejos de varias palabras, utilice una cola de estados. Una cola de estado es básicamente una cola FIFO de un solo lector y un solo escritor sin bloqueo que se utiliza para el estado en lugar de los datos, excepto que el escritor colapsa las inserciones adyacentes en una sola inserción.
- Preste atención a las barreras de la memoria para la corrección del SMP.
- Confiar pero verificar . Al compartir el estado entre procesos, no asuma que el estado está bien formado. Por ejemplo, compruebe que los índices estén dentro de los límites. Esta verificación no es necesaria entre subprocesos del mismo proceso, entre procesos de confianza mutua (que normalmente tienen el mismo UID). También es innecesario para datos compartidos como el audio PCM, donde la corrupción no tiene consecuencias.
Algoritmos sin bloqueo
Los algoritmos sin bloqueo han sido objeto de muchos estudios recientes. Pero con la excepción de las colas FIFO de un solo lector y un solo escritor, hemos descubierto que son complejas y propensas a errores.
A partir de Android 4.2, puede encontrar nuestras clases de un solo lector/escritor sin bloqueo en estas ubicaciones:
- marcos/av/include/media/nbaio/
- marcos/av/media/libnbaio/
- frameworks/av/services/audioflinger/StateQueue*
Estos fueron diseñados específicamente para AudioFlinger y no son de uso general. Los algoritmos sin bloqueo son conocidos por ser difíciles de depurar. Puedes mirar este código como modelo. Pero tenga en cuenta que puede haber errores y no se garantiza que las clases sean adecuadas para otros fines.
Para los desarrolladores, algunos de los códigos de muestra de la aplicación OpenSL ES deben actualizarse para utilizar algoritmos sin bloqueo o hacer referencia a una biblioteca de código abierto que no sea de Android.
Hemos publicado un ejemplo de implementación FIFO sin bloqueo que está diseñada específicamente para código de aplicación. Vea estos archivos ubicados en el directorio fuente de la plataforma frameworks/av/audio_utils
:
Herramientas
Hasta donde sabemos, no existen herramientas automáticas para encontrar la inversión de prioridad, especialmente antes de que suceda. Algunas herramientas de investigación de análisis de código estático son capaces de encontrar inversiones de prioridad si pueden acceder a todo el código base. Por supuesto, si se trata de un código de usuario arbitrario (como ocurre aquí para la aplicación) o se trata de 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 resultar poco práctico. Lo más importante es leer el código con mucha atención y comprender bien todo el sistema y las interacciones. Herramientas como systrace y ps -t -p
son útiles para ver la inversión de prioridad después de que ocurre, pero no se lo informan con anticipación.
Una última palabra
Después de toda esta discusión, no tenga miedo de los mutex. Los mutex son tus amigos para el uso normal, cuando se usan e implementan correctamente en casos de uso normales que no son críticos en el tiempo. Pero entre tareas de alta y baja prioridad y en sistemas urgentes, es más probable que los mutex causen problemas.