Aggiornamenti di sistema A/B (seamless)

Gli aggiornamenti di sistema A/B legacy, noti anche come aggiornamenti senza interruzioni , garantiscono che un sistema di avvio funzionante rimanga sul disco durante un aggiornamento over-the-air (OTA). Questo approccio riduce la probabilità di un dispositivo inattivo dopo un aggiornamento, il che significa meno sostituzioni e ripristini dei dispositivi presso i centri di riparazione e garanzia. Anche altri sistemi operativi di livello commerciale, come ChromeOS, utilizzano gli aggiornamenti A/B con successo.

Per saperne di più sugli aggiornamenti di sistema A/B e sul loro funzionamento, vedi Selezione della partizione (slot).

Gli aggiornamenti di sistema A/B offrono i seguenti vantaggi:

  • Gli aggiornamenti OTA possono verificarsi mentre il sistema è in esecuzione, senza interrompere l'utente. Gli utenti possono continuare a utilizzare i propri dispositivi durante un aggiornamento OTA. L'unico periodo di inattività durante un aggiornamento è quando il dispositivo si riavvia nella partizione del disco aggiornata.
  • Dopo un aggiornamento, il riavvio non richiede più tempo di un riavvio normale.
  • Se un aggiornamento OTA non viene applicato (ad esempio a causa di un flash errato), l'utente non ne risentirà. L'utente continuerà a eseguire il vecchio sistema operativo e il client è libero di riprovare l'aggiornamento.
  • Se un aggiornamento OTA viene applicato ma non viene avviato, il dispositivo si riavvia nella vecchia partizione e rimane utilizzabile. Il client può riprovare l'aggiornamento.
  • Eventuali errori (ad esempio errori I/O) interessano solo il set di partizioni non utilizzato e possono essere riprovati. Inoltre, questi errori diventano meno probabili perché il carico I/O è volutamente basso per evitare di peggiorare l'esperienza utente.
  • Gli aggiornamenti possono essere trasmessi in streaming ai dispositivi A/B, eliminando la necessità di scaricare il pacchetto prima di installarlo. Lo streaming significa che l'utente non deve disporre di spazio libero sufficiente per archiviare il pacchetto di aggiornamento su /data o /cache.
  • La partizione della cache non viene più utilizzata per archiviare i pacchetti di aggiornamento OTA, quindi non è necessario assicurarsi che sia sufficientemente grande per gli aggiornamenti futuri.
  • dm-verity garantisce che un dispositivo avvii un'immagine non danneggiata. Se un dispositivo non si avvia a causa di un problema OTA o dm-verity, può riavviarsi in un'immagine precedente. (Android Avvio verificato non richiede aggiornamenti A/B.)

Informazioni sugli aggiornamenti di sistema A/B

Gli aggiornamenti A/B richiedono modifiche sia al client sia al sistema. Il server dei pacchetti OTA, tuttavia, non dovrebbe richiedere modifiche: i pacchetti di aggiornamento vengono ancora pubblicati tramite HTTPS. Per i dispositivi che utilizzano l'infrastruttura OTA di Google, le modifiche al sistema sono tutte in AOSP e il codice client è fornito da Google Play Services. I produttori OEM che non utilizzano l'infrastruttura OTA di Google potranno riutilizzare il codice di sistema AOSP, ma dovranno fornire il proprio client.

Per gli OEM che forniscono il proprio client, quest'ultimo deve:

  • Decidi quando eseguire un aggiornamento. Poiché gli aggiornamenti A/B vengono eseguiti in background, non sono più avviati dall'utente. Per evitare interruzioni per gli utenti, è consigliabile programmare gli aggiornamenti quando il dispositivo è in modalità di manutenzione inattiva, ad esempio di notte e tramite Wi-Fi. Tuttavia, il client può utilizzare l'euristica che preferisci.
  • Controlla i server dei pacchetti OTA e determina se è disponibile un aggiornamento. Questo dovrebbe essere per lo più uguale al codice client esistente, tranne per il fatto che devi segnalare che il dispositivo supporta A/B. Il client di Google include anche un pulsante Controlla ora che consente agli utenti di verificare la disponibilità dell'ultimo aggiornamento.
  • Chiama update_engine con l'URL HTTPS del pacchetto di aggiornamento, se disponibile. update_engine aggiornerà i blocchi non elaborati sulla partizione attualmente inutilizzata durante lo streaming del pacchetto di aggiornamento.
  • Segnala ai tuoi server le installazioni riuscite o non riuscite in base al codice di risultato update_engine. Se l'aggiornamento viene applicato correttamente, update_engine indicherà al bootloader di avviare il nuovo sistema operativo al riavvio successivo. Se il nuovo sistema operativo non si avvia, il bootloader ripristina il vecchio sistema operativo, quindi non è richiesto alcun intervento da parte del client. Se l'aggiornamento non riesce, il client deve decidere quando (e se) riprovare, in base al codice di errore dettagliato. Ad esempio, un buon client potrebbe riconoscere che un pacchetto OTA parziale ("diff") non va a buon fine e provare invece un pacchetto OTA completo.

