Unikaj odwracania priorytetów

W tym artykule wyjaśniono, w jaki sposób system audio systemu Android próbuje uniknąć odwrócenia priorytetów, a także przedstawiono techniki, z których można skorzystać.

Techniki te mogą być przydatne dla twórców wysokowydajnych aplikacji audio, producentów OEM i dostawców SoC, którzy wdrażają audio HAL. Należy pamiętać, że wdrożenie tych technik nie gwarantuje zapobiegania błędom lub innym awariom, szczególnie jeśli są używane poza kontekstem audio. Twoje wyniki mogą się różnić i powinieneś przeprowadzić własną ocenę i testy.

Tło

Serwer audio Android AudioFlinger i implementacja klienta AudioTrack/AudioRecord są przebudowywane w celu zmniejszenia opóźnień. Prace te rozpoczęły się w systemie Android 4.1 i były kontynuowane w wersjach 4.2, 4.3, 4.4 i 5.0.

Aby osiągnąć to mniejsze opóźnienie, konieczne było wiele zmian w całym systemie. Jedną z ważnych zmian jest przypisanie zasobów procesora do wątków krytycznych czasowo za pomocą bardziej przewidywalnych zasad planowania. Niezawodne planowanie pozwala zmniejszyć rozmiary i liczbę buforów audio, jednocześnie unikając niedopełnień i przekroczeń.

Odwrócenie priorytetów

Odwrócenie priorytetów to klasyczny tryb awarii systemów czasu rzeczywistego, w którym zadanie o wyższym priorytecie jest blokowane przez nieograniczony czas w oczekiwaniu na zadanie o niższym priorytecie, które zwolni zasób, taki jak (stan współdzielony chroniony przez) mutex .

W systemie audio odwrócenie priorytetu zwykle objawia się usterką (kliknięcie, trzask, zanik), powtarzaniem dźwięku w przypadku użycia buforów kołowych lub opóźnieniem w odpowiedzi na polecenie.

Typowym obejściem inwersji priorytetów jest zwiększenie rozmiaru bufora audio. Jednak ta metoda zwiększa opóźnienia i jedynie ukrywa problem, zamiast go rozwiązać. Lepiej zrozumieć i zapobiec odwróceniu priorytetów, jak pokazano poniżej.

W implementacji audio Androida inwersja priorytetów najprawdopodobniej wystąpi w tych miejscach. Dlatego powinieneś skupić swoją uwagę tutaj:

  • pomiędzy normalnym wątkiem miksera a wątkiem szybkiego miksera w AudioFlinger
  • pomiędzy wątkiem wywołania zwrotnego aplikacji dla szybkiego AudioTrack i szybkiego wątku miksera (oba mają podwyższony priorytet, ale nieco inne priorytety)
  • pomiędzy wątkiem wywołania zwrotnego aplikacji dla szybkiego AudioRecord i wątkiem szybkiego przechwytywania (podobnie jak poprzednio)
  • w ramach implementacji warstwy abstrakcji sprzętu audio (HAL), np. do telefonii lub eliminacji echa
  • w sterowniku audio w jądrze
  • pomiędzy wątkiem wywołania zwrotnego AudioTrack lub AudioRecord a innymi wątkami aplikacji (jest to poza naszą kontrolą)

Wspólne rozwiązania

Typowe rozwiązania obejmują:

  • wyłączenie przerwań
  • muteksy dziedziczenia priorytetów

Wyłączenie przerwań nie jest możliwe w przestrzeni użytkownika systemu Linux i nie działa w przypadku symetrycznych procesorów wieloprocesorowych (SMP).

Futeksy dziedziczenia priorytetów (szybkie muteksy przestrzeni użytkownika) nie są używane w systemie audio, ponieważ są stosunkowo ciężkie i ponieważ polegają na zaufanym kliencie.

Techniki stosowane przez Androida

Eksperymenty rozpoczęły się od „spróbuj zablokować” i zablokuj po przekroczeniu limitu czasu. Są to nieblokujące i ograniczone warianty blokowania operacji blokady mutex. Próby blokowania i blokowania z limitem czasu działały całkiem dobrze, ale były podatne na kilka niejasnych trybów awarii: serwer nie miał gwarancji, że będzie mógł uzyskać dostęp do stanu udostępnionego, jeśli klient był zajęty, a skumulowany limit czasu mógł być zbyt długi, jeśli istniała długa sekwencja niepowiązanych ze sobą blokad, dla których upłynął limit czasu.

Używamy również operacji atomowych, takich jak:

  • przyrost
  • bitowe „lub”
  • bitowe „i”

Wszystkie zwracają poprzednią wartość i zawierają niezbędne bariery SMP. Wadą jest to, że mogą wymagać nieograniczonej liczby ponownych prób. W praktyce odkryliśmy, że ponowne próby nie stanowią problemu.

