Aktualizacje systemu A/B (płynne).

Aktualizacje systemu A/B, zwane także aktualizacjami płynnymi, zapewniają, że działający system rozruchowy pozostanie na dysku podczas aktualizacji OTA . Takie podejście zmniejsza prawdopodobieństwo, że urządzenie będzie nieaktywne po aktualizacji, co oznacza mniejszą liczbę wymian i ponownych instalacji urządzeń w centrach naprawczych i gwarancyjnych. Inne komercyjne systemy operacyjne, takie jak ChromeOS, również z powodzeniem korzystają z aktualizacji A/B.

Aby uzyskać więcej informacji na temat aktualizacji systemu A/B i sposobu ich działania, zobacz Wybór partycji (gniazda) .

Aktualizacje systemu A/B zapewniają następujące korzyści:

  • Aktualizacje OTA mogą odbywać się podczas działania systemu, bez zakłócania pracy użytkownika. Użytkownicy mogą nadal korzystać ze swoich urządzeń podczas OTA — jedyny przestój podczas aktualizacji występuje, gdy urządzenie uruchamia się ponownie na zaktualizowanej partycji dysku.
  • Po aktualizacji ponowne uruchomienie nie trwa dłużej niż zwykłe ponowne uruchomienie.
  • Jeśli OTA nie zostanie zastosowana (na przykład z powodu złego flashowania), nie będzie to miało wpływu na użytkownika. Użytkownik będzie nadal korzystał ze starego systemu operacyjnego, a klient może ponownie podjąć próbę aktualizacji.
  • Jeśli aktualizacja OTA zostanie zastosowana, ale nie uda się uruchomić urządzenia, urządzenie uruchomi się ponownie na starej partycji i będzie nadal działać. Klient może ponowić próbę aktualizacji.
  • Wszelkie błędy (takie jak błędy we/wy) dotyczą tylko nieużywanego zestawu partycji i można je ponowić. Takie błędy stają się również mniej prawdopodobne, ponieważ obciążenie we/wy jest celowo niskie, aby uniknąć pogorszenia komfortu użytkowania.
  • Aktualizacje można przesyłać strumieniowo do urządzeń A/B, eliminując potrzebę pobierania pakietu przed jego instalacją. Przesyłanie strumieniowe oznacza, że ​​użytkownik nie musi mieć wystarczającej ilości wolnego miejsca do przechowywania pakietu aktualizacji w /data lub /cache .
  • Partycja pamięci podręcznej nie jest już używana do przechowywania pakietów aktualizacji OTA, więc nie ma potrzeby upewniania się, że partycja pamięci podręcznej jest wystarczająco duża dla przyszłych aktualizacji.
  • dm-verity gwarantuje, że urządzenie uruchomi się z nieuszkodzonym obrazem. Jeśli urządzenie nie uruchamia się ze względu na zły problem z OTA lub dm-verity, urządzenie może uruchomić się ponownie ze starym obrazem. ( Rozruch zweryfikowany systemu Android nie wymaga aktualizacji A/B.)

Informacje o aktualizacjach systemu A/B

Aktualizacje A/B wymagają zmian zarówno w kliencie, jak i w systemie. Serwer pakietów OTA nie powinien jednak wymagać zmian: pakiety aktualizacji są nadal obsługiwane przez HTTPS. W przypadku urządzeń korzystających z infrastruktury OTA Google wszystkie zmiany systemowe odbywają się w AOSP, a kod klienta jest dostarczany przez usługi Google Play. Producenci OEM nie korzystający z infrastruktury OTA Google będą mogli ponownie wykorzystać kod systemowy AOSP, ale będą musieli dostarczyć go własnemu klientowi.

