Aktualizacje systemu A/B (bezproblemowe)

Starsze aktualizacje systemu A/B, zwane też bezproblemowymi aktualizacjami, zapewniają, że podczas bezprzewodowej aktualizacji (OTA) na dysku pozostaje działający system uruchamiania. Takie podejście zmniejsza prawdopodobieństwo, że urządzenie będzie nieaktywne po aktualizacji, co oznacza mniejszą liczbę wymian urządzeń i przeprogramowań w centrach napraw i gwarancji. Inne systemy operacyjne klasy komercyjnej, takie jak ChromeOS, również korzystają z aktualizacji A/B.

Więcej informacji o aktualizacjach systemu A/B i ich działaniu znajdziesz w artykule Wybór partycji (slotów).

Aktualizacje systemu A/B zapewniają te korzyści:

  • Aktualizacje OTA mogą być przeprowadzane podczas działania systemu bez przerywania pracy użytkownika. Podczas aktualizacji OTA użytkownicy mogą nadal korzystać ze swoich urządzeń. Jedynym okresem przestoju podczas aktualizacji jest czas, gdy urządzenie uruchamia się ponownie z aktualizowaną partycją dysku.
  • Po aktualizacji ponowne uruchomienie nie trwa dłużej niż zwykłe ponowne uruchomienie.
  • Jeśli nie uda się zastosować aktualizacji OTA (na przykład z powodu nieprawidłowego flashowania), użytkownik nie odczuje tego. Użytkownik będzie nadal korzystać ze starego systemu operacyjnego, a klient może ponownie spróbować przeprowadzić aktualizację.
  • Jeśli aktualizacja OTA zostanie zastosowana, ale nie uda się uruchomić urządzenia, zostanie ono ponownie uruchomione na starym partycji i nadal będzie można z niego korzystać. Klient może ponownie spróbować przeprowadzić aktualizację.
  • Błędy (np. błędy we/wy) wpływają tylko na nieużywane zestawy partycji i można je powtórzyć. Takie błędy są też mniej prawdopodobne, ponieważ obciążenie wejść/wyjść jest celowo niskie, aby nie pogarszać wrażeń użytkowników.
  • Aktualizacje mogą być przesyłane strumieniowo na urządzenia A/B, co eliminuje konieczność pobierania pakietu przed instalacją. Transmisja strumieniowa oznacza, że użytkownik nie musi mieć wystarczającej ilości wolnego miejsca na przechowywanie pakietu aktualizacji na urządzeniu z systemem /data lub /cache.
  • Partycja pamięci podręcznej nie jest już używana do przechowywania pakietów aktualizacji OTA, więc nie musisz dbać o to, aby była wystarczająco duża na przyszłe aktualizacje.
  • dm-veritygwarantuje, że urządzenie uruchomi nieuszkodzony obraz. Jeśli urządzenie nie uruchamia się z powodu błędu OTA lub problemu z dziennikiem weryfikacji, może się zrestartować ze starym obrazem. ( Weryfikacja podczas uruchamiania w Androidzie nie wymaga aktualizacji A/B).

Aktualizacje systemu A/B

Aktualizacje A/B wymagają wprowadzenia zmian zarówno w kliencie, jak i w systemie. Serwer pakietów OTA nie powinien jednak wymagać zmian: pakiety aktualizacji są nadal udostępniane przez HTTPS. W przypadku urządzeń korzystających z infrastruktury OTA Google wszystkie zmiany systemowe są wprowadzane w AOSP, a kod klienta jest dostarczany przez Usługi Google Play. Producenci OEM, którzy nie korzystają z infrastruktury OTA Google, będą mogli ponownie użyć kodu systemu AOSP, ale będą musieli dostarczyć własnego klienta.