In via facoltativa, il client può:

  • Mostra una notifica che chiede all'utente di riavviare il dispositivo. Se vuoi implementare un criterio in cui l'utente è invitato a eseguire aggiornamenti di routine, questa notifica può essere aggiunta al tuo client. Se il client non chiede agli utenti, questi riceveranno l'aggiornamento al successivo riavvio. Il client di Google ha un ritardo configurabile per aggiornamento.
  • Mostra una notifica che indica agli utenti se è stato avviato un nuovo sistema operativo o se era previsto che lo facessero, ma è stato ripristinato il sistema operativo precedente. (Il client di Google in genere non fa nessuna delle due cose.)

Dal lato del sistema, gli aggiornamenti di sistema A/B interessano quanto segue:

  • Selezione delle partizioni (slot), il daemon update_engine e le interazioni del bootloader (descritte di seguito)
  • Generazione del pacchetto di aggiornamento OTA e della procedura di build (descritta in Implementazione degli aggiornamenti A/B)

Selezione della partizione (slot)

Gli aggiornamenti del sistema A/B utilizzano due insiemi di partizioni chiamate slot (normalmente slot A e slot B). Il sistema viene eseguito dallo slot current, mentre le partizioni nello slot unused non vengono accessibili dal sistema in esecuzione durante il normale funzionamento. Questo approccio rende gli aggiornamenti resistenti agli errori mantenendo lo slot inutilizzato come fallback: se si verifica un errore durante o immediatamente dopo un aggiornamento, il sistema può eseguire il rollback allo slot precedente e continuare a disporre di un sistema funzionante. Per raggiungere questo obiettivo, nessuna partizione utilizzata dallo slot attuale deve essere aggiornata nell'ambito dell'aggiornamento OTA (incluse le partizioni per le quali esiste una sola copia).

Ogni slot ha un attributo avviabile che indica se lo slot contiene un sistema corretto da cui il dispositivo può essere avviato. Lo slot corrente è avviabile quando il sistema è in esecuzione, ma l'altro slot potrebbe contenere una versione precedente (ma comunque corretta) del sistema, una versione più recente o dati non validi. Indipendentemente dallo slot corrente, esiste uno slot che è lo slot attivo (quello da cui verrà avviato il bootloader al successivo avvio) o lo slot preferito.

Ogni slot ha anche un attributo successful impostato dallo spazio utente, che è pertinente solo se lo slot è anche avviabile. Uno slot riuscito deve essere in grado di avviarsi, funzionare e aggiornarsi autonomamente. Uno slot avviabile che non è stato contrassegnato come riuscito (dopo diversi tentativi di avvio) deve essere contrassegnato come non avviabile dal bootloader, inclusa la modifica dello slot attivo in un altro slot avviabile (normalmente nello slot in esecuzione immediatamente prima del tentativo di avvio nel nuovo slot attivo). I dettagli specifici dell'interfaccia sono definiti in boot_control.h.

Aggiorna il daemon del motore

Gli aggiornamenti di sistema A/B utilizzano un daemon in background chiamato update_engine per preparare il sistema all'avvio in una nuova versione aggiornata. Questo daemon può eseguire le seguenti azioni:

  • Leggi dalle partizioni A/B dello slot corrente e scrivi i dati nelle partizioni A/B dello slot inutilizzato come indicato dal pacchetto OTA.
  • Chiama l'interfaccia boot_control in un flusso di lavoro predefinito.
  • Esegui un programma post-installazione dalla partizione new dopo aver scritto tutte le partizioni di slot inutilizzate, come indicato dal pacchetto OTA. Per maggiori dettagli, vedi Post-installazione.