W przypadku producentów OEM zaopatrujących własnego klienta, klient musi:

  • Zdecyduj, kiedy przeprowadzić aktualizację. Ponieważ aktualizacje A/B odbywają się w tle, nie są już inicjowane przez użytkownika. Aby uniknąć zakłócania pracy użytkowników, zaleca się planowanie aktualizacji, gdy urządzenie znajduje się w trybie konserwacji bezczynności, na przykład w nocy, i gdy jest połączone z siecią Wi-Fi. Jednak Twój klient może zastosować dowolną heurystykę.
  • Skontaktuj się z serwerami pakietów OTA i sprawdź, czy dostępna jest aktualizacja. Powinien być w większości taki sam, jak istniejący kod klienta, z tą różnicą, że będziesz chciał zasygnalizować, że urządzenie obsługuje A/B. (Klient Google zawiera także przycisk Sprawdź teraz , dzięki któremu użytkownicy mogą sprawdzić dostępność najnowszej aktualizacji).
  • Wywołaj update_engine z adresem URL HTTPS pakietu aktualizacji, zakładając, że jest on dostępny. update_engine zaktualizuje surowe bloki na aktualnie nieużywanej partycji podczas przesyłania strumieniowego pakietu aktualizacji.
  • Zgłaszaj sukcesy lub niepowodzenia instalacji na swoich serwerach na podstawie kodu wynikowego update_engine . Jeśli aktualizacja zostanie pomyślnie zastosowana, update_engine poinformuje program ładujący, aby przy następnym uruchomieniu uruchomił się nowy system operacyjny. Program ładujący powróci do starego systemu operacyjnego, jeśli nowy system operacyjny nie uruchomi się, więc klient nie będzie wymagał żadnej pracy. Jeśli aktualizacja się nie powiedzie, klient musi zdecydować, kiedy (i czy) spróbować ponownie, na podstawie szczegółowego kodu błędu. Na przykład dobry klient mógłby rozpoznać, że częściowy („diff”) pakiet OTA kończy się niepowodzeniem i zamiast tego wypróbować pełny pakiet OTA.

Opcjonalnie Klient może:

  • Wyświetl powiadomienie z prośbą o ponowne uruchomienie komputera. Jeśli chcesz wdrożyć politykę zachęcającą użytkownika do rutynowej aktualizacji, możesz dodać to powiadomienie do swojego klienta. Jeśli klient nie wyświetli monitu dla użytkowników, użytkownicy i tak otrzymają aktualizację przy następnym uruchomieniu komputera. (Klient Google ma konfigurowalne opóźnienie dla każdej aktualizacji).
  • Wyświetlaj powiadomienie informujące użytkowników, czy uruchomili nową wersję systemu operacyjnego, czy też oczekiwali tego, ale wrócili do starej wersji systemu operacyjnego. (Klient Google zazwyczaj nie robi żadnego z nich).

Po stronie systemu aktualizacje systemu A/B wpływają na:

  • Wybór partycji (gniazdo), demon update_engine i interakcje z bootloaderem (opisane poniżej)
  • Proces kompilacji i generowanie pakietu aktualizacji OTA (opisane w Wdrażanie aktualizacji A/B )

Wybór partycji (sloty)

Aktualizacje systemu A/B wykorzystują dwa zestawy partycji zwanych gniazdami (zwykle gniazdo A i gniazdo B). System działa z bieżącego gniazda, podczas gdy działający system nie ma dostępu do partycji w nieużywanym gnieździe podczas normalnej pracy. Takie podejście sprawia, że ​​aktualizacje są odporne na błędy, ponieważ nieużywane gniazdo jest rezerwowe: jeśli w trakcie aktualizacji lub bezpośrednio po niej wystąpi błąd, system może przywrócić stare gniazdo i nadal działać. Aby osiągnąć ten cel, w ramach aktualizacji OTA nie należy aktualizować żadnej partycji używanej przez bieżący slot (w tym również partycji, dla których jest tylko jedna kopia).

Każde gniazdo ma atrybut rozruchowy , który określa, czy w gnieździe znajduje się właściwy system, z którego można uruchomić urządzenie. Bieżące gniazdo można uruchomić, gdy system jest uruchomiony, ale w drugim gnieździe może znajdować się stara (nadal poprawna) wersja systemu, nowsza wersja lub nieprawidłowe dane. Niezależnie od tego, jakie jest obecne gniazdo, istnieje jedno gniazdo, które jest gniazdem aktywnym (tym, z którego program ładujący uruchomi się przy następnym uruchomieniu) lub gniazdem preferowanym .