W przypadku OEM-ów dostarczających własne klienty klient musi:

  • Zdecyduj, kiedy chcesz wykonać aktualizację. Aktualizacje A/B są przeprowadzane w tle, więc nie są już inicjowane przez użytkownika. Aby nie zakłócać pracy użytkowników, zalecamy planowanie aktualizacji na czas, gdy urządzenie jest w trybie konserwacji, np. w nocy, i jest połączone z siecią Wi-Fi. Twój klient może jednak używać dowolnej heurystyki.
  • Sprawdź na serwerach pakietów OTA, czy dostępna jest aktualizacja. Ten kod powinien być w większości taki sam jak dotychczasowy kod klienta, ale musisz zaznaczyć, że urządzenie obsługuje test A/B. (klient Google zawiera też przycisk Sprawdź teraz, który umożliwia użytkownikom sprawdzenie dostępności najnowszej aktualizacji).
  • Wywołaj funkcję update_engine, podając adres URL HTTPS pakietu aktualizacji (jeśli jest dostępny). update_engine zaktualizuje bloki nieprzetworzone na obecnie nieużywanym partycji podczas przesyłania pakietu aktualizacji.
  • Zgłaszanie na serwery informacji o udanych lub nieudanych instalacjach na podstawie kodu wyniku update_engine. Jeśli aktualizacja zostanie zastosowana, update_engine poinformuje bootloader, aby podczas następnego ponownego uruchamiania uruchomił nowy system operacyjny. Jeśli nowy system operacyjny nie uruchomi się, bootloader użyje starego systemu operacyjnego, więc klient nie musi nic robić. 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 pakiet OTA z danymi częściowymi (z różnicami) zakończył się niepowodzeniem, i spróbować użyć pełnego pakietu OTA.

Opcjonalnie klient może:

  • Wyświetlanie powiadomienia z prośbą o ponowne uruchomienie. Jeśli chcesz wdrożyć zasadę, która zachęca użytkownika do regularnego aktualizowania, możesz dodać to powiadomienie do klienta. Jeśli klient nie wyświetla użytkownikom prośby, aktualizacja zostanie zainstalowana przy następnym uruchomieniu. (klient Google ma konfigurowalne opóźnienie na potrzeby każdej aktualizacji).
  • wyświetlać powiadomienie informujące użytkowników, czy uruchomili nową wersję systemu operacyjnego, czy też mieli to zrobić, ale wrócili do starej wersji; (klient Google zwykle nie korzysta z żadnego z tych formatów).

Zmiany w systemie A/B dotyczą:

  • wybór partycji (slotów), demon update_engine i interfejsy bootloadera (opisane poniżej);
  • proces kompilacji i generowania pakietu aktualizacji OTA (opisany w artykule Wdrażanie aktualizacji A/B);

Wybór partycji (przedziały)

Aktualizacje systemu A/B korzystają z 2 zbiorów partycji zwanych slotami (zwykle slot A i slot B). System działa w bieżącym schowku, a system w nieużywanym schowku nie jest używany podczas normalnej pracy systemu. Dzięki temu podejściu aktualizacje są odporne na błędy, ponieważ nieużywany slot stanowi zabezpieczenie. Jeśli podczas aktualizacji lub bezpośrednio po niej wystąpi błąd, system może przywrócić poprzedni stan i nadal działać. Aby to osiągnąć, żadna partycja używana przez obecny slot nie powinna być aktualizowana w ramach aktualizacji OTA (w tym partycje, dla których istnieje tylko jedna kopia).

Każdy slot ma atrybut bootable, który określa, czy slot zawiera prawidłowy system, z którego urządzenie może się uruchamiać. Bieżący slot można uruchomić, gdy system jest włączony, ale drugi slot może zawierać starą (nadal prawidłową) wersję systemu, nowszą wersję lub nieprawidłowe dane. Niezależnie od tego, jaki jest obecny slot, jest jeden slot, który jest aktywny (z którego bootloader uruchomi się podczas następnego rozruchu) lub preferowany.

Każdy slot ma też atrybut successful ustawiony przez przestrzeń użytkownika, który jest istotny tylko wtedy, gdy slot jest też uruchamiany. Uruchomiony slot powinien być w stanie uruchomić się, działać i aktualizować się. Bootable slot, który nie został oznaczony jako udany (po kilku próbach uruchomienia z niego) powinien zostać oznaczony przez bootloader jako niemożliwy do uruchomienia, w tym poprzez zmianę aktywnego slotu na inny bootable slot (zwykle na ten, który działał bezpośrednio przed próbą uruchomienia nowego, aktywnego slotu). Szczegóły dotyczące interfejsu są zdefiniowane w pliku boot_control.h.

Aktualizacja demona silnika

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

  • odczytywać partycje A/B bieżącego slotu i zapisywać dane na nieużywanych partycjach A/B zgodnie z instrukcjami pakietu OTA;
  • Wywołaj interfejs boot_control w ramach zdefiniowanego wstępnie procesu.
  • Uruchom program po zainstalowaniu z nowego partycji po zapisaniu wszystkich nieużywanych partycji slotów zgodnie z instrukcjami pakietu OTA. (szczegółowe informacje znajdziesz w sekcji Po instalacji).

