В этой статье объясняется, как аудиосистема Android пытается избежать инверсии приоритетов, а также освещаются методы, которые вы также можете использовать.
Эти методы могут быть полезны разработчикам высокопроизводительных аудиоприложений, OEM-производителям и поставщикам SoC, которые внедряют аудио HAL. Обратите внимание, что внедрение этих методов не гарантирует предотвращения сбоев или других сбоев, особенно если они используются вне аудиоконтекста. Ваши результаты могут отличаться, и вам следует провести собственную оценку и тестирование.
Фон
Реализация аудиосервера Android AudioFlinger и клиента AudioTrack/AudioRecord перестраивается для уменьшения задержки. Эта работа началась в Android 4.1 и продолжилась дальнейшими улучшениями в 4.2, 4.3, 4.4 и 5.0.
Для достижения этой меньшей задержки потребовалось много изменений во всей системе. Одним из важных изменений является назначение ресурсов ЦП критичным по времени потокам с более предсказуемой политикой планирования. Надежное планирование позволяет уменьшить размеры и количество аудиобуферов, при этом избегая недогрузки и перегрузки.
Инверсия приоритетов
Инверсия приоритетов — классический режим отказа систем реального времени, при котором задача с более высоким приоритетом блокируется на неограниченное время, ожидая, пока задача с более низким приоритетом освободит ресурс, например (общее состояние, защищенное) мьютексом .
В аудиосистеме инверсия приоритета обычно проявляется в виде сбоев (щелчков, тресков, пропадания звука), повторяющегося звука при использовании кольцевых буферов или задержки ответа на команду.
Распространенным обходным путем для инверсии приоритетов является увеличение размера аудиобуфера. Однако этот метод увеличивает задержку и просто скрывает проблему, а не решает ее. Лучше понимать и предотвращать инверсию приоритетов, как показано ниже.
В реализации Android audio инверсия приоритетов, скорее всего, произойдет в этих местах. И поэтому вам следует сосредоточить свое внимание здесь:
- между обычным потоком микшера и быстрым потоком микшера в AudioFlinger
- между потоком обратного вызова приложения для быстрого AudioTrack и потоком быстрого микшера (они оба имеют повышенный приоритет, но немного разные приоритеты)
- между потоком обратного вызова приложения для быстрой AudioRecord и быстрым потоком захвата (аналогично предыдущему)
- в рамках реализации уровня абстракции оборудования (HAL) для аудио, например, для телефонии или эхоподавления
- в звуковом драйвере ядра
- между потоком обратного вызова AudioTrack или AudioRecord и другими потоками приложения (это вне нашего контроля)
Общие решения
Типичные решения включают в себя:
- отключение прерываний
- приоритетные мьютексы наследования
Отключение прерываний невозможно в пользовательском пространстве Linux и не работает для симметричных мультипроцессоров (SMP).
Фьютексы с приоритетным наследованием (быстрые мьютексы пользовательского пространства) не используются в аудиосистеме, поскольку они относительно тяжеловесны и полагаются на доверенного клиента.
Методы, используемые Android
Эксперименты начались с "try lock" и lock with timeout. Это неблокирующие и ограниченные блокирующие варианты операции блокировки мьютекса. Try lock и lock with timeout работали довольно хорошо, но были подвержены паре неясных режимов отказа: сервер не мог гарантированно получить доступ к общему состоянию, если клиент был занят, а кумулятивный тайм-аут мог быть слишком долгим, если была длинная последовательность несвязанных блокировок, все из которых истекли по тайм-ауту.
Мы также используем атомарные операции , такие как:
- приращение
- побитовое "или"
- побитовое "и"
Все они возвращают предыдущее значение и включают необходимые барьеры SMP. Недостатком является то, что они могут потребовать неограниченных повторных попыток. На практике мы обнаружили, что повторные попытки не являются проблемой.
Примечание: Атомарные операции и их взаимодействие с барьерами памяти, как известно, плохо понимаются и используются неправильно. Мы включили эти методы сюда для полноты, но рекомендуем вам также прочитать статью SMP Primer for Android для получения дополнительной информации.
Мы по-прежнему имеем и используем большинство из вышеперечисленных инструментов, а недавно добавили следующие методы:
- Используйте неблокируемые очереди FIFO с одним читателем и одной записью для данных.
- Попробуйте скопировать состояние, а не делиться им между модулями с высоким и низким приоритетом.
- Если состояние необходимо совместно использовать, ограничьте его максимальным размером слова , к которому можно получить атомарный доступ при работе с одной шиной без повторных попыток.
- Для сложного многословного состояния используйте очередь состояний. Очередь состояний — это по сути просто неблокируемая одночитающая однопишущая очередь FIFO, используемая для состояния, а не для данных, за исключением того, что писатель сворачивает смежные push-уведомления в один push-уведомления.
- Обратите внимание на барьеры памяти для корректности SMP.
- Доверяй, но проверяй . При совместном использовании состояния между процессами не предполагайте, что состояние правильно сформировано. Например, проверьте, что индексы находятся в пределах границ. Эта проверка не нужна между потоками в одном процессе, между взаимно доверяющими процессами (которые обычно имеют одинаковый UID). Она также не нужна для общих данных , таких как аудио PCM, где повреждение несущественно.
Неблокирующие алгоритмы
Неблокирующие алгоритмы были предметом многих недавних исследований. Но за исключением очередей FIFO с одним читателем и одной записью, мы обнаружили, что они сложны и подвержены ошибкам.
Начиная с Android 4.2, вы можете найти наши неблокирующие классы с одним читателем/писателем в следующих местах:
- рамки/av/include/media/nbaio/
- фреймворки/av/media/libnbaio/
- frameworks/av/services/audioflinger/StateQueue*
Они были разработаны специально для AudioFlinger и не являются универсальными. Неблокирующие алгоритмы печально известны своей сложностью отладки. Вы можете рассматривать этот код как модель. Но имейте в виду, что могут быть ошибки, и классы не гарантированно подходят для других целей.
Разработчикам следует обновить некоторые примеры кода приложений OpenSL ES, чтобы использовать неблокирующие алгоритмы или ссылаться на библиотеку с открытым исходным кодом, отличную от Android.
Мы опубликовали пример неблокирующей реализации FIFO, специально разработанной для кода приложения. Смотрите эти файлы, расположенные в исходном каталоге платформы frameworks/av/audio_utils
:
Инструменты
Насколько нам известно, не существует автоматических инструментов для поиска инверсии приоритетов, особенно до того, как она произойдет. Некоторые инструменты статического анализа кода для исследований способны находить инверсии приоритетов, если имеют доступ ко всей кодовой базе. Конечно, если задействован произвольный пользовательский код (как в случае с приложением) или большая кодовая база (как в случае с ядром Linux и драйверами устройств), статический анализ может быть непрактичным. Самое главное — очень внимательно прочитать код и хорошо понять всю систему и ее взаимодействия. Такие инструменты, как systrace и ps -t -p
полезны для обнаружения инверсии приоритетов после ее возникновения, но не сообщают вам об этом заранее.
Последнее слово
После всего этого обсуждения, не бойтесь мьютексов. Мьютексы — ваши друзья для обычного использования, если они используются и реализованы правильно в обычных некритичных по времени случаях использования. Но между высоко- и низкоприоритетными задачами и в системах, чувствительных ко времени, мьютексы с большей вероятностью могут вызвать проблемы.