Każde gniazdo ma również atrybut pomyślny ustawiony przez przestrzeń użytkownika, który ma znaczenie tylko wtedy, gdy gniazdo jest również bootowalne. Pomyślnie działający slot powinien być w stanie sam się uruchomić, uruchomić i zaktualizować. Bootowalne gniazdo, które nie zostało oznaczone jako pomyślne (po kilku próbach uruchomienia z niego), powinno zostać oznaczone przez program ładujący jako niemożliwe do uruchomienia, co obejmuje zmianę aktywnego gniazda na inne bootowalne gniazdo (zwykle na gniazdo działające bezpośrednio przed próbą rozruchu) do nowego, aktywnego). Konkretne szczegóły interfejsu są zdefiniowane w boot_control.h .

Zaktualizuj demona silnika

Aktualizacje systemu A/B korzystają z demona działającego w tle o nazwie update_engine , aby przygotować system do uruchomienia w nowej, zaktualizowanej wersji. Ten demon może wykonywać następujące czynności:

  • Odczytaj z bieżących partycji slotu A/B i zapisz dowolne dane na nieużywanych partycjach slotu A/B zgodnie z instrukcjami zawartymi w pakiecie OTA.
  • Wywołaj interfejs boot_control w ramach wstępnie zdefiniowanego przepływu pracy.
  • Uruchom program poinstalacyjny z nowej partycji po zapisaniu wszystkich nieużywanych partycji slotów, zgodnie z instrukcją zawartą w pakiecie OTA. (Aby uzyskać szczegółowe informacje, zobacz Poinstalacja ).

Ponieważ demon update_engine nie bierze udziału w samym procesie rozruchu, jego możliwości podczas aktualizacji są ograniczone przez zasady i funkcje SELinux w bieżącym gnieździe (takich zasad i funkcji nie można zaktualizować, dopóki system nie uruchomi się w Nowa wersja). Aby zachować solidny system, proces aktualizacji nie powinien modyfikować tablicy partycji, zawartości partycji w bieżącym gnieździe ani zawartości partycji innych niż A/B, których nie można wyczyścić przywracając ustawienia fabryczne.

Zaktualizuj źródło silnika

Źródło update_engine znajduje się w system/update_engine . Pliki dexopt A/B OTA są podzielone pomiędzy installd i menedżera pakietów:

Działający przykład można znaleźć w /device/google/marlin/device-common.mk .

Zaktualizuj logi silnika

W przypadku wersji Androida 8.x i wcześniejszych dzienniki update_engine można znaleźć w logcat oraz w raporcie o błędzie. Aby udostępnić dzienniki update_engine w systemie plików, załataj następujące zmiany w swojej kompilacji:

Zmiany te powodują zapisanie kopii najnowszego dziennika update_engine w pliku /data/misc/update_engine_log/update_engine. YEAR - TIME . Oprócz bieżącego dziennika, pięć najnowszych dzienników jest zapisywanych w katalogu /data/misc/update_engine_log/ . Użytkownicy posiadający identyfikator grupy dzienników będą mieli dostęp do dzienników systemu plików.

Interakcje programu ładującego