Ponieważ demon update_engine nie jest zaangażowany w sam proces uruchamiania, jego możliwości podczas aktualizacji są ograniczone przez zasady i funkcje SELinuxbieżącym slocie (takich zasad i funkcji nie można zaktualizować, dopóki system nie uruchomi nowej wersji). Aby zapewnić stabilność systemu, proces aktualizacji nie powinien modyfikować tabeli partycji, zawartości partycji w bieżącym slocie ani zawartości partycji innych niż A/B, których nie można wymazać przy przywracaniu do ustawień fabrycznych.

Aktualizowanie źródła silnika

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

Przykładowy kod znajdziesz w sekcji /device/google/marlin/device-common.mk.

Aktualizowanie logów mechanizmu

W przypadku wersji Androida 8.x i starszych dzienniki update_engine znajdziesz w logcat i raporcie o błędach. Aby udostępnić logi update_engine w systemie plików, wprowadź w kompilacji te zmiany:

Te zmiany zapisują kopię 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 folderze /data/misc/update_engine_log/. Użytkownicy z identyfikatorem grupy log będą mogli uzyskać dostęp do dzienników systemu plików.

Interakcje z programem rozruchowym

Interfejs boot_control HAL jest używany przez update_engine (i być może inne demony) do przekazywania informacji o tym, z czego ma być ładowany bootloader. Typowe przykładowe scenariusze i powiązane stany:

  • Normalny przypadek: system działa w bieżącym slocie, czyli w slocie A lub B. Do tej pory nie zastosowano żadnych aktualizacji. Bieżący slot systemu jest uruchamialny, udany i aktywny.
  • Aktualizacja w toku: system działa w slocie B, więc slot B jest sformatowanym, uruchomionym i aktywnym. Gniazdo A zostało oznaczone jako nieuruchamialne, 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 została zastosowana, oczekuje się na ponowne uruchomienie: system działa w gniazdku B, gniazdo B można uruchomić i działa prawidłowo, ale gniazdo A zostało oznaczone jako aktywne (i dlatego można je uruchomić). Slot A nie został jeszcze oznaczony jako udany, a ładowarka powinna wykonać pewną liczbę prób rozruchu z tego slotu.
  • System uruchomiony z nową aktualizacją: system jest uruchamiany po raz pierwszy z poziomu gniazda A, gniazdo B nadal można uruchomić i uzyskać z niego dostęp, a gniazdo A można uruchomić i nadal jest aktywne, ale nie można z niego korzystać. Demon przestrzeni użytkownika update_verifier powinien oznaczyć slot A jako udany po wykonaniu pewnych kontroli.

Obsługa aktualizacji strumieniowych

Na urządzeniach użytkowników nie zawsze jest wystarczająco dużo miejsca na /data, aby pobrać pakiet aktualizacji. Ani producenci OEM, ani użytkownicy nie chcą marnować miejsca na partycji /cache, dlatego niektórzy użytkownicy nie instalują aktualizacji, ponieważ na urządzeniu nie ma miejsca na pakiet aktualizacji. Aby rozwiązać ten problem, w Androidzie 8.0 dodano obsługę aktualizacji A/B strumieniowych, które zapisują bloki bezpośrednio na partycji B w miarę ich pobierania, bez konieczności przechowywania bloków na dysku/data. Strumieniowe aktualizacje A/B nie wymagają prawie żadnej pamięci tymczasowej i wystarcza im pamięci na około 100 KiB metadanych.

Aby włączyć aktualizacje strumieniowe w Androidzie 7.1, wybierz te poprawki:

Te poprawki są wymagane do obsługi aktualizacji A/B strumieniowych w Androidzie 7.1 i nowszych, niezależnie od tego, czy korzystasz z usług mobilnych Google (GMS), czy z innego klienta aktualizacji.

Cykl życia aktualizacji testu A/B

