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

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

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

Фон

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

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

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

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

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

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

В реализации Android-аудио в этих местах скорее всего произойдет инверсия приоритетов. И поэтому вам следует сосредоточить свое внимание здесь:

  • между обычным потоком микшера и потоком быстрого микшера в AudioFlinger
  • между потоком обратного вызова приложения для быстрого AudioTrack и потоком быстрого микшера (оба имеют повышенный приоритет, но немного разные приоритеты)
  • между потоком обратного вызова приложения для быстрой аудиозаписи и потоком быстрого захвата (аналогично предыдущему)
  • в реализации уровня аппаратной абстракции звука (HAL), например, для телефонии или эхоподавления
  • внутри аудиодрайвера в ядре
  • между потоком обратного вызова AudioTrack или AudioRecord и другими потоками приложения (это находится вне нашего контроля)

Общие решения

Типичные решения включают в себя:

  • отключение прерываний
  • мьютексы приоритетного наследования

Отключение прерываний невозможно в пользовательском пространстве Linux и не работает для симметричных многопроцессоров (SMP).

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

Методы, используемые Android

Эксперименты начались с «попробовать блокировку» и блокировки с таймаутом. Это неблокирующий и ограниченно блокирующий варианты операции блокировки мьютекса. Пробная блокировка и блокировка с тайм-аутом работали довольно хорошо, но были подвержены нескольким неясным режимам сбоя: не гарантировалось, что сервер сможет получить доступ к общему состоянию, если клиент был занят, а совокупный тайм-аут мог быть слишком длинным, если была длинная последовательность несвязанных блокировок, время ожидания которых истекло.

Мы также используем атомарные операции , такие как:

  • приращение
  • побитовое «или»
  • побитовое «и»

Все они возвращают предыдущее значение и включают необходимые барьеры SMP. Недостатком является то, что они могут потребовать неограниченного количества повторных попыток. На практике мы обнаружили, что повторные попытки не являются проблемой.

Примечание. Атомарные операции и их взаимодействие с барьерами памяти, как известно, неправильно понимаются и используются. Мы включили эти методы сюда для полноты картины, но рекомендуем вам также прочитать статью SMP Primer для Android для получения дополнительной информации.

У нас до сих пор есть и мы используем большинство из вышеперечисленных инструментов, а недавно добавили следующие методы:

  • Используйте неблокирующие очереди FIFO с одним считывателем и одной записью для данных.
  • Попробуйте копировать состояние, а не разделять его между модулями с высоким и низким приоритетом.
  • Если состояние необходимо совместно использовать, ограничьте состояние словом максимального размера, к которому можно получить доступ атомарно в операции с одной шиной без повторных попыток.
  • Для сложного состояния из нескольких слов используйте очередь состояний. Очередь состояний — это, по сути, просто неблокирующая очередь FIFO с одним считывателем и одной записью, используемая для состояния, а не для данных, за исключением того, что записывающее устройство сжимает соседние запросы в один запрос.
  • Обратите внимание на барьеры памяти для корректности SMP.
  • Доверяй, но проверяй . При совместном использовании состояния между процессами не думайте, что состояние правильно сформировано. Например, проверьте, находятся ли индексы в пределах допустимых значений. Эта проверка не требуется между потоками одного и того же процесса, между процессами взаимного доверия (которые обычно имеют один и тот же UID). Это также необязательно для общих данных , таких как аудио PCM, где повреждение несущественно.

Неблокирующие алгоритмы

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

Начиная с Android 4.2, вы можете найти наши неблокирующие классы с одним чтением/записью в следующих местах:

  • рамки/av/include/media/nbaio/
  • рамки/av/медиа/libnbaio/
  • frameworks/av/services/audioflinger/StateQueue*

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

Разработчикам следует обновить часть примера кода приложения OpenSL ES, чтобы использовать неблокирующие алгоритмы или ссылаться на библиотеку с открытым исходным кодом, не относящуюся к Android.

Мы опубликовали пример реализации неблокирующего FIFO, специально разработанного для кода приложения. См. эти файлы, расположенные в исходном каталоге платформы frameworks/av/audio_utils :

Инструменты

Насколько нам известно, не существует автоматических инструментов для обнаружения инверсии приоритетов, особенно до того, как она произойдет. Некоторые исследовательские инструменты статического анализа кода способны находить инверсии приоритетов, если имеют доступ ко всей базе кода. Конечно, если задействован произвольный пользовательский код (как в данном случае для приложения) или большая база кода (как в случае ядра Linux и драйверов устройств), статический анализ может оказаться непрактичным. Самое главное — очень внимательно прочитать код и хорошо понять всю систему и ее взаимодействия. Такие инструменты, как systrace и ps -t -p полезны для обнаружения инверсии приоритета после того, как она произошла, но не сообщают об этом заранее.

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

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