boot_control HAL jest używany przez update_engine (i prawdopodobnie inne demony) do instruowania bootloadera, z czego ma startować. Typowe przykładowe scenariusze i powiązane z nimi stany obejmują:

  • Przypadek normalny : System działa w swoim bieżącym gnieździe, albo w gnieździe A, albo w gnieździe B. Do tej pory nie zastosowano żadnych aktualizacji. Bieżące gniazdo systemu jest przeznaczone do rozruchu, pomyślne i aktywne.
  • Aktualizacja w toku : System jest uruchamiany z gniazda B, zatem gniazdo B jest gniazdem, które można uruchomić, pomyślnie i jest aktywne. Gniazdo A zostało oznaczone jako niemożliwe do uruchomienia, ponieważ zawartość gniazda A jest aktualizowana, ale nie została jeszcze ukończona. Ponowne uruchomienie w tym stanie powinno kontynuować uruchamianie z gniazda B.
  • Zastosowano aktualizację, oczekiwanie na ponowne uruchomienie : System działa z gniazda B, gniazdo B można uruchomić i zakończyło się pomyślnie, ale gniazdo A zostało oznaczone jako aktywne (i dlatego jest oznaczone jako nadające się do rozruchu). Gniazdo A nie zostało jeszcze oznaczone jako pomyślne i program ładujący powinien podjąć pewną liczbę prób rozruchu z gniazda A.
  • System uruchomił się ponownie w ramach nowej aktualizacji : System jest uruchamiany z gniazda A po raz pierwszy, z gniazda B można nadal uruchamiać się pomyślnie, natomiast z gniazda A można tylko uruchamiać i nadal jest aktywny, ale nie udało się. Demon przestrzeni użytkownika, update_verifier , powinien oznaczyć gniazdo A jako pomyślne po przeprowadzeniu kilku kontroli.

Obsługa aktualizacji strumieniowych

Urządzenia użytkowników nie zawsze mają wystarczającą ilość miejsca w /data , aby pobrać pakiet aktualizacji. Ponieważ ani producenci OEM, ani użytkownicy nie chcą marnować miejsca na partycji /cache , niektórzy użytkownicy rezygnują z aktualizacji, ponieważ urządzenie nie ma gdzie przechowywać pakietu aktualizacji. Aby rozwiązać ten problem, w systemie Android 8.0 dodano obsługę strumieniowego przesyłania aktualizacji A/B, które zapisują bloki bezpośrednio na partycję B podczas ich pobierania, bez konieczności przechowywania bloków w /data . Aktualizacje strumieniowe A/B prawie nie wymagają tymczasowego przechowywania i wymagają wystarczającej ilości miejsca na około 100 KiB metadanych.

Aby włączyć aktualizacje strumieniowe w systemie Android 7.1, wybierz następujące łatki:

Te poprawki są wymagane do obsługi strumieniowego przesyłania aktualizacji A/B w systemie Android 7.1 i nowszych wersjach, niezależnie od tego, czy korzystasz z usług Google Mobile Services (GMS) , czy dowolnego innego klienta aktualizacji.

Życie aktualizacji A/B

Proces aktualizacji rozpoczyna się, gdy pakiet OTA (określany w kodzie jako ładunek ) jest dostępny do pobrania. Zasady w urządzeniu mogą opóźniać pobieranie ładunku i aplikacji w zależności od poziomu naładowania baterii, aktywności użytkownika, stanu ładowania lub innych zasad. Ponadto, ponieważ aktualizacja działa w tle, użytkownicy mogą nie wiedzieć, że aktualizacja jest w toku. Wszystko to oznacza, że ​​proces aktualizacji może zostać przerwany w dowolnym momencie z powodu zasad, nieoczekiwanego ponownego uruchomienia lub działań użytkownika.

Opcjonalnie metadane w samym pakiecie OTA wskazują, że aktualizacja może być przesyłana strumieniowo; tego samego pakietu można również użyć do instalacji bez przesyłania strumieniowego. Serwer może użyć metadanych, aby poinformować klienta, że ​​przesyła strumieniowo, aby klient poprawnie przekazał OTA do update_engine . Producenci urządzeń posiadający własny serwer i klienta mogą włączyć przesyłanie strumieniowe aktualizacji, upewniając się, że serwer wykryje, że aktualizacja jest przesyłana strumieniowo (lub zakłada, że ​​wszystkie aktualizacje są przesyłane strumieniowo), a klient wykona prawidłowe wywołanie update_engine w celu przesyłania strumieniowego. Producenci mogą wykorzystać fakt, że pakiet jest w wariancie przesyłania strumieniowego, aby wysłać do klienta flagę w celu wyzwolenia przekazania do strony frameworka w trybie przesyłania strumieniowego.

Po udostępnieniu ładunku proces aktualizacji wygląda następująco:

