Избегайте инверсии приоритетов

В этой статье объясняется, как аудиосистема Android пытается избежать инверсии приоритетов, а также описываются методы, которые вы также можете использовать.

Эти методы могут быть полезны разработчикам высокопроизводительных аудиоприложений, производителям оригинального оборудования (OEM) и поставщикам SoC, реализующим аудио HAL. Обратите внимание, что реализация этих методов не гарантирует предотвращения сбоев и других сбоев, особенно при использовании вне аудиоконтекста. Ваши результаты могут отличаться, поэтому вам следует провести собственную оценку и тестирование.

Фон

Реализация аудиосервера Android AudioFlinger и клиента AudioTrack/AudioRecord в настоящее время перерабатывается для уменьшения задержек. Эта работа началась в Android 4.1 и продолжилась дальнейшими улучшениями в версиях 4.2, 4.3, 4.4 и 5.0.

Для достижения такой низкой задержки потребовалось внести множество изменений в систему. Одним из важных изменений стало назначение ресурсов ЦП потокам, критически важным по времени, с более предсказуемой политикой планирования. Надёжное планирование позволяет уменьшить размеры и количество аудиобуферов, одновременно избегая недогрузки и перегрузки.

Инверсия приоритетов

Инверсия приоритетов — классический режим сбоя систем реального времени, при котором задача с более высоким приоритетом блокируется на неограниченное время, ожидая, пока задача с более низким приоритетом освободит ресурс, например (общее состояние, защищенное) мьютексом .

В аудиосистеме инверсия приоритетов обычно проявляется в виде сбоев (щелчков, тресков, пропадания звука), повторного звука при использовании кольцевых буферов или задержки в ответе на команду.

Распространенным решением проблемы инверсии приоритетов является увеличение размера аудиобуфера. Однако этот метод увеличивает задержку и лишь скрывает проблему, а не решает её. Лучше понимать механизм инверсии приоритетов и предотвращать её, как показано ниже.

В реализации звука в Android инверсия приоритетов чаще всего происходит в этих местах. Поэтому вам следует обратить внимание на следующее:

  • между обычным потоком микшера и быстрым потоком микшера в 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-очередь с одним читателем и одной записью, используемая для состояния, а не для данных, за исключением того, что запись сворачивает смежные запросы в один.
  • Обратите внимание на барьеры памяти для корректности 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 полезны для обнаружения инверсии приоритетов после её возникновения, но не сообщают о ней заранее.

Последнее слово

После всего этого обсуждения, не бойтесь мьютексов. Мьютексы — ваши лучшие друзья в обычной работе, если они правильно используются и реализованы в обычных сценариях, не критичных ко времени выполнения. Но между задачами с высоким и низким приоритетом, а также в системах, чувствительных ко времени выполнения, мьютексы чаще создают проблемы.