Aktualizacje systemu A/B (bezproblemowe)

Aktualizacje systemu A/B, znane również jako aktualizacje bezproblemowe, zapewniają, że działający system rozruchowy pozostaje na dysku podczas aktualizacji bezprzewodowej (OTA) . Takie podejście zmniejsza prawdopodobieństwo nieaktywności urządzenia po aktualizacji, co oznacza mniejszą liczbę wymian urządzeń i powtórnych flashów urządzeń w centrach naprawczych i gwarancyjnych. Inne systemy operacyjne klasy komercyjnej, takie jak ChromeOS , również z powodzeniem korzystają z aktualizacji A/B.

Aby uzyskać więcej informacji o aktualizacjach systemu A/B i sposobie 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 przerywania użytkownikowi. Użytkownicy mogą nadal korzystać ze swoich urządzeń podczas OTA — jedynym przestojem podczas aktualizacji jest ponowne uruchomienie urządzenia na zaktualizowanej partycji dysku.
  • Po aktualizacji ponowne uruchomienie nie trwa dłużej niż zwykłe ponowne uruchomienie.
  • Jeśli OTA nie zostanie zastosowane (na przykład z powodu złego flasha), 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 ponowić próbę aktualizacji.
  • Jeśli aktualizacja OTA zostanie zastosowana, ale nie powiedzie się, 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żytkownika.
  • 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 nieuszkodzony obraz. Jeśli urządzenie nie uruchamia się z powodu złego problemu z OTA lub dm-verity, urządzenie może ponownie uruchomić się ze starym obrazem. ( Zweryfikowany rozruch systemu Android nie wymaga aktualizacji A/B).

O aktualizacjach systemu A/B

Aktualizacje A/B wymagają zmian zarówno w kliencie, jak iw 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 są w AOSP, a kod klienta jest dostarczany przez usługi Google Play. Producenci OEM, którzy nie korzystają z infrastruktury OTA firmy Google, będą mogli ponownie wykorzystać kod systemu AOSP, ale będą musieli dostarczyć własnego klienta.

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

  • Zdecyduj, kiedy wykonać 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 jest w trybie bezczynności, na przykład w nocy, i jest połączone z siecią Wi-Fi. Jednak Twój klient może używać dowolnej heurystyki.
  • Skontaktuj się z serwerami pakietów OTA i ustal, czy aktualizacja jest dostępna. Powinno to być w większości takie samo jak twój istniejący kod klienta, z wyjątkiem tego, że będziesz chciał zasygnalizować, że urządzenie obsługuje A/B. (Klient Google zawiera również przycisk Sprawdź teraz , aby użytkownicy mogli sprawdzić najnowszą aktualizację).
  • 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.
  • Raportuj sukcesy lub niepowodzenia instalacji na swoich serwerach na podstawie kodu wyniku update_engine . Jeśli aktualizacja zostanie zastosowana pomyślnie, update_engine poinformuje bootloader, aby uruchomił się w nowym systemie operacyjnym przy następnym ponownym uruchomieniu. Bootloader powróci do starego systemu operacyjnego, jeśli nowy system operacyjny nie uruchomi się, więc klient nie będzie musiał wykonywać ż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 może rozpoznać, że częściowy ("różnic") pakiet OTA nie działa i zamiast tego spróbować użyć pełnego pakietu OTA.

Opcjonalnie klient może:

  • Pokaż powiadomienie z prośbą o ponowne uruchomienie. Jeśli chcesz zaimplementować zasady, w których użytkownik jest zachęcany do rutynowego aktualizowania, to powiadomienie można dodać do klienta. Jeśli klient nie wyświetli monitu, użytkownicy i tak otrzymają aktualizację przy następnym uruchomieniu. (Klient Google ma konfigurowalne opóźnienie dla każdej aktualizacji).
  • Pokaż powiadomienie informujące użytkowników, czy uruchomili nową wersję systemu operacyjnego, czy też mieli to zrobić, ale wrócili do starej wersji systemu operacyjnego. (Klient Google zwykle tego nie robi).

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

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