Poiché il daemon update_engine non è coinvolto nel processo di avvio, le sue azioni durante un aggiornamento sono limitate dalle norme e dalle funzionalità SELinux nello slot corrente (queste norme e funzionalità non possono essere aggiornate finché il sistema non si avvia in una nuova versione). Per mantenere un sistema robusto, la procedura di aggiornamento non deve modificare la tabella delle partizioni, i contenuti delle partizioni nello slot corrente o i contenuti delle partizioni non A/B che non possono essere cancellati con un ripristino dei dati di fabbrica.

Aggiorna origine motore

L'origine update_engine si trova in system/update_engine. I file dexopt OTA A/B sono suddivisi tra installd e un gestore di pacchetti:

Per un esempio funzionante, consulta /device/google/marlin/device-common.mk.

Aggiorna i log del motore

Per le release Android 8.x e precedenti, i log update_engine sono disponibili in logcat e nel report sui bug. Per rendere disponibili i log update_engine nel file system, applica le seguenti modifiche alla build:

Queste modifiche salvano una copia del log update_engine più recente in /data/misc/update_engine_log/update_engine.YEAR-TIME. Oltre al log corrente, gli ultimi cinque log vengono salvati in /data/misc/update_engine_log/. Gli utenti con l'ID gruppo log potranno accedere ai log del file system.

Interazioni con il bootloader

L'HAL boot_control viene utilizzato da update_engine (e possibilmente da altri daemon) per indicare al bootloader da cosa avviare il sistema. Ecco alcuni scenari di esempio comuni e i relativi stati:

  • Caso normale: il sistema è in esecuzione dallo slot corrente, ovvero lo slot A o B. Finora non è stato applicato alcun aggiornamento. Lo slot attuale del sistema è avviabile, riuscito e attivo.
  • Aggiornamento in corso: il sistema è in esecuzione dallo slot B, quindi lo slot B è lo slot avviabile, riuscito e attivo. Lo slot A è stato contrassegnato come non avviabile perché i contenuti dello slot A sono in fase di aggiornamento, ma non sono ancora stati completati. Un riavvio in questo stato dovrebbe continuare l'avvio dallo slot B.
  • Aggiornamento applicato, riavvio in attesa: il sistema è in esecuzione dallo slot B, lo slot B è avviabile e riuscito, ma lo slot A è stato contrassegnato come attivo (e quindi come avviabile). Lo slot A non è ancora contrassegnato come riuscito e il bootloader deve effettuare un certo numero di tentativi di avvio dallo slot A.
  • System rebooted into new update: il sistema viene eseguito dallo slot A per la prima volta, lo slot B è ancora avviabile e riuscito, mentre lo slot A è solo avviabile e ancora attivo, ma non riuscito. Il daemon dello spazio utente, update_verifier, deve contrassegnare lo slot A come riuscito dopo alcuni controlli.

Supporto per gli aggiornamenti dello streaming

I dispositivi degli utenti non sempre hanno spazio sufficiente su /data per scaricare il pacchetto di aggiornamento. Poiché né gli OEM né gli utenti vogliono sprecare spazio su una partizione /cache, alcuni utenti non ricevono aggiornamenti perché il dispositivo non ha spazio per archiviare il pacchetto di aggiornamento. Per risolvere questo problema, Android 8.0 ha aggiunto il supporto per gli aggiornamenti A/B in streaming che scrivono i blocchi direttamente nella partizione B durante il download, senza doverli archiviare su /data. Gli aggiornamenti A/B in streaming non richiedono quasi spazio di archiviazione temporaneo e ne richiedono solo uno spazio sufficiente per circa 100 KiB di metadati.

Per attivare gli aggiornamenti in streaming in Android 7.1, seleziona i seguenti patch:

Queste patch sono necessarie per supportare gli aggiornamenti A/B in streaming in Android 7.1 e versioni successive, indipendentemente dall'utilizzo di Google Mobile Services (GMS) o di qualsiasi altro client di aggiornamento.

