Atualizações do sistema A/B (ininterruptas)

As atualizações legadas do sistema A/B, também conhecidas como atualizações contínuas , garantem que um sistema de inicialização funcional permaneça no disco durante uma atualização over the air (OTA). Essa abordagem reduz a probabilidade de um dispositivo ficar inativo após uma atualização, o que significa menos substituições e reflashes de dispositivos em centros de reparo e garantia. Outros sistemas operacionais de nível comercial, como o ChromeOS, também usam atualizações A/B com sucesso.

Para mais informações sobre as atualizações do sistema A/B e como elas funcionam, consulte Seleção de partição (slots).

As atualizações do sistema A/B oferecem os seguintes benefícios:

  • As atualizações OTA podem ocorrer enquanto o sistema está em execução, sem interromper o usuário. Os usuários podem continuar usando os dispositivos durante uma OTA. O único tempo de inatividade durante uma atualização é quando o dispositivo é reinicializado na partição do disco atualizada.
  • Depois de uma atualização, a reinicialização não leva mais tempo do que uma reinicialização normal.
  • Se uma OTA não for aplicada (por exemplo, devido a um flash incorreto), o usuário não será afetado. O usuário vai continuar executando o SO antigo, e o cliente pode tentar a atualização novamente.
  • Se uma atualização OTA for aplicada, mas não for inicializada, o dispositivo será reinicializado na partição antiga e continuará utilizável. O cliente pode tentar atualizar novamente.
  • Qualquer erro (como erros de E/S) afeta apenas o conjunto de partições não usado e pode ser tentado novamente. Esses erros também se tornam menos prováveis porque a carga de E/S é deliberadamente baixa para evitar a degradação da experiência do usuário.
  • As atualizações podem ser transmitidas por streaming para dispositivos A/B, eliminando a necessidade de fazer o download do pacote antes da instalação. O streaming significa que não é necessário que o usuário tenha espaço livre suficiente para armazenar o pacote de atualização em /data ou /cache.
  • A partição de cache não é mais usada para armazenar pacotes de atualização OTA. Portanto, não é necessário garantir que a partição de cache seja grande o suficiente para atualizações futuras.
  • O dm-verity garante que um dispositivo inicialize uma imagem não corrompida. Se um dispositivo não inicializar devido a um problema de OTA ou dm-verity, ele poderá ser reinicializado para uma imagem antiga. A Inicialização verificada do Android não requer atualizações A/B.

Sobre as atualizações A/B do sistema

As atualizações A/B exigem mudanças no cliente e no sistema. O servidor de pacotes OTA, no entanto, não precisa de mudanças: os pacotes de atualização ainda são transmitidos por HTTPS. Para dispositivos que usam a infraestrutura OTA do Google, todas as mudanças do sistema estão no AOSP, e o código do cliente é fornecido pelo Google Play Services. Os OEMs que não usam a infraestrutura OTA do Google poderão reutilizar o código do sistema AOSP, mas precisarão fornecer o próprio cliente.

Para OEMs que fornecem o próprio cliente, ele precisa:

  • Decidir quando fazer uma atualização. Como as atualizações A/B acontecem em segundo plano, elas não são mais iniciadas pelo usuário. Para evitar interrupções aos usuários, recomendamos que as atualizações sejam programadas quando o dispositivo estiver no modo de manutenção ocioso, como durante a noite, e no Wi-Fi. No entanto, o cliente pode usar qualquer heurística que quiser.
  • Verifique com os servidores de pacotes OTA se há uma atualização disponível. Ele precisa ser semelhante ao código do cliente atual, exceto que você vai precisar sinalizar que o dispositivo oferece suporte a A/B. O cliente do Google também inclui um botão Verificar agora para que os usuários verifiquem a atualização mais recente.
  • Chame update_engine com o URL HTTPS do pacote de atualização, se houver um disponível. O update_engine vai atualizar os blocos brutos na partição atualmente não utilizada enquanto transmite o pacote de atualização.
  • Informe aos servidores se a instalação foi bem-sucedida ou não com base no código de resultado update_engine. Se a atualização for aplicada, update_engine vai informar ao carregador de inicialização para inicializar o novo SO na próxima reinicialização. O carregador de inicialização vai retornar ao SO antigo se o novo SO não inicializar. Portanto, não é necessário fazer nada no cliente. Se a atualização falhar, o cliente precisa decidir quando (e se) tentar novamente com base no código de erro detalhado. Por exemplo, um bom cliente pode reconhecer que um pacote OTA parcial ("diff") falha e tentar um pacote OTA completo.