Krok Zajęcia
1 Bieżący slot (lub „slot źródłowy”) zostaje oznaczony jako pomyślny (jeśli nie został jeszcze oznaczony) za pomocą markBootSuccessful() .
2 Nieużywane gniazdo (lub „gniazdo docelowe”) jest oznaczane jako niemożliwe do uruchomienia poprzez wywołanie funkcji setSlotAsUnbootable() . Bieżący slot jest zawsze oznaczany jako pomyślny na początku aktualizacji, aby zapobiec cofnięciu się programu ładującego do nieużywanego slotu, w którym wkrótce będą znajdować się nieprawidłowe dane. Jeśli system osiągnął punkt, w którym może rozpocząć stosowanie aktualizacji, bieżący slot zostaje oznaczony jako pomyślny, nawet jeśli inne główne komponenty są uszkodzone (takie jak interfejs użytkownika w pętli awarii), ponieważ możliwe jest wypchnięcie nowego oprogramowania, aby je naprawić problemy.

Ładunek aktualizacji to nieprzezroczysty obiekt blob zawierający instrukcje dotyczące aktualizacji do nowej wersji. Ładunek aktualizacji składa się z następujących elementów:
  • Metadane . Metadane, stanowiące stosunkowo niewielką część ładunku aktualizacji, zawierają listę operacji niezbędnych do wytworzenia i sprawdzenia nowej wersji w gnieździe docelowym. Na przykład operacja może zdekompresować określony obiekt BLOB i zapisać go w określonych blokach partycji docelowej lub odczytać z partycji źródłowej, zastosować poprawkę binarną i zapisać w określonych blokach partycji docelowej.
  • Dodatkowe dane . W tych przykładach dodatkowe dane powiązane z operacjami stanowią większość ładunku aktualizacji i obejmują skompresowany obiekt blob lub poprawkę binarną.
3 Metadane ładunku zostaną pobrane.
4 Dla każdej operacji zdefiniowanej w metadanych, powiązane dane (jeśli istnieją) są pobierane do pamięci, operacja jest wykonywana, a powiązana pamięć jest usuwana.
5 Całe partycje są ponownie odczytywane i weryfikowane pod kątem oczekiwanego skrótu.
6 Uruchamiany jest krok poinstalacyjny (jeśli istnieje). W przypadku błędu podczas wykonywania dowolnego kroku aktualizacja nie powiedzie się i zostanie podjęta ponowna próba z prawdopodobnie innym ładunkiem. Jeśli wszystkie dotychczasowe kroki powiodły się, aktualizacja zakończy się pomyślnie i zostanie wykonany ostatni krok.
7 Nieużywany slot jest oznaczany jako aktywny poprzez wywołanie metody setActiveBootSlot() . Oznaczenie nieużywanego slotu jako aktywnego nie oznacza, że ​​zakończy się jego uruchamianie. Program ładujący (lub sam system) może przełączyć aktywne gniazdo z powrotem, jeśli nie odczyta stanu pomyślnego.
8 Poinstalacja (opisana poniżej) polega na uruchomieniu programu z wersji „nowej aktualizacji”, jednocześnie działającej w starej wersji. Jeśli zdefiniowano to w pakiecie OTA, ten krok jest obowiązkowy i program musi zwrócić z kodem wyjścia 0 ; w przeciwnym razie aktualizacja nie powiedzie się.
9 Gdy system pomyślnie uruchomi się wystarczająco głęboko w nowym gnieździe i zakończy sprawdzanie po ponownym uruchomieniu, aktualnie bieżące gniazdo (dawniej „gniazdo docelowe”) zostaje oznaczone jako pomyślne poprzez wywołanie markBootSuccessful() .

Po instalacji

Dla każdej partycji, dla której zdefiniowany jest krok po instalacji, update_engine montuje nową partycję w określonej lokalizacji i uruchamia program określony w OTA względem zamontowanej partycji. Na przykład, jeśli program poinstalacyjny jest zdefiniowany na partycji systemowej jako usr/bin/postinstall , ta partycja z nieużywanego gniazda zostanie zamontowana w stałej lokalizacji (takiej jak /postinstall_mount ), a /postinstall_mount/usr/bin/postinstall wykonywane jest polecenie /postinstall_mount/usr/bin/postinstall .