Proces aktualizacji rozpoczyna się, gdy pakiet OTA (w kodzie nazywany ładunkiem) jest dostępny do pobrania. Zasady na urządzeniu mogą opóźnić pobieranie i stosowanie ładunku w zależności od poziomu naładowania baterii, aktywności użytkownika, stanu ładowania lub innych zasad. Ponadto aktualizacja działa w tle, więc użytkownicy mogą nie wiedzieć, że jest w trakcie. Oznacza to, że proces aktualizacji może zostać przerwany w dowolnym momencie z powodu zasad, nieoczekiwanego restartu lub działań użytkownika.

Opcjonalnie metadane w pakiecie OTA mogą wskazywać, że aktualizacja może być przesyłana strumieniowo. Tego samego pakietu można też użyć do instalacji bez strumieniowego przesyłania. Serwer może użyć metadanych, aby poinformować klienta o transmisji strumieniowej, dzięki czemu klient przekaże OTA do update_engine. Producenci urządzeń, którzy mają własny serwer i klienta, mogą włączyć aktualizacje strumieniowe, zapewniając, że serwer rozpoznaje, że aktualizacja jest strumieniowa (lub zakłada, że wszystkie aktualizacje są strumieniowe), a klient wykona odpowiedni wywołanie update_engine w celu strumieniowego przesyłania danych. Producenci mogą wykorzystać fakt, że pakiet jest wersją strumieniową, aby wysłać flagę do klienta i wyzwolić przekazanie do strony frameworku jako strumieniowe.

Gdy ładunek jest dostępny, proces aktualizacji wygląda następująco:

Krok Działania
1 Bieżący slot (czyli „slot źródłowy”) jest oznaczony jako udany (jeśli nie był oznaczony wcześniej) za pomocą wartości markBootSuccessful().
2 Nieużywany slot (czyli „docelowy slot”) jest oznaczony jako nieuruchamialny przez wywołanie funkcji setSlotAsUnbootable(). Bieżący slot jest zawsze oznaczany jako udany na początku aktualizacji, aby uniemożliwić bootloaderowi przejście na nieużywany slot, w którym wkrótce znajdą się nieprawidłowe dane. Jeśli system osiągnął punkt, w którym może rozpocząć stosowanie aktualizacji, bieżący slot zostanie oznaczony jako udany, nawet jeśli inne główne komponenty są uszkodzone (np. interfejs w pętli awarii), ponieważ można przesłać nowe oprogramowanie w celu rozwiązania tych problemów.

Obejmuje ona spakowany ładunek z instrukcjami dotyczącymi aktualizacji do nowej wersji. Ładunek aktualizacji składa się z tych elementów:
  • Metadane. Metadane stanowią stosunkowo niewielką część ładunku aktualizacji. Zawierają one listę operacji, które mają wygenerować i zweryfikować nową wersję na docelowym slocie. Operacja może na przykład rozpakować określony blob i zapisz go w określonych blokach na partycji docelowej lub odczytać z partycji źródłowej, zastosować binarną łatkę i zapisz w określonych blokach na partycji docelowej.
  • Dodatkowe dane. W przypadku tych przykładów większość danych aktualizacji, czyli dodatkowe dane związane z operacjami, składa się z skompresowanego bloba lub binarnego pakietu poprawek.
3 Pobieranie metadanych ładunku.
4 W przypadku każdej operacji zdefiniowanej w metadanych po kolei pobierane są do pamięci powiązane dane (jeśli istnieją), a następnie operacja jest stosowana, a powiązana pamięć jest odrzucana.
5 Cała partycja jest ponownie odczytywana i weryfikowana pod kątem oczekiwanego wartości funkcji skrótu.
6 Etap po instalacji (jeśli występuje) jest wykonywany. W przypadku błędu podczas wykonywania dowolnego kroku aktualizacja kończy się niepowodzeniem i jest ponownie podejmowana z możliwie inną zawartością. Jeśli wszystkie kroki zostały wykonane prawidłowo, aktualizacja zakończy się powodzeniem i zostanie wykonany ostatni krok.
7 Nieużywany slot jest oznaczony jako aktywny przez wywołanie setActiveBootSlot(). Oznaczenie nieużywanego slotu jako aktywnego nie oznacza, że zakończy on uruchamianie. Program rozruchowy (lub sam system) może przywrócić aktywny slot, jeśli nie odczyta stanu powodzenia.
8 Po instalacji (opisanej poniżej) należy uruchomić program z „nowego pakietu aktualizacji”, gdy nadal działa stara wersja. Jeśli jest zdefiniowany w pakiecie OTA, ten krok jest obowiązkowy, a program musi zwrócić kod zakończenia 0. W przeciwnym razie aktualizacja się nie powiedzie.
9 Gdy system uruchomi się w nowym slocie i zakończy sprawdzanie po ponownym uruchomieniu, bieżący slot (dawniej „docelowy”) zostanie oznaczony jako udany przez wywołanie funkcji markBootSuccessful().

