As atualizações 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 OTA (over-the-air) . Essa abordagem reduz a probabilidade de um dispositivo inativo após uma atualização, o que significa menos substituições de dispositivos 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 obter mais informações sobre 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 seus dispositivos durante uma OTA—o único tempo de inatividade durante uma atualização é quando o dispositivo é reinicializado na partição de disco atualizada.
- Após uma atualização, a reinicialização não demora mais do que uma reinicialização normal.
- Se uma OTA não for aplicada (por exemplo, devido a um flash ruim), o usuário não será afetado. O usuário continuará executando o sistema operacional antigo e o cliente poderá tentar novamente a atualização.
- Se uma atualização OTA for aplicada, mas não inicializar, o dispositivo será reinicializado na partição antiga e permanecerá utilizável. O cliente é livre para tentar novamente a atualização.
- Quaisquer erros (como erros de E/S) afetam apenas o conjunto de partições não utilizado e podem ser repetidos. 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 para dispositivos A/B, eliminando a necessidade de baixar o pacote antes de instalá-lo. 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 há necessidade de garantir que a partição de cache seja grande o suficiente para atualizações futuras.
- dm-verity garante que um dispositivo inicializará uma imagem não corrompida. Se um dispositivo não inicializar devido a um problema de OTA ou dm-verity incorreto, o dispositivo pode reinicializar em uma imagem antiga. (A inicialização verificada do Android não requer atualizações A/B.)
Sobre as atualizações do sistema A/B
As atualizações A/B exigem alterações no cliente e no sistema. O servidor de pacotes OTA, no entanto, não deve exigir alterações: os pacotes de atualização ainda são servidos por HTTPS. Para dispositivos que usam a infraestrutura OTA do Google, as alterações do sistema são todas em AOSP e o código do cliente é fornecido pelos serviços do Google Play. Os OEMs que não usam a infraestrutura OTA do Google poderão reutilizar o código do sistema AOSP, mas precisarão fornecer seu próprio cliente.
Para OEMs que fornecem seu próprio cliente, o cliente precisa:
- Decida 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 a interrupção dos usuários, é recomendável que as atualizações sejam agendadas quando o dispositivo estiver em modo de manutenção ocioso, como durante a noite e em Wi-Fi. No entanto, seu cliente pode usar qualquer heurística que desejar.
- Verifique com seus servidores de pacotes OTA e determine se uma atualização está disponível. Isso deve ser basicamente o mesmo que seu código de cliente existente, exceto que você desejará sinalizar que o dispositivo suporta 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 para seu pacote de atualização, supondo que um esteja disponível.update_engine
atualizará os blocos brutos na partição atualmente não utilizada à medida que transmite o pacote de atualização. - Relate sucessos ou falhas de instalação para seus servidores, com base no código de resultado
update_engine
. Se a atualização for aplicada com sucesso,update_engine
dirá ao bootloader para inicializar no novo sistema operacional na próxima reinicialização. O carregador de inicialização fará fallback para o sistema operacional antigo se o novo sistema operacional falhar ao inicializar, portanto, nenhum trabalho é necessário do 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") falhou e tentar um pacote OTA completo.
Opcionalmente, o cliente pode:
- Mostre uma notificação solicitando que o usuário reinicie. Se você deseja implementar uma política em que o usuário é incentivado a atualizar rotineiramente, essa notificação pode ser adicionada ao seu cliente. Se o cliente não avisar os usuários, os usuários receberão a atualização na próxima vez que reiniciarem. (O cliente do Google tem um atraso configurável por atualização.)
- Mostrar uma notificação informando aos usuários se eles inicializaram em uma nova versão do sistema operacional ou se eles deveriam fazer isso, mas voltaram para a versão antiga do sistema operacional. (O cliente do Google normalmente não faz nenhum dos dois.)
No lado do sistema, as atualizações do sistema 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 compilação e geração de pacote de atualização OTA (descrito em Implementação de atualizações A/B )
Seleção de partição (slots)
As atualizações do sistema A/B usam dois conjuntos de partições denominados slots (normalmente slot A e slot B). O sistema é executado a partir do 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 um fallback: se ocorrer um erro durante ou imediatamente após uma atualização, o sistema pode reverter para o slot antigo e continuar a ter um sistema funcionando. Para atingir esse objetivo, nenhuma partição usada pelo slot atual deve ser atualizada como parte da atualização OTA (incluindo partições para as quais há apenas uma cópia).
Cada slot tem um atributo inicializável que informa se o slot contém um sistema correto a partir do qual o dispositivo pode inicializar. O slot atual é inicializável 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 seja o slot atual , há um slot que é o slot ativo (aquele que o bootloader inicializará na próxima inicialização) ou o slot preferido .
Cada slot também possui um atributo de sucesso definido pelo espaço do usuário, que é relevante apenas se o slot também for inicializável. Um slot bem-sucedido deve ser capaz de inicializar, executar e atualizar-se. Um slot inicializável que não foi marcado como bem-sucedido (após várias tentativas de inicialização a partir dele) deve ser marcado como não inicializável pelo gerenciador de inicialização, incluindo a alteração do slot ativo para outro slot inicializável (normalmente para o slot executado imediatamente antes da tentativa de inicialização no novo e ativo). Os detalhes específicos da interface são definidos em boot_control.h
.
Atualizar 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. Este daemon pode executar as seguintes ações:
- Leia das partições do slot A/B atual e grave quaisquer dados nas partições do slot A/B não utilizadas 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 a partir da nova partição após gravar todas as partições de slot não utilizadas, conforme instruído pelo pacote OTA. (Para obter detalhes, consulte Pós-instalação ).
Como o daemon update_engine
não está envolvido no processo de inicialização em si, ele é limitado no que pode fazer durante uma atualização pelas políticas e recursos do SELinux no slot atual (tais políticas e recursos não podem ser atualizados até que o sistema seja inicializado em um 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 não A/B que não podem ser apagadas com uma redefinição de fábrica.
Atualizar fonte do mecanismo
A fonte update_engine
está localizada em system/update_engine
. Os arquivos dexopt A/B OTA são divididos entre installd
e um gerenciador de pacotes:
-
frameworks/native/cmds/installd/
ota* inclui o script postinstall, o binário para chroot, o clone installd que chama dex2oat, o script post-OTA move-artifacts e o arquivo rc para o script move. -
frameworks/base/services/core/java/com/android/server/pm/OtaDexoptService.java
(maisOtaDexoptShellCommand
) é o gerenciador de pacotes que prepara comandos dex2oat para aplicativos.
Para obter um exemplo funcional, consulte /device/google/marlin/device-common.mk
.
Atualizar registros do mecanismo
Para versões do Android 8.x e anteriores, os logs update_engine
podem ser encontrados no logcat
e no relatório de bugs. Para disponibilizar os logs update_engine
no sistema de arquivos, aplique o patch das seguintes alterações em sua compilação:
Essas alterações salvam uma cópia do log update_engine
mais recente em /data/misc/update_engine_log/update_engine. YEAR - TIME
. Além do log atual, os cinco logs mais recentes são salvos em /data/misc/update_engine_log/
. Os usuários com o ID do grupo de logs poderão acessar os logs do sistema de arquivos.
Interações do carregador de inicialização
O boot_control
HAL é usado pelo update_engine
(e possivelmente outros daemons) para instruir o bootloader a partir do qual inicializar. Cenários de exemplo comuns e seus estados associados incluem o seguinte:
- Caso normal : O sistema está sendo executado a partir de seu slot atual, slot A ou B. Nenhuma atualização foi aplicada até o momento. O slot atual do sistema é inicializável, bem-sucedido e o slot ativo.
- Atualização em andamento : O sistema está sendo executado a partir do slot B, portanto, o slot B é o slot inicializável, bem-sucedido e ativo. O slot A foi marcado como não inicializável, pois o conteúdo do slot A está sendo atualizado, mas ainda não foi concluído. Uma reinicialização neste estado deve continuar inicializando a partir do slot B.
- Atualização aplicada, reinicialização pendente : O sistema está sendo executado a partir do slot B, o slot B é inicializável e bem-sucedido, mas o slot A foi marcado como ativo (e, portanto, está marcado como inicializável). O slot A ainda não está marcado como bem-sucedido e algumas tentativas de inicialização do slot A devem ser feitas pelo carregador de inicialização.
- Sistema reinicializado em nova atualização : O sistema está sendo executado a partir do slot A pela primeira vez, o slot B ainda é inicializável e bem-sucedido, enquanto o slot A é apenas inicializável e ainda ativo, mas sem sucesso. Um daemon de espaço do usuário,
update_verifier
, deve marcar o slot A como bem-sucedido após algumas verificações serem feitas.
Suporte para atualização de streaming
Os dispositivos do usuário nem sempre têm espaço suficiente em /data
para baixar o 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 à medida que são baixados, sem precisar armazenar os blocos em /data
. As atualizações de streaming A/B quase não precisam de armazenamento temporário e exigem apenas armazenamento suficiente para aproximadamente 100 KiB de metadados.
Para habilitar atualizações de streaming no Android 7.1, escolha os seguintes patches:
- Permitir cancelar uma solicitação de resolução de proxy
- Corrigir o encerramento de uma transferência ao resolver proxies
- Adicionar teste de unidade para TerminateTransfer entre intervalos
- Limpe o RetryTimeoutCallback()
Esses patches são necessários para oferecer suporte a atualizações A/B de streaming no Android 7.1 e posterior, seja usando o 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 (referido no código como carga útil ) está disponível para download. As políticas do dispositivo podem adiar o download e o aplicativo da carga útil com base no nível da bateria, atividade do usuário, status de carregamento ou 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. Tudo 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 próprio pacote OTA indicam que a atualização pode ser transmitida; 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á transmitindo para que o cliente entregue o OTA para update_engine
corretamente. Os fabricantes de dispositivos com seu próprio servidor e cliente podem habilitar atualizações de streaming, garantindo que o servidor identifique que a atualização está em streaming (ou suponha que todas as atualizações estejam em streaming) 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 um sinalizador ao cliente para acionar a transferência para o lado da estrutura como streaming.
Depois que uma carga útil estiver disponível, o processo de atualização é o seguinte:
Etapa | Atividades |
---|---|
1 | O slot atual (ou "slot de origem") é marcado como bem-sucedido (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 bootloader volte ao slot não utilizado, que em breve terá dados inválidos. Se o sistema atingiu o ponto em que pode começar a aplicar uma atualização, o slot atual é marcado como bem-sucedido, mesmo se outros componentes principais estiverem quebrados (como a interface do usuário em um loop de falha), pois é possível enviar um novo software para corrigi-los problemas.A carga útil de atualização é um blob opaco com as instruções para atualizar para a nova versão. A carga útil de atualização consiste no seguinte:
|
3 | Os metadados da carga útil são baixados. |
4 | Para cada operação definida nos metadados, na ordem em que os dados associados (se houver) são baixados para a memória, a operação é aplicada e a memória associada é descartada. |
5 | As partições inteiras 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 é tentada novamente com uma carga útil diferente. Se todas as etapas até agora foram bem-sucedidas, a atualização é bem-sucedida e a última etapa é executada. |
7 | O slot não utilizado é marcado como ativo chamando setActiveBootSlot() . Marcar o slot não utilizado como ativo não significa que ele terminará a inicialização. O bootloader (ou o próprio sistema) pode mudar o slot ativo de volta se não ler um estado bem-sucedido. |
8 | A pós-instalação (descrita abaixo) envolve a execução de um programa da versão "nova atualização" enquanto ainda está em execução na versão antiga. Se definido no pacote OTA, esta etapa é obrigatória e o programa deve retornar com o código de saída 0 ; caso contrário, a atualização falhará. | 9 | Depois que o sistema inicializar com sucesso no novo slot e concluir as verificações pós-reinicializaçã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 de 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, esta partição do slot não utilizado será montada em um local fixo (como /postinstall_mount
) e o /postinstall_mount/usr/bin/postinstall
comando /postinstall_mount/usr/bin/postinstall
é executado.
Para que a pós-instalação seja bem-sucedida, o kernel antigo deve ser capaz de:
- Monte o novo formato do sistema de arquivos . O tipo de sistema de arquivos não pode mudar a menos que haja suporte para ele no kernel antigo, incluindo detalhes como o algoritmo de compactação usado se estiver usando um sistema de arquivos compactado (ou seja, SquashFS).
- Entenda o formato do programa de pós-instalação da nova partição . Se estiver usando um binário Executable and Linkable Format (ELF), ele deve ser compatível com o kernel antigo (por exemplo, um novo programa de 64 bits rodando em um kernel antigo de 32 bits se a arquitetura mudou de compilações de 32 para 64 bits). A menos que o carregador (
ld
) seja instruído a usar outros caminhos ou construir 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 de pós-instalação interpretado pelo binário shell do sistema antigo com um #!
marcador na parte superior), em seguida, configure os caminhos da biblioteca do novo ambiente para executar um programa pós-instalação binário mais complexo. Como alternativa, você pode executar a etapa de pós-instalação de uma partição menor dedicada para permitir que o formato do sistema de arquivos na partição principal do sistema seja atualizado sem incorrer em problemas de compatibilidade com versões anteriores ou atualizações de etapas; isso permitiria que os usuários atualizassem diretamente para a versão mais recente de uma imagem de fábrica.
O novo programa de pós-instalação é limitado pelas políticas do SELinux definidas no sistema antigo. Como tal, a etapa de pós-instalação é adequada para executar tarefas exigidas pelo projeto em um determinado dispositivo ou outras tarefas de melhor esforço (ou seja, atualizar o firmware ou carregador de inicialização com capacidade A/B, preparar cópias de bancos de dados para a nova versão etc. ). A etapa de pós-instalação não é adequada para correções de bugs pontuais antes da reinicialização que exigem permissões imprevistas.
O programa de pós-instalação selecionado é executado no contexto de pós-instalação do postinstall
. Todos os arquivos na nova partição montada serão marcados com postinstall_file
, independentemente de quais sejam seus atributos após a reinicialização nesse novo sistema. Alterações nos atributos do SELinux no novo sistema não afetarão a etapa de pós-instalação. Se o programa de pós-instalação precisar de permissões extras, elas devem ser adicionadas ao contexto de pós-instalação.
Após a reinicialização
Após a reinicialização, update_verifier
aciona a verificação de integridade usando dm-verity. Essa verificação começa antes do zigoto para evitar que os serviços Java façam alterações irreversíveis que impeçam uma reversão segura. Durante esse processo, o bootloader e o kernel também podem acionar uma reinicialização se a inicialização verificada ou o dm-verity detectar qualquer corrupção. Após a conclusão da verificação, update_verifier
marca a inicialização como bem-sucedida.
update_verifier
lerá apenas os blocos listados em /data/ota_package/care_map.txt
, que está incluído em um pacote A/B OTA ao usar o código AOSP. O cliente de atualização do sistema Java, como GmsCore, extrai care_map.txt
, configura a permissão de acesso antes de reinicializar o dispositivo e exclui o arquivo extraído após o sistema inicializar com êxito na nova versão.