Wybór partycji (gniazda)

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 partycje w nieużywanym gnieździe nie są dostępne dla działającego systemu podczas normalnej pracy. Takie podejście sprawia, że ​​aktualizacje są odporne na błędy, zachowując nieużywane gniazdo jako rezerwę: jeśli błąd wystąpi podczas aktualizacji lub bezpośrednio po niej, system może powrócić do starego gniazda i nadal mieć działający system. Aby osiągnąć ten cel, żadna partycja używana przez bieżące gniazdo nie powinna być aktualizowana w ramach aktualizacji OTA (w tym partycje, dla których jest tylko jedna kopia).

Każde gniazdo ma atrybut bootowalny , który określa, czy gniazdo zawiera poprawny system, z którego można uruchomić urządzenie. Bieżący slot jest bootowalny, gdy system jest uruchomiony, ale drugi slot może mieć starą (nadal poprawną) wersję systemu, nowszą wersję lub nieprawidłowe dane. Niezależnie od tego, jakie jest obecne gniazdo, istnieje jedno gniazdo, które jest aktywnym gniazdem (tym, z którego bootloader uruchomi się podczas następnego rozruchu) lub preferowanym gniazdem.

Każdy slot ma również udany atrybut ustawiony przez przestrzeń użytkownika, co ma znaczenie tylko wtedy, gdy slot jest również bootowalny. Udany slot powinien być w stanie sam się uruchomić, uruchomić i zaktualizować. Bootowalny slot, który nie został oznaczony jako udany (po kilku próbach uruchomienia z niego), powinien zostać oznaczony jako niebootowalny przez bootloader, łącznie ze zmianą aktywnego slotu na inny bootowalny slot (zwykle na slot uruchomiony bezpośrednio przed próbą rozruchu w nowy, aktywny). Konkretne szczegóły interfejsu są zdefiniowane w boot_control.h .

Zaktualizuj demona silnika

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

  • Odczytuj z bieżących partycji gniazda A/B i zapisuj dane na nieużywanych partycjach gniazda A/B zgodnie z instrukcjami pakietu OTA.
  • Wywołaj interfejs boot_control we wstępnie zdefiniowanym przepływie pracy.
  • Uruchom program poinstalacyjny z nowej partycji po zapisaniu wszystkich nieużywanych partycji slotów, zgodnie z instrukcjami pakietu OTA. (Aby uzyskać szczegółowe informacje, patrz Po instalacji ).

Ponieważ demon update_engine nie jest zaangażowany w sam proces rozruchu, jego możliwości działania 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 zostanie uruchomiony w trybie Nowa wersja). Aby utrzymać 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ć za pomocą przywrócenia ustawień fabrycznych.

Zaktualizuj źródło silnika

Źródło update_engine znajduje się w system/update_engine . Pliki A/B OTA installd są dzielone na zainstalowane 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 i w raporcie o błędzie. Aby udostępnić dzienniki update_engine w systemie plików, załataj w swojej kompilacji następujące zmiany:

Te zmiany zapisują kopię najnowszego dziennika update_engine w /data/misc/update_engine_log/update_engine. YEAR - TIME . Oprócz bieżącego dziennika w katalogu /data/misc/update_engine_log/ zapisanych jest pięć najnowszych dzienników. Użytkownicy z identyfikatorem grupy dzienników będą mogli uzyskać dostęp do dzienników systemu plików.

Interakcje bootloadera

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

  • Przypadek normalny : system działa z bieżącego gniazda, gniazda A lub B. Do tej pory nie zastosowano żadnych aktualizacji. Obecne gniazdo systemu jest uruchamialne, pomyślne i aktywne.
  • Aktualizacja w toku : System działa ze slotu B, więc slot B jest startowym, udanym i aktywnym slotem. Gniazdo A zostało oznaczone jako nieuruchamiające się, ponieważ zawartość gniazda A jest aktualizowana, ale nie została jeszcze ukończona. Ponowne uruchomienie w tym stanie powinno kontynuować uruchamianie z gniazda B.
  • Aktualizacja zastosowana, oczekuje na ponowne uruchomienie : system działa ze slotu B, slot B jest bootowalny i pomyślnie, ale slot A został oznaczony jako aktywny (i dlatego jest oznaczony jako bootowalny). Slot A nie jest jeszcze oznaczony jako pomyślny i pewna liczba prób uruchomienia z gniazda A powinna zostać wykonana przez bootloader.
  • System został ponownie uruchomiony z nową aktualizacją : System działa z gniazda A po raz pierwszy, gniazdo B jest nadal bootowalne i działa pomyślnie, podczas gdy gniazdo A jest tylko bootowalne i nadal aktywne, ale nie powiodło się. Demon przestrzeni użytkownika, update_verifier , powinien oznaczyć gniazdo A jako pomyślne po wykonaniu kilku sprawdzeń.

