Este artículo explica cómo el sistema de audio de Android intenta evitar la inversión de prioridad y destaca las técnicas que también puede usar.
Estas técnicas pueden ser útiles para los desarrolladores de aplicaciones de audio de alto rendimiento, OEM y proveedores de SoC que implementan una 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 usa fuera del contexto del audio. Sus resultados pueden variar, y debe realizar su propia evaluación y prueba.
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 latencia más baja, se necesitaron muchos cambios en todo el sistema. Un cambio importante es asignar recursos de CPU a subprocesos de tiempo crítico con una política de programación más predecible. La programación confiable permite que se reduzcan los tamaños y los conteos del búfer de audio y, al mismo tiempo, se evitan los agotamientos y los 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 a la espera de 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 una falla (clic, estallido, abandono), audio repetido cuando se usan búferes circulares o demora en responder a un comando.
Una solución 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 entender 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 ocurra una inversión de prioridad en estos lugares. Y por lo que debe centrar su atención aquí:
- entre el hilo del mezclador normal y el hilo del mezclador rápido en AudioFlinger
- entre el subproceso de devolución de llamada de la aplicación para un AudioTrack rápido y el subproceso 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ápido (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 subproceso de devolución de llamada de AudioTrack o AudioRecord y otros subprocesos de la aplicación (esto está fuera de nuestro control)
Soluciones comunes
Las soluciones típicas incluyen:
- deshabilitar interrupciones
- mutex de herencia de prioridad
Deshabilitar las interrupciones no es factible en el espacio de usuario de Linux y no funciona para los multiprocesadores simétricos (SMP).
Los futex de herencia de prioridad (mutex de espacio de usuario rápido) no se utilizan en el sistema de audio porque son relativamente pesados y porque dependen de un cliente de confianza.
Técnicas utilizadas por Android
Los experimentos comenzaron con "intentar bloquear" y bloquear con tiempo de espera. Estas son variantes de bloqueo limitado y sin bloqueo 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 que expiraron.
También usamos 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 encontrado que los reintentos no son un problema.
Nota: Las operaciones atómicas y sus interacciones con las barreras de la memoria se malinterpretan y se usan incorrectamente. Incluimos estos métodos aquí para completar, pero le recomendamos que también lea el artículo SMP Primer para Android para obtener más información.
Todavía tenemos y usamos la mayoría de las herramientas anteriores, y recientemente hemos agregado estas técnicas:
- Use colas FIFO de un solo lector y un solo escritor sin bloqueo para los datos.
- Intente copiar el estado en lugar de compartir el estado entre los módulos de alta y baja prioridad.
- Cuando sea necesario compartir el estado, limite el estado a la palabra de tamaño máximo a la que se puede acceder atómicamente en una operación de un solo bus sin reintentos.
- Para un estado complejo de varias palabras, use una cola de estado. Una cola de estado es básicamente una cola FIFO de un solo lector y un solo escritor que no bloquea y se utiliza para el estado en lugar de para los datos, excepto que el escritor colapsa las inserciones adyacentes en una sola inserción.
- Preste atención a las barreras de memoria para la corrección de SMP.
- Confía, pero verifica . Al compartir el estado entre procesos, no asuma que el estado está bien formado. Por ejemplo, verifique que los índices estén dentro de los límites. Esta verificación no es necesaria entre subprocesos en el mismo proceso, entre procesos de confianza mutua (que normalmente tienen el mismo UID). También es innecesario para datos compartidos, como audio PCM, donde una corrupción es intrascendente.
Algoritmos sin bloqueo
Los algoritmos de no 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 encontrado que son complejas y propensas a errores.
A partir de Android 4.2, puede encontrar nuestras clases de lector/escritor único sin bloqueo en estas ubicaciones:
- frameworks/av/include/media/nbaio/
- frameworks/av/media/libnbaio/
- frameworks/av/services/audioflinger/StateQueue*
Estos fueron diseñados específicamente para AudioFlinger y no son de propósito general. Los algoritmos sin bloqueo son conocidos por ser difíciles de depurar. Puedes ver este código como un modelo. Pero tenga en cuenta que puede haber errores, y no se garantiza que las clases sean adecuadas para otros fines.
Para los desarrolladores, parte del código de la aplicación OpenSL ES de muestra debe actualizarse para usar 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ñado específicamente para el código de la aplicación. Consulte estos archivos ubicados en el directorio de origen de la plataforma frameworks/av/audio_utils
:
Instrumentos
Hasta donde sabemos, no existen herramientas automáticas para encontrar la inversión de prioridad, especialmente antes de que suceda. Algunas herramientas de análisis de código estático de investigación son capaces de encontrar inversiones prioritarias si pueden acceder a la base de código completa. Por supuesto, si se trata de un código de usuario arbitrario (como lo es aquí para la aplicación) o si se trata de una gran base de código (como para el 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 mucho cuidado 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 ocurra, pero no se lo digan por adelantado.
una última palabra
Después de toda esta discusión, no tenga miedo de mutexes. Mutexes es su amigo para el uso ordinario, cuando se usa e implementa correctamente en casos de uso ordinarios que no son críticos en cuanto al tiempo. Pero entre tareas de alta y baja prioridad y en sistemas sensibles al tiempo, es más probable que los mutex causen problemas.