Aktualizacje systemu A/B, znane również jako aktualizacje bezproblemowe, zapewniają, że działający system rozruchowy pozostaje na dysku podczas aktualizacji OTA . Takie podejście zmniejsza prawdopodobieństwo nieaktywności urządzenia po aktualizacji, co oznacza mniej wymian urządzeń i ponownego flashowania urządzeń w centrach napraw i gwarancji. Inne systemy operacyjne klasy komercyjnej, 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 pracy systemu, bez przerywania pracy użytkownika. 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 się nie powiedzie (na przykład z powodu złego flashowania), nie wpłynie to 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 uruchomienie się nie powiedzie, urządzenie uruchomi się ponownie z powrotem na starej partycji i nadal będzie 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 powtórzyć. Takie błędy stają się również mniej prawdopodobne, ponieważ obciążenie we/wy jest celowo niskie, aby uniknąć pogorszenia doświadczenia użytkownika.
- Aktualizacje mogą być przesyłane strumieniowo do urządzeń A/B, co eliminuje konieczność pobierania pakietu przed jego zainstalowaniem. Przesyłanie strumieniowe oznacza, że użytkownik nie musi mieć wystarczającej ilości wolnego miejsca do przechowywania pakietu aktualizacji na
/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. (Android Verified Boot nie wymaga aktualizacji A/B).
Informacje 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 Google OTA wszystkie zmiany systemowe są dokonywane w AOSP, a kod klienta jest dostarczany przez usługi Google Play. Producenci OEM, którzy nie korzystają z infrastruktury Google OTA, będą mogli ponownie wykorzystać kod systemowy AOSP, ale będą musieli dostarczyć własnego klienta.
W przypadku producentów OEM dostarczających własnemu klientowi klient musi:
- Zdecyduj, kiedy pobrać 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, oraz w przypadku połączenia Wi-Fi. Jednak twój klient może użyć dowolnej heurystyki.
- Sprawdź na swoich serwerach pakietów OTA i ustal, czy aktualizacja jest dostępna. Powinno to być w większości takie samo, jak 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ć dostępność najnowszej aktualizacji).
- Wywołaj
update_engine
z adresem URL HTTPS dla pakietu aktualizacji, zakładając, że jest 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 wyniku
update_engine
. Jeśli aktualizacja zostanie zastosowana pomyślnie,update_engine
powie programowi ładującemu, aby uruchomił nowy system operacyjny przy następnym uruchomieniu. Program ładujący powróci do starego systemu operacyjnego, jeśli nowy system operacyjny nie uruchomi się, więc klient nie musi wykonywać żadnych czynności. 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 („diff”) pakiet OTA zawodzi i zamiast tego wypróbować pełny pakiet OTA.
Opcjonalnie Klient może:
- Pokaż powiadomienie z prośbą o ponowne uruchomienie. Jeśli chcesz wdrożyć politykę, w której użytkownik jest zachęcany do rutynowej aktualizacji, to powiadomienie można dodać do klienta. Jeśli klient nie monituje użytkowników, 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ż powinni to zrobić, ale powrócili do starej wersji systemu operacyjnego. (Klient Google zazwyczaj nie wykonuje żadnego z nich).
Po stronie systemowej aktualizacje systemu A/B mają wpływ na:
- Wybór partycji (gniazda), demon
update_engine
i interakcje programu ładującego (opisane poniżej) - Proces kompilacji i generowanie pakietów aktualizacji OTA (opisane w Implementowanie aktualizacji A/B )
Wybór partycji (sloty)
Aktualizacje systemu A/B używają dwóch zestawów partycji zwanych gniazdami (zwykle gniazda A i gniazda 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 rezerwowe: jeśli podczas 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, żadna partycja używana przez bieżący slot nie powinna być aktualizowana w ramach aktualizacji OTA (w tym partycje, dla których istnieje tylko jedna kopia).
Każde gniazdo ma atrybut rozruchowy , który określa, czy gniazdo zawiera prawidłowy system, z którego można uruchomić urządzenie. Bieżące gniazdo jest bootowalne, gdy system jest uruchomiony, ale inne gniazdo może zawierać starą (nadal poprawną) wersję systemu, nowszą wersję lub nieprawidłowe dane. Niezależnie od tego, jakie jest obecne gniazdo, jedno gniazdo jest aktywne (to, z którego bootloader uruchomi się przy następnym uruchomieniu) lub preferowane .
Każde gniazdo ma również pomyślny atrybut ustawiony przez przestrzeń użytkownika, co jest istotne tylko wtedy, gdy gniazdo jest również bootowalne. Udany slot powinien być w stanie sam się uruchamiać, uruchamiać i aktualizować. Bootowalne gniazdo, które nie zostało oznaczone jako pomyślne (po kilku próbach uruchomienia z niego) powinno zostać oznaczone jako nieuruchamiane przez program ładujący, w tym zmiana aktywnego gniazda na inne bootowalne (zwykle na gniazdo uruchomione bezpośrednio przed próbą uruchomienia) w nową, aktywną). Konkretne szczegóły interfejsu są zdefiniowane w boot_control.h
.
Zaktualizuj demona silnika
Aktualizacje systemu A/B wykorzystują działającego w tle demona o nazwie update_engine
, aby przygotować system do rozruchu w nowej, zaktualizowanej wersji. Ten demon może wykonywać następujące akcje:
- Odczytaj z bieżących partycji gniazda A/B i zapisz dowolne dane na nieużywanych partycjach gniazda A/B zgodnie z instrukcjami zawartymi w pakiecie 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 gniazd, zgodnie z instrukcją pakietu OTA. (Aby uzyskać szczegółowe informacje, zobacz 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 SELinux w bieżącym gnieździe (takie zasady i funkcje nie mogą być aktualizowane, dopóki system nie uruchomi się w Nowa wersja). Aby zachować solidność systemu, 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 między installd
i menedżera pakietów:
-
frameworks/native/cmds/installd/
ota* zawiera skrypt postinstall, plik binarny dla chroot, klon installd, który wywołuje dex2oat, skrypt post-OTA move-artifacts oraz plik rc dla skryptu move. -
frameworks/base/services/core/java/com/android/server/pm/OtaDexoptService.java
(plusOtaDexoptShellCommand
) to menedżer pakietów, który przygotowuje polecenia dex2oat dla aplikacji.
Aby zapoznać się z działającym przykładem, zobacz /device/google/marlin/device-common.mk
.
Zaktualizuj dzienniki silnika
W przypadku wersji Androida 8.x i wcześniejszych dzienniki update_engine
można znaleźć w logcat
iw raporcie o błędzie. Aby udostępnić dzienniki update_engine
w systemie plików, wprowadź następujące zmiany do swojej kompilacji:
Te zmiany zapisują kopię najnowszego dziennika update_engine
w /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 z identyfikatorem grupy dzienników będą mogli uzyskać 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 się uruchamiać. Typowe przykładowe scenariusze i powiązane z nimi stany obejmują:
- Przypadek normalny : System działa z bieżącego gniazda, A lub B. Do tej pory nie zastosowano żadnych aktualizacji. Bieżące gniazdo systemu jest uruchamialne, pomyślne i aktywne.
- Aktualizacja w toku : system działa z gniazda B, więc gniazdo B jest bootowalnym, udanym i aktywnym gniazdem. 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.
- Aktualizacja zastosowana, ponowne uruchomienie w toku : System jest uruchomiony z gniazda B, gniazdo B jest bootowalne i pomyślne, ale gniazdo A zostało oznaczone jako aktywne (i dlatego jest oznaczone jako bootowalne). Gniazdo A nie zostało jeszcze oznaczone jako udane, a program ładujący powinien wykonać pewną liczbę prób rozruchu z gniazda A.
- System został ponownie uruchomiony w nowej aktualizacji : system jest uruchamiany z gniazda A po raz pierwszy, gniazdo B nadal można uruchomić i pomyślnie, podczas gdy gniazdo A jest tylko bootowalne i nadal jest aktywne, ale nie powiodło się. Demon przestrzeni użytkownika,
update_verifier
, powinien oznaczyć gniazdo A jako udane po przeprowadzeniu pewnych kontroli.
Obsługa aktualizacji strumieniowych
Urządzenia użytkowników nie zawsze mają wystarczającą ilość 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 miejsca na przechowywanie pakietu aktualizacji. Aby rozwiązać ten problem, w systemie Android 8.0 dodano obsługę przesyłania strumieniowego aktualizacji A/B, które zapisują bloki bezpośrednio na partycji B podczas ich pobierania, bez konieczności przechowywania bloków w /data
. Przesyłane strumieniowo aktualizacje A/B nie wymagają prawie żadnej tymczasowej pamięci masowej i wymagają wystarczającej ilości miejsca na około 100 KiB metadanych.
Aby włączyć przesyłanie strumieniowe aktualizacji w systemie Android 7.1, wybierz następujące poprawki:
- Zezwalaj na anulowanie żądania rozwiązania problemu proxy
- Naprawiono przerywanie transferu podczas rozwiązywania serwerów proxy
- Dodaj test jednostkowy dla TerminateTransfer między zakresami
- Oczyść RetryTimeoutCallback()
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 korzystasz z 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ą odroczyć pobieranie ładunku i aplikację na podstawie 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, nieoczekiwanych ponownych uruchomień 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 rozpoznaje, że aktualizacja jest przesyłana strumieniowo (lub zakłada, że wszystkie aktualizacje są przesyłane strumieniowo), a klient wykonuje prawidłowe wywołanie funkcji update_engine
w celu przesyłania strumieniowego. Producenci mogą wykorzystać fakt, że pakiet jest wariantem przesyłania strumieniowego, aby wysłać flagę do klienta, aby wyzwolić przekazanie do strony szkieletu jako przesyłanie strumieniowe.
Po udostępnieniu ładunku proces aktualizacji wygląda następująco:
Krok | Zajęcia |
---|---|
1 | Bieżące gniazdo (lub „miejsce źródłowe”) jest oznaczane jako udane (jeśli nie zostało jeszcze zaznaczone) za pomocą markBootSuccessful() . |
2 | Nieużywane gniazdo (lub „miejsce docelowe”) jest oznaczane jako nie do uruchomienia przez wywołanie funkcji setSlotAsUnbootable() . Bieżące gniazdo jest zawsze oznaczane jako pomyślne na początku aktualizacji, aby program ładujący nie powrócił 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ąć wdrażanie aktualizacji, bieżące gniazdo jest oznaczone jako udane, 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 w celu naprawy tych problemy.Ładunek aktualizacji to nieprzezroczysty obiekt blob z instrukcjami aktualizacji do nowej wersji. Ładunek aktualizacji składa się z następujących elementów:
|
3 | Pobierane są metadane ładunku. |
4 | Dla każdej operacji zdefiniowanej w metadanych, w kolejności, powiązane dane (jeśli istnieją) są ładowane do pamięci, operacja jest wykonywana, a powiązana pamięć jest odrzucana. |
5 | Całe partycje są ponownie odczytywane i weryfikowane pod kątem oczekiwanego skrótu. |
6 | Krok poinstalacyjny (jeśli istnieje) jest uruchamiany. W przypadku błędu podczas wykonywania dowolnego kroku aktualizacja kończy się niepowodzeniem i następuje ponowna próba z prawdopodobnie innym ładunkiem. Jeśli wszystkie dotychczasowe kroki zakończyły się pomyślnie, aktualizacja powiedzie się i zostanie wykonany ostatni krok. |
7 | Nieużywane gniazdo jest oznaczane jako aktywne przez wywołanie metody setActiveBootSlot() . Oznaczenie nieużywanego gniazda jako aktywnego nie oznacza, że zakończy się uruchamianie. Program ładujący (lub sam system) może przełączyć aktywne gniazdo z powrotem, jeśli nie odczyta pomyślnego stanu. |
8 | Post-instalacja (opisana poniżej) polega na uruchomieniu programu z wersji „nowej aktualizacji” przy jednoczesnym działaniu w starej wersji. Jeśli zdefiniowano w pakiecie OTA, ten krok jest obowiązkowy i program musi powrócić z kodem wyjścia 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, bieżące gniazdo (wcześniej „miejsce docelowe”) jest oznaczane jako udane przez wywołanie markBootSuccessful() . |
Po instalacji
Dla każdej partycji, dla której zdefiniowano 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 ustalonej lokalizacji (takiej jak /postinstall_mount
), a /postinstall_mount/usr/bin/postinstall
wykonywane jest polecenie /postinstall_mount/usr/bin/postinstall
.
Aby poinstalacja zakończyła się powodzeniem, stare jądro musi być w stanie:
- Zamontuj nowy format systemu plików . Typ systemu plików nie może się zmienić, jeśli nie ma obsługi tego w starym jądrze, w tym szczegółów, takich jak algorytm kompresji używany w przypadku 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 jądrze 32-bitowym, jeśli architektura została zmieniona z kompilacji 32- na 64-bitową). O ile moduł ładujący (
ld
) nie otrzyma instrukcji użycia innych ścieżek lub zbudowania statycznego pliku binarnego, biblioteki zostaną zał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 z #!
znacznik u góry), a następnie skonfiguruj ścieżki bibliotek z nowego środowiska do wykonywania bardziej złożonego binarnego programu poinstalacyjnego. Alternatywnie możesz uruchomić krok poinstalacyjny 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 przejściowych; pozwoliłoby to użytkownikom na bezpośrednią aktualizację do najnowszej wersji z obrazu fabrycznego.
Nowy program poinstalacyjny jest ograniczony przez zasady SELinux zdefiniowane w starym systemie. W związku z tym krok poinstalacyjny jest odpowiedni do wykonywania zadań wymaganych przez projekt na danym urządzeniu lub innych zadań wymagających jak najlepszej staranności (np. ). Krok poinstalacyjny nie jest odpowiedni do 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 tym nowym systemie. Zmiany atrybutów 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 kontrolę integralności przy użyciu dm-verity. Ta kontrola rozpoczyna się przed zygotą, aby uniknąć wprowadzania przez usługi Java jakichkolwiek nieodwracalnych zmian, które uniemożliwiłyby bezpieczne wycofanie. Podczas tego procesu program ładujący 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ślne uruchomienie.
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, rozpakowuje care_map.txt
, ustawia uprawnienia dostępu przed ponownym uruchomieniem urządzenia i usuwa wyodrębniony plik po pomyślnym uruchomieniu systemu w nowej wersji.