Obsługa aktualizacji strumieniowej

Urządzenia użytkownika nie zawsze mają wystarczająco dużo miejsca w katalogu /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ę strumieniowych aktualizacji A/B, które zapisują bloki bezpośrednio na partycji B podczas ich pobierania, bez konieczności przechowywania bloków w /data . Aktualizacje strumieniowe A/B prawie nie wymagają tymczasowej pamięci masowej i wymagają tylko pamięci wystarczającej na około 100 KB metadanych.

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

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

Żywotność aktualizacji A/B

Proces aktualizacji rozpoczyna się, gdy pakiet OTA (określany w kodzie jako payload ) jest dostępny do pobrania. Zasady w urządzeniu mogą opóźniać pobieranie ładunku i aplikacji w zależności od poziomu 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; ten sam pakiet może być również użyty 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ń z własnym serwerem i klientem mogą włączyć przesyłanie strumieniowe aktualizacji, upewniając się, że serwer identyfikuje, że aktualizacja jest przesyłana strumieniowo (lub zakłada, że ​​wszystkie aktualizacje są przesyłane strumieniowo), a klient wykonuje 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ć flagę do klienta, aby wyzwolić przekazanie po stronie platformy jako przesyłanie strumieniowe.

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

Krok Zajęcia
1 Bieżący slot (lub "slot źródłowy") jest oznaczony jako pomyślny (jeśli nie został jeszcze oznaczony) za pomocą markBootSuccessful() .
2 Nieużywane gniazdo (lub „miejsce docelowe”) jest oznaczane jako niebootowalne przez wywołanie funkcji setSlotAsUnbootable() . Aktualne gniazdo jest zawsze oznaczane jako pomyślne na początku aktualizacji, aby uniemożliwić bootloaderowi powrót do nieużywanego gniazda, które wkrótce będzie zawierało nieprawidłowe dane. Jeśli system osiągnął punkt, w którym może rozpocząć stosowanie aktualizacji, bieżące gniazdo jest oznaczane jako pomyślne, nawet jeśli inne główne komponenty są uszkodzone (takie jak interfejs użytkownika w pętli awaryjnej), ponieważ możliwe jest wypchnięcie nowego oprogramowania, aby je naprawić problemy.

Ładunek aktualizacji to nieprzezroczysty obiekt blob z instrukcjami aktualizacji do nowej wersji. Pakiet aktualizacji składa się z następujących elementów:
  • Metadane . Metadane, stosunkowo niewielka część ładunku aktualizacji, zawierają listę operacji do utworzenia i zweryfikowania nowej wersji w gnieździe docelowym. Na przykład operacja może zdekompresować określony obiekt blob i zapisać go w określonych blokach na partycji docelowej lub odczytać z partycji źródłowej, zastosować poprawkę binarną i zapisać do określonych bloków na partycji docelowej.
  • Dodatkowe dane . Ponieważ większość ładunku aktualizacji, dodatkowe dane związane z operacjami składają się w tych przykładach ze skompresowanego obiektu BLOB lub poprawki binarnej.