Durata di un aggiornamento A/B

La procedura di aggiornamento inizia quando è disponibile per il download un pacchetto OTA (nel codice indicato come payload). I criteri nel dispositivo possono posticipare il download e l'applicazione del payload in base al livello della batteria, all'attività dell'utente, allo stato di ricarica o ad altri criteri. Inoltre, poiché l'aggiornamento viene eseguito in background, gli utenti potrebbero non sapere che è in corso. Tutto ciò significa che il processo di aggiornamento potrebbe essere interrotto in qualsiasi momento a causa di criteri, riavvii imprevisti o azioni dell'utente.

Facoltativamente, i metadati nel pacchetto OTA stesso indicano che l'aggiornamento può essere trasmesso in streaming; lo stesso pacchetto può essere utilizzato anche per l'installazione non in streaming. Il server può utilizzare i metadati per comunicare al client che sta eseguendo lo streaming, in modo che il client trasferisca l'OTA a update_engine correttamente. I produttori di dispositivi con server e client propri possono attivare gli aggiornamenti in streaming assicurandosi che il server identifichi l'aggiornamento come streaming (o presuma che tutti gli aggiornamenti siano in streaming) e che il client effettui la chiamata corretta a update_engine per lo streaming. I produttori possono utilizzare il fatto che il pacchetto è della variante di streaming per inviare un flag al client per attivare il trasferimento al framework lato streaming.

Una volta disponibile un payload, la procedura di aggiornamento è la seguente:

Passaggio Attività
1 Lo slot corrente (o "slot di origine") viene contrassegnato come riuscito (se non è già contrassegnato) con markBootSuccessful().
2 Lo slot inutilizzato (o "slot di destinazione") viene contrassegnato come non avviabile chiamando la funzione setSlotAsUnbootable(). Lo slot corrente viene sempre contrassegnato come riuscito all'inizio dell'aggiornamento per impedire al bootloader di tornare allo slot inutilizzato, che presto conterrà dati non validi. Se il sistema ha raggiunto il punto in cui può iniziare ad applicare un aggiornamento, lo slot corrente viene contrassegnato come riuscito anche se altri componenti principali sono danneggiati (ad esempio l'interfaccia utente in un ciclo di arresti anomali), in quanto è possibile eseguire il push di un nuovo software per risolvere questi problemi.

Il payload di aggiornamento è un blob opaco con le istruzioni per l'aggiornamento alla nuova versione. Il payload dell'aggiornamento è costituito da quanto segue:
  • Metadati. Una parte relativamente piccola del payload di aggiornamento, i metadati contengono un elenco di operazioni per produrre e verificare la nuova versione nello slot di destinazione. Ad esempio, un'operazione potrebbe decomprimere un determinato blob e scriverlo in blocchi specifici di una partizione di destinazione oppure leggere da una partizione di origine, applicare una patch binaria e scrivere in determinati blocchi di una partizione di destinazione.
  • Dati aggiuntivi. Poiché la maggior parte del payload di aggiornamento, i dati aggiuntivi associati alle operazioni sono costituiti dal blob compresso o dalla patch binaria in questi esempi.
3 I metadati del payload vengono scaricati.
4 Per ogni operazione definita nei metadati, nell'ordine, i dati associati (se presenti) vengono scaricati in memoria, l'operazione viene applicata e la memoria associata viene eliminata.
5 Tutte le partizioni vengono rilette e verificate rispetto all'hash previsto.
6 Viene eseguito il passaggio post-installazione (se presente). In caso di errore durante l'esecuzione di un passaggio, l'aggiornamento non va a buon fine e viene ritentato con un payload possibilmente diverso. Se tutti i passaggi finora sono andati a buon fine, l'aggiornamento viene eseguito correttamente e viene eseguito l'ultimo passaggio.
7 Lo slot inutilizzato viene contrassegnato come attivo chiamando setActiveBootSlot(). Se contrassegni lo slot inutilizzato come attivo, non significa che il processo di avvio verrà completato. Il bootloader (o il sistema stesso) può ripristinare lo slot attivo se non legge uno stato di successo.
8 L'installazione successiva (descritta di seguito) prevede l'esecuzione di un programma dalla versione "nuovo aggiornamento" durante l'esecuzione nella versione precedente. Se definito nel pacchetto OTA, questo passaggio è obbligatorio e il programma deve restituire il codice di uscita 0; in caso contrario, l'aggiornamento non va a buon fine.
9 Dopo che il sistema si avvia correttamente nel nuovo slot e termina i controlli post-riavvio, lo slot ora corrente (precedentemente lo "slot di destinazione") viene contrassegnato come riuscito chiamando markBootSuccessful().

Post-installazione

Per ogni partizione in cui è definito un passaggio post-installazione, update_engine monta la nuova partizione in una posizione specifica ed esegue il programma specificato nell'OTA rispetto alla partizione montata. Ad esempio, se il programma post-installazione è definito come usr/bin/postinstall nella partizione di sistema, questa partizione dallo slot inutilizzato verrà montata in una posizione fissa (ad esempio /postinstall_mount) e il comando /postinstall_mount/usr/bin/postinstall viene eseguito.

Perché l'installazione post-installazione vada a buon fine, il vecchio kernel deve essere in grado di:

  • Monta il nuovo formato del file system. Il tipo di file system non può cambiare a meno che non sia supportato nel vecchio kernel, inclusi dettagli come l'algoritmo di compressione utilizzato se si utilizza un file system compresso (ad es. SquashFS).
  • Comprendere il formato del programma post-installazione della nuova partizione. Se utilizzi un binario ELF (Executable and Linkable Format), deve essere compatibile con il vecchio kernel (ad es. un nuovo programma a 64 bit in esecuzione su un vecchio kernel a 32 bit se l'architettura è passata da build a 32 bit a build a 64 bit). A meno che il caricatore (ld) non riceva istruzioni per utilizzare altri percorsi o per creare un binario statico, le librerie verranno caricate dalla vecchia immagine di sistema e non da quella nuova.

Ad esempio, potresti utilizzare uno script shell come programma post-installazione interpretato dal vecchio binario della shell del sistema con un marcatore #! nella parte superiore, quindi configurare i percorsi delle librerie dal nuovo ambiente per eseguire un programma post-installazione binario più complesso. In alternativa, puoi eseguire il passaggio post-installazione da una partizione dedicata più piccola per consentire l'aggiornamento del formato del file system nella partizione di sistema principale senza incorrere in problemi di compatibilità con le versioni precedenti o aggiornamenti intermedi. In questo modo, gli utenti possono eseguire l'aggiornamento direttamente all'ultima versione da un'immagine di fabbrica.

Il nuovo programma post-installazione è limitato dalle norme SELinux definite nel vecchio sistema. Pertanto, il passaggio post-installazione è adatto per eseguire le attività richieste dalla progettazione su un determinato dispositivo o altre attività di tipo best effort. Il passaggio post-installazione non è adatto per correzioni di bug una tantum prima del riavvio che richiedono autorizzazioni impreviste.

Il programma post-installazione selezionato viene eseguito nel contesto SELinux postinstall. Tutti i file nella nuova partizione montata verranno taggati con postinstall_file, indipendentemente dai loro attributi dopo il riavvio nel nuovo sistema. Le modifiche agli attributi SELinux nel nuovo sistema non influiranno sul passaggio post-installazione. Se il programma post-installazione richiede autorizzazioni aggiuntive, queste devono essere aggiunte al contesto post-installazione.

Dopo il riavvio

Dopo il riavvio, update_verifier attiva il controllo dell'integrità utilizzando dm-verity. Questo controllo inizia prima di zygote per evitare che i servizi Java apportino modifiche irreversibili che impedirebbero un rollback sicuro. Durante questo processo, il bootloader e il kernel potrebbero anche attivare un riavvio se l'avvio verificato o dm-verity rilevano un danneggiamento. Al termine del controllo, update_verifier contrassegna l'avvio come riuscito.

update_verifier leggerà solo i blocchi elencati in /data/ota_package/care_map.txt, che è incluso in un pacchetto OTA A/B quando si utilizza il codice AOSP. Il client di aggiornamento del sistema Java, come GmsCore, estrae care_map.txt, configura l'autorizzazione di accesso prima di riavviare il dispositivo ed elimina il file estratto dopo che il sistema si avvia correttamente nella nuova versione.