Aby instalacja poinstalacyjna przebiegła pomyślnie, stare jądro musi umożliwiać:

  • Zamontuj nowy format systemu plików . Typ systemu plików nie może się zmienić, chyba że jest to obsługiwane w starym jądrze, włączając w to szczegóły, takie jak algorytm kompresji używany w przypadku używania skompresowanego systemu plików (np. SquashFS).
  • Zapoznaj się z formatem programu poinstalacyjnego nowej partycji . Jeśli używasz pliku binarnego w formacie wykonywalnym i linkowalnym (ELF), powinien on być kompatybilny ze starym jądrem (np. nowym 64-bitowym programem działającym na starym 32-bitowym jądrze, jeśli architektura została zmieniona z kompilacji 32-bitowej na 64-bitową). O ile program ładujący ( ld ) nie otrzyma polecenia użycia innych ścieżek lub zbudowania statycznego pliku binarnego, biblioteki zostaną załadowane ze starego obrazu systemu, a nie nowego.

Na przykład możesz użyć skryptu powłoki jako programu poinstalacyjnego interpretowanego przez plik binarny powłoki starego systemu z #! znacznik na górze), a następnie skonfiguruj ścieżki bibliotek z nowego środowiska do wykonywania bardziej złożonego binarnego programu poinstalacyjnego. Alternatywnie możesz uruchomić krok po instalacji z dedykowanej mniejszej partycji, aby umożliwić aktualizację formatu systemu plików na głównej partycji systemowej bez powodowania problemów ze zgodnością wsteczną lub aktualizacji przejściowych; umożliwiłoby to użytkownikom bezpośrednią aktualizację do najnowszej wersji z obrazu fabrycznego.

Nowy program poinstalacyjny jest ograniczony polityką SELinux zdefiniowaną w starym systemie. W związku z tym etap poinstalacyjny jest odpowiedni do wykonywania zadań wymaganych przez projekt na danym urządzeniu lub innych zadań wymagających najwyższego wysiłku (tj. aktualizacji oprogramowania sprzętowego lub programu ładującego obsługującego A/B, przygotowania kopii baz danych dla nowej wersji itp.). ). Krok po instalacji nie jest odpowiedni w przypadku jednorazowych poprawek błędów przed ponownym uruchomieniem, które wymagają nieprzewidzianych uprawnień.

Wybrany program poinstalacyjny działa w kontekście postinstall SELinux. Wszystkie pliki na nowo zamontowanej partycji zostaną oznaczone tagiem postinstall_file , niezależnie od ich atrybutów po ponownym uruchomieniu w nowym systemie. Zmiany atrybutów SELinux w nowym systemie nie będą miały wpływu na etap poinstalacyjny. Jeśli program poinstalacyjny wymaga dodatkowych uprawnień, należy je dodać do kontekstu poinstalacyjnego.

Po ponownym uruchomieniu

Po ponownym uruchomieniu update_verifier uruchamia kontrolę integralności za pomocą dm-verity. Ta kontrola rozpoczyna się przed zygotą, aby uniknąć wprowadzenia przez usługi Java jakichkolwiek nieodwracalnych zmian, które uniemożliwiłyby bezpieczne wycofanie zmian. Podczas tego procesu moduł ładujący i jądro mogą również spowodować ponowne uruchomienie, jeśli zweryfikowany rozruch lub dm-verity wykryją jakiekolwiek uszkodzenia. Po zakończeniu sprawdzania update_verifier oznacza, że ​​rozruch zakończył się pomyślnie.

update_verifier odczyta tylko bloki wymienione w /data/ota_package/care_map.txt , który jest zawarty w pakiecie A/B OTA podczas korzystania z kodu AOSP. Klient aktualizacji systemu Java, taki jak GmsCore, wyodrębnia plik care_map.txt , konfiguruje uprawnienia dostępu przed ponownym uruchomieniem urządzenia i usuwa wyodrębniony plik po pomyślnym uruchomieniu systemu w nowej wersji.