Como alternativa, o cliente pode:

  • Mostrar uma notificação pedindo que o usuário reinicie o dispositivo. Se você quiser implementar uma política em que o usuário seja incentivado a fazer atualizações rotineiras, essa notificação poderá ser adicionada ao cliente. Se o cliente não solicitar aos usuários, eles vão receber a atualização na próxima reinicialização. O cliente do Google tem um atraso configurável por atualização.
  • Mostre uma notificação informando aos usuários se eles inicializaram uma nova versão do SO ou se eles deveriam fazer isso, mas voltaram para a versão antiga do SO. O cliente do Google normalmente não faz isso.

No sistema, as atualizações A/B afetam o seguinte:

  • Seleção de partição (slots), o daemon update_engine e interações do carregador de inicialização (descritas abaixo)
  • Processo de build e geração de pacotes de atualização OTA (descrito em Como implementar atualizações A/B)

Seleção de partição (slots)

As atualizações do sistema A/B usam dois conjuntos de partições chamados de slots (normalmente slot A e slot B). O sistema é executado no slot atual, enquanto as partições no slot não utilizado não são acessadas pelo sistema em execução durante a operação normal. Essa abordagem torna as atualizações resistentes a falhas, mantendo o slot não utilizado como substituto: se um erro ocorrer durante ou imediatamente após uma atualização, o sistema poderá reverter para o slot antigo e continuar funcionando. Para alcançar esse objetivo, nenhuma partição usada pelo slot atual pode ser atualizada como parte da atualização OTA, incluindo partições para as quais há apenas uma cópia.

Cada slot tem um atributo bootable que indica se o slot contém um sistema correto para inicialização do dispositivo. O slot atual pode ser inicializado quando o sistema está em execução, mas o outro slot pode ter uma versão antiga (ainda correta) do sistema, uma versão mais recente ou dados inválidos. Independentemente de qual é o slot atual, há um slot que é o ativo (aquele que o carregador de inicialização vai usar na próxima inicialização) ou o preferencial.

Cada slot também tem um atributo successful definido pelo espaço do usuário, que é relevante somente se o slot também for inicializável. Um slot bem-sucedido precisa ser capaz de inicializar, executar e atualizar sozinho. Um slot inicializável que não foi marcado como bem-sucedido (após várias tentativas de inicialização) precisa ser marcado como não inicializável pelo carregador de inicialização, incluindo a mudança do slot ativo para outro slot inicializável (normalmente para o slot em execução imediatamente antes da tentativa de inicialização do novo slot ativo). Os detalhes específicos da interface são definidos em boot_control.h.

Atualizar o daemon do mecanismo

As atualizações do sistema A/B usam um daemon em segundo plano chamado update_engine para preparar o sistema para inicializar em uma versão nova e atualizada. Esse daemon pode realizar as seguintes ações:

  • Ler as partições A/B do slot atual e gravar dados nas partições A/B do slot não utilizado, conforme instruído pelo pacote OTA.
  • Chame a interface boot_control em um fluxo de trabalho predefinido.
  • Execute um programa de pós-instalação na partição nova depois de gravar todas as partições de slot não utilizadas, conforme instruído pelo pacote OTA. Para mais detalhes, consulte Pós-instalação.

Como o daemon update_engine não está envolvido no processo de inicialização, ele é limitado no que pode fazer durante uma atualização pelas políticas e recursos do SELinux no slot atual (essas políticas e recursos não podem ser atualizados até que o sistema seja inicializado em uma nova versão). Para manter um sistema robusto, o processo de atualização não deve modificar a tabela de partições, o conteúdo das partições no slot atual ou o conteúdo de partições que não sejam A/B que não possam ser apagadas com uma redefinição de fábrica.

Atualizar a origem do mecanismo

A origem update_engine está localizada em system/update_engine. Os arquivos dexopt do OTA A/B são divididos entre installd e um gerenciador de pacotes:

Para conferir um exemplo funcional, consulte /device/google/marlin/device-common.mk.

Atualizar registros do mecanismo

Para versões do Android 8.x e anteriores, os registros update_engine podem ser encontrados em logcat e no relatório de bug. Para disponibilizar os registros update_engine no sistema de arquivos, aplique o patch das seguintes mudanças no build:

Essas mudanças salvam uma cópia do registro update_engine mais recente em /data/misc/update_engine_log/update_engine.YEAR-TIME. Além do registro atual, os cinco registros mais recentes são salvos em /data/misc/update_engine_log/. Os usuários com o ID de grupo log podem acessar os registros do sistema de arquivos.

Interações com o carregador de inicialização