Po instalacji

W przypadku każdej partycji, w której zdefiniowano krok po instalacji, update_engine montuje nową partycję w określonej lokalizacji i wykonuje program określony w OTA względem zamontowanej partycji. Jeśli na przykład program po instalacji jest zdefiniowany jako usr/bin/postinstall na partycji systemowej, ta partycja z niewykorzystanego slotu zostanie zamontowana w stałym miejscu (np. /postinstall_mount), a następnie zostanie wykonane polecenie /postinstall_mount/usr/bin/postinstall.

Aby pomyślnie przeprowadzić instalację, stare jądro musi:

  • Zamontuj nowy format systemu plików. Typ systemu plików nie może się zmienić, chyba że jest obsługiwany przez stare jądro, w tym szczegóły takie jak algorytm kompresji używany w przypadku systemu plików skompresowanego (np. SquashFS).
  • Poznaj format programu po instalacji nowej partycji. Jeśli używasz binarnego formatu ELF, musi 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 przełączona z 32- na 64-bitową). O ile nie zostanie podane instrukcje dotyczące ładowarki (ld) dotyczące korzystania z innych ścieżek lub tworzenia statycznych plików binarnych, biblioteki będą ładowane ze starego obrazu systemu, a nie z nowego.

Możesz na przykład użyć skryptu powłoki jako programu po zainstalowaniu, który jest interpretowany przez binarny kod powłoki starego systemu (z znakami #!na początku), a następnie skonfigurować ścieżki bibliotek z nowego środowiska, aby wykonać bardziej złożony binarny program po zainstalowaniu. Możesz też wykonać krok po instalacji z dedykowanej mniejszej partycji, aby umożliwić aktualizację formatu systemu plików na głównej partycji systemu bez problemów z kompatybilnością wsteczną lub aktualizacjami pośrednimi. Pozwoli to użytkownikom na bezpośrednie przejście do najnowszej wersji z obrazu fabrycznego.

Nowy program po instalacji jest ograniczony przez zasady SELinux zdefiniowane w starym systemie. Dlatego krok po instalacji jest odpowiedni do wykonywania zadań wymaganych przez projekt na danym urządzeniu lub innych zadań wykonywanych z możliwie największą dokładnością. Krok po instalacji nie jest odpowiedni do wprowadzania jednorazowych poprawek błędów przed ponownym uruchomieniem, które wymagają nieprzewidzianych uprawnień.

Wybrany program instalacyjny działa w kontekście postinstall SELinux. Wszystkie pliki na nowym zamontowanym partycji zostaną oznaczone etykietą postinstall_file, niezależnie od ich atrybutów po ponownym uruchomieniu nowego systemu. Zmiany atrybutów SELinux w nowym systemie nie wpłyną na krok po instalacji. Jeśli program po zainstalowaniu potrzebuje dodatkowych uprawnień, należy je dodać do kontekstu po zainstalowaniu.

Po ponownym uruchomieniu

Po ponownym uruchomieniu update_verifier uruchamia sprawdzanie integralności za pomocą dm-verity. Ta kontrola rozpoczyna się przed zygote, aby uniknąć wprowadzania przez usługi Java nieodwracalnych zmian, które uniemożliwiłyby bezpieczne cofnięcie. Podczas tego procesu program rozruchowy i jądro mogą też wywołać ponowne uruchamianie, jeśli weryfikacja podczas uruchamiania lub dm-verity wykryje jakiekolwiek uszkodzenia. Po zakończeniu sprawdzania update_verifier oznacza, że rozruch zakończył się pomyślnie.

update_verifier będzie odczytywać tylko bloki wymienione w /data/ota_package/care_map.txt, który jest zawarty w pakiecie A/B OTA, gdy używasz kodu AOSP. Klient aktualizacji systemu Java, np. GmsCore, wyodrębnia plik care_map.txt, konfiguruje uprawnienia dostępu przed ponownym uruchomieniem urządzenia i usuwa wyodrębniony plik po uruchomieniu nowej wersji systemu.