3 Pobierane są metadane ładunku.
4 Dla każdej operacji zdefiniowanej w metadanych, kolejno, skojarzone dane (jeśli istnieją) są pobierane do pamięci, operacja jest stosowana, a skojarzona pamięć jest odrzucana.
5 Całe partycje są ponownie odczytywane i weryfikowane z oczekiwanym hashem.
6 Uruchamiany jest etap poinstalacyjny (jeśli istnieje). W przypadku błędu podczas wykonywania dowolnego kroku aktualizacja kończy się niepowodzeniem i ponawiana jest próba z ewentualnie innym ładunkiem. Jeśli wszystkie dotychczasowe kroki powiodły się, aktualizacja się powiedzie i zostanie wykonany ostatni krok.
7 Nieużywane gniazdo jest oznaczane jako aktywne przez wywołanie setActiveBootSlot() . Oznaczenie nieużywanego slotu jako aktywnego nie oznacza zakończenia uruchamiania. Bootloader (lub sam system) może przełączyć aktywne gniazdo z powrotem, jeśli nie odczyta pomyślnego stanu.
8 Postinstalacja (opisana poniżej) polega na uruchomieniu programu z wersji „nowej aktualizacji” przy jednoczesnym uruchomieniu programu w starej wersji. Jeśli jest zdefiniowany w pakiecie OTA, ten krok jest obowiązkowy i program musi powrócić z kodem zakończenia 0 ; w przeciwnym razie aktualizacja nie powiedzie się.
9 Gdy system pomyślnie uruchomi się wystarczająco daleko w nowym gnieździe i zakończy sprawdzanie po ponownym uruchomieniu, obecnie bieżące gniazdo (wcześniej „docelowe gniazdo”) jest oznaczane jako pomyślne przez wywołanie markBootSuccessful() .

Po instalacji

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

Aby postinstalacja się powiodła, stare jądro musi być w stanie:

  • Zamontuj nowy format systemu plików . Typ systemu plików nie może się zmienić, chyba że jest dla niego wsparcie w starym jądrze, w tym szczegóły, takie jak algorytm kompresji używany podczas korzystania ze skompresowanego systemu plików (np. SquashFS).
  • Zapoznaj się z formatem programu poinstalacyjnego nowej partycji . Jeśli używasz pliku binarnego Executable and Linkable Format (ELF), powinien on być zgodny ze starym jądrem (np. nowy 64-bitowy program działający 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 zostanie poinstruowany, aby użyć innych ścieżek lub zbudować statyczny plik binarny, biblioteki będą ładowane ze starego obrazu systemu, a nie z nowego.

Na przykład, możesz użyć skryptu powłoki jako programu poinstalacyjnego interpretowanego przez plik binarny powłoki starego systemu za pomocą #! znacznik u góry), a następnie skonfiguruj ścieżki biblioteki z nowego środowiska do wykonywania bardziej złożonego programu poinstalacyjnego binarnego. 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 ponoszenia problemów ze zgodnością wsteczną lub aktualizacji typu stepping; umożliwiłoby to użytkownikom aktualizację bezpośrednio do najnowszej wersji z obrazu fabrycznego.

Nowy program poinstalacyjny jest ograniczony przez polityki SELinux zdefiniowane w starym systemie. Jako taki, etap poinstalacyjny jest odpowiedni do wykonywania zadań wymaganych przez projekt na danym urządzeniu lub innych zadań typu best-effort (tj. aktualizacji oprogramowania układowego lub bootloadera obsługującego A/B, przygotowania kopii baz danych dla nowej wersji itp. ). Krok po instalacji nie jest odpowiedni dla 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 w nowej zamontowanej partycji zostaną oznaczone tagiem postinstall_file , niezależnie od ich atrybutów po ponownym uruchomieniu w nowym systemie. Zmiany w atrybutach SELinux w nowym systemie nie wpłyną 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 sprawdzanie integralności przy użyciu dm-verity. To sprawdzenie rozpoczyna się przed zygote, aby uniknąć wprowadzania przez usługi Java jakichkolwiek nieodwracalnych zmian, które uniemożliwiłyby bezpieczne wycofanie. Podczas tego procesu bootloader i jądro mogą również wywołać ponowne uruchomienie, jeśli zweryfikowany rozruch lub dm-verity wykryją jakiekolwiek uszkodzenie. Po zakończeniu sprawdzania update_verifier oznacza pomyślny rozruch.

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 care_map.txt , ustawia uprawnienia dostępu przed ponownym uruchomieniem urządzenia i usuwa wyodrębniony plik po pomyślnym uruchomieniu systemu do nowej wersji.