O HAL boot_control é usado por update_engine (e possivelmente outros demônios) para instruir o carregador de inicialização sobre o que inicializar. Exemplos de cenários comuns e os estados associados:

  • Caso normal: o sistema está sendo executado no slot atual, A ou B. Nenhuma atualização foi aplicada até agora. O slot atual do sistema pode ser inicializado, é bem-sucedido e é o slot ativo.
  • Atualização em andamento: o sistema está sendo executado no slot B, então o slot B é o slot inicializável, bem-sucedido e ativo. A ranhura A foi marcada como não inicializável, porque o conteúdo dela está sendo atualizado, mas ainda não foi concluído. Uma reinicialização nesse estado precisa continuar a inicialização pelo slot B.
  • Atualização aplicada, reinicialização pendente: o sistema está sendo executado no slot B, o slot B pode ser inicializado e está funcionando, mas o slot A foi marcado como ativo (e, portanto, como inicializável). A slot A ainda não está marcada como concluída, e o carregador de inicialização precisa fazer algumas tentativas de inicialização a partir da slot A.
  • O sistema foi reinicializado para uma nova atualização: o sistema está sendo executado no slot A pela primeira vez. O slot B ainda pode ser inicializado e está funcionando, enquanto o slot A só pode ser inicializado e ainda está ativo, mas não está funcionando. Um daemon de espaço do usuário, update_verifier, precisa marcar a posição A como concluída depois que algumas verificações forem feitas.

Suporte a atualizações de streaming

Os dispositivos dos usuários nem sempre têm espaço suficiente no /data para fazer o download do pacote de atualização. Como nem os OEMs nem os usuários querem desperdiçar espaço em uma partição /cache, alguns usuários ficam sem atualizações porque o dispositivo não tem onde armazenar o pacote de atualização. Para resolver esse problema, o Android 8.0 adicionou suporte para streaming de atualizações A/B que gravam blocos diretamente na partição B conforme são transferidos por download, sem precisar armazenar os blocos em /data. As atualizações A/B de streaming quase não precisam de armazenamento temporário e exigem apenas armazenamento suficiente para cerca de 100 KiB de metadados.

Para ativar as atualizações de streaming no Android 7.1, escolha os seguintes patches:

Esses patches são necessários para oferecer suporte a atualizações A/B de streaming no Android 7.1 e versões mais recentes, seja usando Google Mobile Services (GMS) ou qualquer outro cliente de atualização.

Vida útil de uma atualização A/B

O processo de atualização começa quando um pacote OTA (chamado de payload no código) está disponível para download. As políticas no dispositivo podem adiar o download e a aplicação do payload com base no nível da bateria, na atividade do usuário, no status de carregamento ou em outras políticas. Além disso, como a atualização é executada em segundo plano, os usuários podem não saber que uma atualização está em andamento. Isso significa que o processo de atualização pode ser interrompido a qualquer momento devido a políticas, reinicializações inesperadas ou ações do usuário.

Opcionalmente, os metadados no pacote OTA indicam que a atualização pode ser transmitida por streaming. O mesmo pacote também pode ser usado para instalação sem streaming. O servidor pode usar os metadados para informar ao cliente que está fazendo streaming para que ele transmita o OTA para update_engine corretamente. Os fabricantes de dispositivos com servidor e cliente próprios podem ativar as atualizações de streaming garantindo que o servidor identifique que a atualização está sendo transmitida (ou presumindo que todas as atualizações estão sendo transmitidas) e que o cliente faça a chamada correta para update_engine para streaming. Os fabricantes podem usar o fato de que o pacote é da variante de streaming para enviar uma flag ao cliente e acionar a transferência para o lado do framework como streaming.

Depois que um payload fica disponível, o processo de atualização é o seguinte:

Etapa Atividades
1 O slot atual (ou "slot de origem") é marcado como concluído (se ainda não estiver marcado) com markBootSuccessful().
2 O slot não utilizado (ou "slot de destino") é marcado como não inicializável chamando a função setSlotAsUnbootable(). O slot atual é sempre marcado como bem-sucedido no início da atualização para evitar que o carregador de inicialização volte para o slot não utilizado, que logo terá dados inválidos. Se o sistema tiver alcançado o ponto em que pode começar a aplicar uma atualização, o slot atual será marcado como concluído, mesmo que outros componentes importantes estejam corrompidos (como a interface em um loop de falha), porque é possível enviar um novo software para corrigir esses problemas.

O payload de atualização é um blob opaco com as instruções para atualizar para a nova versão. O payload de atualização consiste no seguinte:
  • Metadados. Uma parte relativamente pequena do payload de atualização, os metadados contêm uma lista de operações para produzir e verificar a nova versão no slot de destino. Por exemplo, uma operação pode descompactar um blob específico e gravá-lo em blocos específicos em uma partição de destino ou ler de uma partição de origem, aplicar um patch binário e gravar em determinados blocos em uma partição de destino.
  • Dados extras. Como a maior parte do payload de atualização, os dados extras associados às operações consistem no blob compactado ou no patch binário nesses exemplos.
