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

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

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

Фон

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • увеличение
  • побитовое "ИЛИ"
  • побитовое "И"

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

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

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

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

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

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

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

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

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

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

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

Инструменты

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

В заключение несколько слов.

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