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, los OEM y los proveedores de SoC que implementan un HAL de audio. Ten en cuenta que la implementación de estas técnicas no garantiza que se eviten fallas o errores, en especial si se usan fuera del contexto de audio. Tus resultados pueden variar, y 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 AudioTrack/AudioRecord para reducir la latencia. Este trabajo comenzó en Android 4.1 y continuó con más mejoras en las versiones 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 críticos para el tiempo con una política de programación más predecible. La programación confiable permite reducir los tamaños y los recuentos de búferes de audio y, al mismo tiempo, evitar los subdesbordamientos 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, en el que 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 un mutex (estado compartido protegido por).
En un sistema de audio, la inversión de prioridad suele manifestarse como un error (clic, estallido, interrupción), audio repetido cuando se usan búferes circulares o un retraso en la respuesta a un comando.
Una solución alternativa común para la inversión de prioridad es aumentar los tamaños del búfer 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 ocurra en estos lugares. Por lo tanto, debes concentrar 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 el subproceso del 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
Las soluciones típicas incluyen lo siguiente:
- Inhabilita las interrupciones
- Mutex de herencia de prioridad
Inhabilitar las interrupciones no es factible en el espacio del usuario de Linux y no funciona para los multiprocesadores simétricos (SMP).
La herencia de prioridad de los futexes (mutexes rápidos del espacio del usuario) no se usa en el sistema de audio porque son relativamente pesados y porque dependen de un cliente de confianza.
Técnicas que usa Android
Se iniciaron experimentos con "try lock" y bloqueo con tiempo de espera. Estas son variantes de bloqueo limitado y no bloqueo de la operación de bloqueo de mutex. El bloqueo con tiempo de espera funcionó bastante bien, pero era susceptible a algunos modos de falla poco conocidos: no se garantizaba que el servidor pudiera acceder al estado compartido si el cliente estaba ocupado, y el tiempo de espera acumulativo podía ser demasiado largo si había una secuencia larga de bloqueos no relacionados que se agotaron.
También usamos operaciones atómicas, como las siguientes:
- incremento
- "or" a nivel de bits
- "and" a nivel de bits
Todos estos métodos devuelven el valor anterior y contienen las barreras de SMP necesarias. La desventaja es que pueden requerir reintentos ilimitados. En la práctica, descubrimos que los reintentos no son un problema.
Nota: Las operaciones atómicas y sus interacciones con las barreras de memoria son notoriamente mal comprendidas y se usan de forma incorrecta. Incluimos estos métodos aquí para que la información esté completa, 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 agregamos estas técnicas:
- Usa colas FIFO de un solo lector y un solo escritor que no se bloqueen para los datos.
- Intenta copiar el estado en lugar de compartirlo entre los módulos de prioridad alta y baja.
- Cuando sea necesario compartir el estado, limítalo a la palabra de tamaño máximo a la que se puede acceder de forma atómica en una operación de un solo bus sin reintentos.
- Para estados complejos de varias palabras, usa una cola de estados. Una cola de estado es básicamente una cola FIFO de un solo escritor y un solo lector que no se bloquea y que se usa para el estado en lugar de los datos, excepto que el escritor contrae las inserciones adyacentes en una sola inserción.
- Presta atención a las barreras de memoria para garantizar la corrección del SMP.
- Confía, pero verifica. Cuando compartas el estado entre procesos, no supongas que el estado está bien formado. Por ejemplo, verifica que los índices estén dentro de los límites. Esta verificación no es necesaria entre subprocesos del mismo proceso, ni entre procesos que se confían mutuamente (que suelen tener el mismo UID). Tampoco es necesario para los datos compartidos, como el audio PCM, en los que una corrupción es intrascendente.
Algoritmos sin bloqueo
Los algoritmos sin bloqueo han sido objeto de muchos estudios recientes. Sin embargo, con la excepción de las colas FIFO de un solo lector y un solo escritor, las hemos encontrado complejas y propensas a errores.
A partir de Android 4.2, puedes encontrar nuestras clases de un solo lector/escritor y no bloqueantes en las siguientes 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 sin bloqueo son famosos 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 sin bloqueo 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 detectar la inversión de prioridad, especialmente antes de que ocurra. Algunas herramientas de análisis de código estático de investigación pueden encontrar inversiones de prioridad si pueden acceder a toda la base de código. Por supuesto, si se incluye código de usuario arbitrario (como en este caso para la aplicación) o si 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 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 indican con anticipación.
Una última palabra
Después de todo este debate, no tengas miedo de los mutex. Los mutex son útiles para el uso ordinario, cuando se usan y se implementan correctamente en casos de uso ordinarios que no son críticos en el tiempo. 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.