3 Os metadados do payload são transferidos por download.
4 Para cada operação definida nos metadados, em ordem, os dados associados (se houver) são transferidos para a memória, a operação é aplicada e a memória associada é descartada.
5 Todas as partições são lidas novamente e verificadas em relação ao hash esperado.
6 A etapa pós-instalação (se houver) é executada. No caso de um erro durante a execução de qualquer etapa, a atualização falha e é repetida com um payload diferente. Se todas as etapas até agora tiverem sido concluídas, a atualização será bem-sucedida e a última etapa será executada.
7 O slot não utilizado é marcado como ativo ao chamar setActiveBootSlot(). Marcar o slot não utilizado como ativo não significa que ele vai terminar a inicialização. O carregador de inicialização (ou o próprio sistema) pode alternar a posição ativa se não ler um estado bem-sucedido.
8 A pós-instalação (descrita abaixo) envolve a execução de um programa da "nova atualização" enquanto ele ainda está sendo executado na versão antiga. Se definida no pacote OTA, essa etapa é obrigatória, e o programa precisa retornar com o código de saída 0. Caso contrário, a atualização falha.
9 Depois que o sistema inicializa o novo slot e termina as verificações pós-inicialização, o slot atual (anteriormente o "slot de destino") é marcado como bem-sucedido chamando markBootSuccessful().

Pós-instalação

Para cada partição em que uma etapa pós-instalação é definida, update_engine monta a nova partição em um local específico e executa o programa especificado no OTA em relação à partição montada. Por exemplo, se o programa pós-instalação for definido como usr/bin/postinstall na partição do sistema, essa partição do slot não utilizado será montada em um local fixo (como /postinstall_mount) e o comando /postinstall_mount/usr/bin/postinstall será executado.

Para que a instalação pós-instalação seja bem-sucedida, o kernel antigo precisa:

  • Montar o novo formato do sistema de arquivos. O tipo de sistema de arquivos não pode ser alterado, a menos que haja suporte a ele no kernel antigo, incluindo detalhes como o algoritmo de compactação usado se estiver usando um sistema de arquivos compactado (por exemplo, SquashFS).
  • Entenda o formato do programa pós-instalação da nova partição. Se estiver usando um binário de formato executável e linkável (ELF), ele precisa ser compatível com o kernel antigo (por exemplo, um novo programa de 64 bits em execução em um kernel antigo de 32 bits, se a arquitetura tiver mudado de builds de 32 para 64 bits). A menos que o carregador (ld) seja instruído a usar outros caminhos ou criar um binário estático, as bibliotecas serão carregadas da imagem antiga do sistema, e não da nova.

Por exemplo, você pode usar um script de shell como um programa pós-instalação interpretado pelo binário de shell do sistema antigo com um marcador #! na parte de cima e, em seguida, configurar caminhos de biblioteca do novo ambiente para executar um programa binário pós-instalação mais complexo. Como alternativa, é possível executar a etapa pós-instalação em uma partição menor dedicada para permitir que o formato do sistema de arquivos na partição do sistema principal seja atualizado sem causar problemas de compatibilidade com versões anteriores ou atualizações de fase. Isso permitiria que os usuários atualizassem diretamente para a versão mais recente usando uma imagem de fábrica.

O novo programa pós-instalação é limitado pelas políticas do SELinux definidas no sistema antigo. Portanto, a etapa pós-instalação é adequada para realizar tarefas exigidas pelo design em um determinado dispositivo ou outras tarefas de melhor esforço. A etapa pós-instalação não é adequada para correções de bugs únicas antes da reinicialização que exigem permissões imprevisíveis.

O programa pós-instalação selecionado é executado no contexto SELinux postinstall. Todos os arquivos na nova partição montada serão marcados com postinstall_file, independentemente dos atributos após a reinicialização nesse novo sistema. As mudanças nos atributos do SELinux no novo sistema não vão afetar a etapa pós-instalação. Se o programa pós-instalação precisar de permissões extras, elas precisarão ser adicionadas ao contexto pós-instalação.

Após a reinicialização

Após a reinicialização, update_verifier aciona a verificação de integridade usando o dm-verity. Essa verificação começa antes do zygote para evitar que os serviços Java façam mudanças irreversíveis que impediriam uma reversão segura. Durante esse processo, o carregador de inicialização e o kernel também podem acionar uma reinicialização se a inicialização verificada ou o dm-verity detectarem alguma corrupção. Depois que a verificação é concluída, update_verifier marca o boot como bem-sucedido.

update_verifier vai ler apenas os blocos listados em /data/ota_package/care_map.txt, que é incluído em um pacote A/B OTA ao usar o código do AOSP. O cliente de atualização do sistema Java, como o GmsCore, extrai care_map.txt, configura a permissão de acesso antes de reiniciar o dispositivo e exclui o arquivo extraído depois que o sistema inicializa a nova versão.