Uwaga: operacje atomowe i ich interakcje z barierami pamięci są notorycznie źle rozumiane i wykorzystywane nieprawidłowo. Aby zapewnić kompletność, uwzględniliśmy tutaj te metody, ale zalecamy przeczytanie również artykułu SMP Primer dla Androida w celu uzyskania dalszych informacji.

Nadal mamy i używamy większości powyższych narzędzi, a ostatnio dodaliśmy następujące techniki:

  • Używaj nieblokujących kolejek FIFO z jednym czytnikiem i pojedynczym zapisem dla danych.
  • Spróbuj skopiować stan, zamiast udostępniać stan pomiędzy modułami o wysokim i niskim priorytecie.
  • Kiedy stan musi być współużytkowany, ogranicz stan do słowa o maksymalnym rozmiarze, do którego można uzyskać dostęp atomowo w operacji na jednej magistrali bez ponownych prób.
  • W przypadku złożonego stanu składającego się z wielu słów użyj kolejki stanu. Kolejka stanu to w zasadzie po prostu nieblokująca kolejka FIFO z jednym czytnikiem i pojedynczym zapisem, używana do przechowywania stanu, a nie danych, z tą różnicą, że moduł piszący zwija sąsiednie wypchnięcia w pojedyncze wypchnięcie.
  • Zwróć uwagę na bariery pamięciowe dla poprawności SMP.
  • Ufaj ale sprawdzaj . Dzieląc stan pomiędzy procesami, nie zakładaj, że stan jest dobrze uformowany. Sprawdź na przykład, czy indeksy mieszczą się w określonych granicach. Ta weryfikacja nie jest wymagana między wątkami w tym samym procesie, między procesami wzajemnie ufającymi (które zazwyczaj mają ten sam UID). Nie jest to również konieczne w przypadku współdzielonych danych , takich jak dźwięk PCM, gdzie uszkodzenie jest nieistotne.

Algorytmy nieblokujące

Algorytmy nieblokujące były przedmiotem wielu niedawnych badań. Jednak z wyjątkiem kolejek FIFO z jednym czytnikiem i jednym zapisem, odkryliśmy, że są one złożone i podatne na błędy.

Począwszy od wersji Androida 4.2, nasze nieblokujące się klasy z jednym czytnikiem/zapisem znajdziesz w następujących lokalizacjach:

  • frameworks/av/include/media/nbaio/
  • frameworks/av/media/libnbaio/
  • frameworks/av/services/audioflinger/StateQueue*

Zostały one zaprojektowane specjalnie dla AudioFlinger i nie są przeznaczone do użytku ogólnego. Algorytmy nieblokujące są znane z tego, że są trudne do debugowania. Możesz spojrzeć na ten kod jako model. Należy jednak pamiętać, że mogą występować błędy i nie ma gwarancji, że klasy będą odpowiednie do innych celów.

W przypadku programistów część przykładowego kodu aplikacji OpenSL ES powinna zostać zaktualizowana, aby korzystała z algorytmów nieblokujących lub odwoływała się do biblioteki open source innej niż Android.

Opublikowaliśmy przykładową nieblokującą implementację FIFO zaprojektowaną specjalnie dla kodu aplikacji. Zobacz te pliki znajdujące się w katalogu źródeł platformy frameworks/av/audio_utils :

Narzędzia

O ile nam wiadomo, nie ma automatycznych narzędzi do wyszukiwania odwrócenia priorytetów, zwłaszcza zanim to nastąpi. Niektóre narzędzia badawcze do analizy kodu statycznego są w stanie znaleźć inwersje priorytetów, jeśli mają dostęp do całej bazy kodu. Oczywiście, jeśli w grę wchodzi dowolny kod użytkownika (jak w przypadku aplikacji) lub jest duża baza kodu (jak w przypadku jądra Linuksa i sterowników urządzeń), analiza statyczna może być niepraktyczna. Najważniejszą rzeczą jest bardzo uważne przeczytanie kodu i dobre zrozumienie całego systemu i interakcji. Narzędzia takie jak systrace i ps -t -p są przydatne do sprawdzania inwersji priorytetów po jej wystąpieniu, ale nie informują o tym z wyprzedzeniem.

Ostatnie słowo

Po całej tej dyskusji nie bój się muteksów. Muteksy są Twoim przyjacielem w zwykłym użyciu, jeśli są prawidłowo używane i zaimplementowane w zwykłych, niekrytycznych czasowo przypadkach użycia. Jednak pomiędzy zadaniami o wysokim i niskim priorytecie oraz w systemach wrażliwych na czas muteksy częściej powodują problemy.