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 em centros de conserto e garantia. Outros sistemas operacionais comerciais, como o ChromeOS, também usam atualizações A/B com sucesso.
Para 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 os dispositivos durante uma atualização OTA. O único tempo de inatividade durante uma atualização é quando o dispositivo é reinicializado na partição de 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 fazer a atualização de novo.
- Se uma atualização OTA for aplicada, mas não for inicializada, o dispositivo será reiniciado na partição antiga e poderá ser usado. O cliente pode tentar atualizar de novo.
- Qualquer erro (como erros de E/S) afeta apenas o conjunto de partições não usadas e pode ser repetido. 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 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 no
/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 ela seja grande o suficiente para atualizações futuras.
- O dm-verity garante que um dispositivo vai inicializar uma imagem não corrompida. Se um dispositivo não for inicializado devido a um problema de OTA ou dm-verity, ele poderá ser reinicializado em uma imagem antiga. (A Inicialização verificada do Android não exige atualizações A/B.)
Sobre as atualizações do sistema A/B
As atualizações A/B exigem mudanças no cliente e no sistema. No entanto, o servidor de pacotes OTA não exige mudanças: os pacotes de atualização ainda são veiculados por HTTPS. Para dispositivos que usam a infraestrutura OTA do Google, todas as mudanças no 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:
- 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 interrupções, recomendamos que as atualizações sejam programadas quando o dispositivo estiver no modo de manutenção inativa, como durante a noite, e conectado ao Wi-Fi. No entanto, seu cliente pode usar qualquer heurística que você quiser.
- Verifique os servidores de pacotes OTA e determine se uma atualização está disponível. Isso deve ser quase igual ao seu código de cliente atual, exceto que você vai querer sinalizar que o dispositivo é compatível com o teste 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 um estiver disponível. Oupdate_engine
vai atualizar os blocos brutos na partição atualmente não utilizada enquanto transmite o pacote de atualização. -
Informe os sucessos ou falhas de instalação aos seus servidores com base no código de resultado
update_engine
. Se a atualização for aplicada com sucesso, oupdate_engine
vai instruir o carregador de inicialização a inicializar o novo SO na próxima reinicialização. O carregador de inicialização vai voltar para o SO antigo se o novo não for inicializado. Portanto, não é necessário fazer nada no cliente. Se a atualização falhar, o cliente precisará 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.
O cliente também pode:
- Mostrar uma notificação pedindo que o usuário reinicie. Se você quiser implementar uma política em que o usuário seja incentivado a atualizar rotineiramente, essa notificação poderá ser adicionada ao seu cliente. Se o cliente não solicitar aos usuários, eles vão receber a atualização na próxima vez que reiniciarem o dispositivo. O cliente do Google tem um atraso configurável por atualização.
- Mostrar uma notificação informando aos usuários se eles iniciaram uma nova versão do SO ou se era esperado que fizessem isso, mas voltaram para a versão antiga do SO. (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 build e geração de pacotes de atualização OTA (descritos em 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 usado 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 usado como um fallback: se ocorrer um erro durante ou imediatamente após uma atualização, o sistema poderá fazer o rollback para o slot antigo e continuar com um sistema funcional. Para atingir essa meta, 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 inicializável que indica se ele contém um sistema correto de onde o dispositivo pode ser inicializado. 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. Independente de qual seja o slot atual, há um slot ativo (aquele de que o carregador de inicialização fará a inicialização na próxima vez) ou o slot preferido.
Cada slot também tem um atributo successful definido pelo espaço do usuário, que é relevante apenas se o slot também for inicializável. Um slot bem-sucedido precisa ser capaz de inicializar, executar e se atualizar. 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 no 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:
- Leia das partições A/B do slot atual e grave dados nas partições A/B do slot não usado 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 da partição nova depois de gravar todas as partições de slot não usadas, 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 tem limitações durante uma atualização devido às 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ção, o conteúdo das partições no slot atual ou o conteúdo das partições não A/B que não podem ser apagadas com uma redefinição de fábrica.
Atualizar origem do mecanismo
A origem update_engine
está localizada em
system/update_engine
. Os arquivos A/B OTA dexopt 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 de movimentação. -
frameworks/base/services/core/java/com/android/server/pm/OtaDexoptService.java
(maisOtaDexoptShellCommand
) é o gerenciador de pacotes que prepara comandos dex2oat para aplicativos.
Para 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, adicione as seguintes mudanças ao build:
- Mudança 486618 (link em inglês)
- Mudança 529080
- Mudança 529081 (link em inglês)
- Mudança 534660
- Mudança 594637 (link em inglês)
Essas mudanças salvam uma cópia do registro de 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 do grupo log poderão acessar os registros do sistema de arquivos.
Interações com o carregador de inicialização
A HAL boot_control
é usada por update_engine
(e possivelmente outros daemons) para instruir o carregador de inicialização sobre o que inicializar. Exemplos comuns de cenários e seus estados associados:
- Caso normal: o sistema está sendo executado no slot atual, A ou B. Nenhuma atualização foi aplicada até o momento. O slot atual do sistema é inicializável, bem-sucedido e ativo.
- Atualização em andamento: o sistema está sendo executado no slot B, que é o slot inicializável, bem-sucedido e ativo. O slot A foi marcado como não inicializável porque o conteúdo dele está sendo atualizado, mas ainda não foi concluído. Uma reinicialização nesse estado deve continuar a inicialização do slot B.
- Atualização aplicada, reinicialização pendente: o sistema está sendo executado no slot B, que pode ser inicializado e foi bem-sucedido, mas o slot A foi marcado como ativo (e, portanto, como inicializável). O slot A ainda não foi marcado como bem-sucedido, e o carregador de inicialização precisa fazer algumas tentativas de inicialização do slot A.
-
O sistema foi reinicializado na 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 o slot A como bem-sucedido depois que algumas verificações forem feitas.
Suporte para 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 não recebem 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 atualizações A/B de streaming que gravam blocos diretamente na partição B à medida que são baixados, 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
espaço suficiente para aproximadamente 100 KiB de metadados.
Para ativar as atualizações de streaming no Android 7.1, faça o cherrypick dos seguintes patches:
- Permitir o cancelamento de uma solicitação de resolução de proxy
- Correção da interrupção de uma transferência ao resolver proxies
- Adicionar teste de unidade para TerminateTransfer entre intervalos
- Limpar o RetryTimeoutCallback()
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, usando os Serviços do Google Mobile (GMS) ou qualquer outro cliente de atualização.
Ciclo de vida 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. 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.
Como opção, 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 transmissão. O servidor pode usar os metadados para
informar ao cliente que ele está fazendo streaming para que o cliente transfira a 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
supondo que todas as atualizações sejam de streaming) e que o cliente faça a chamada correta para
update_engine
para streaming. Os fabricantes podem usar o fato de o pacote ser
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 bem-sucedido (se ainda não estiver marcado) com
markBootSuccessful() .
|
2 |
O slot não usado (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 ao slot não usado,
que logo terá dados inválidos. Se o sistema tiver chegado ao ponto em que pode começar
a aplicar uma atualização, o slot atual será marcado como concluído, mesmo que outros componentes
principais estejam quebrados (como a interface do usuário em um loop de falhas), já que é possível enviar novos
softwares 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:
|
3 | Os metadados do payload são baixados. |
4 | Para cada operação definida nos metadados, na ordem, os dados associados (se houver) são baixados para a memória, a operação é aplicada e a memória associada é descartada. |
5 | Todas as partições são relidas e verificadas com base no hash esperado. |
6 | A etapa pós-instalação (se houver) é executada. Em caso de erro durante a execução de qualquer etapa, a atualização falha e é tentada novamente, possivelmente com uma carga útil diferente. Se todas as etapas até agora forem concluídas, a atualização será bem-sucedida e a última etapa será executada. |
7 |
O slot não usado é marcado como ativo ao chamar setActiveBootSlot() .
Marcar o slot não usado como ativo não significa que ele vai terminar a inicialização. O carregador de inicialização (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
será
obrigatória e o programa precisará retornar com o código de saída 0 ;
caso contrário, a atualização vai falhar.
|
9 |
Depois que o sistema inicializar com sucesso o novo slot e concluir as
verificações pós-reinicialização, o slot atual (antes o "slot de destino") será marcado como
bem-sucedido chamando
markBootSuccessful() .
|
Pós-instalação
Para cada partição em que uma etapa pós-instalação é definida,
o update_engine
monta a nova partição em um local específico e executa o
programa especificado na 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 usado será montada em um local fixo (como
/postinstall_mount
) e o
comando /postinstall_mount/usr/bin/postinstall
será executado.
Para que a pós-instalação seja bem-sucedida, o kernel antigo precisa:
- 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 um sistema de arquivos compactado (ou seja, SquashFS) estiver sendo usado.
-
Entenda o novo formato do programa pós-instalação da partição. Se você estiver usando um binário no formato executável e vinculável (ELF), ele precisará ser compatível com o kernel antigo (por exemplo, um novo programa de 64 bits executado 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 do sistema antigo, não da nova.
Por exemplo, você pode usar um script shell como um programa pós-instalação interpretado pelo binário 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. Outra opção é 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 principal do sistema seja atualizado sem problemas de compatibilidade com versões anteriores ou atualizações incrementais. Isso permitiria que os usuários atualizassem diretamente para a versão mais recente de uma imagem de fábrica.
O novo programa pós-instalação é limitado pelas políticas do SELinux definidas no sistema antigo. Assim, a etapa pós-instalação é adequada para realizar tarefas exigidas por 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 imprevistas.
O programa selecionado de pós-instalação é executado no contexto do SELinux postinstall
. Todos os arquivos na nova partição montada serão
marcados com postinstall_file
, independentemente dos atributos deles após
a reinicialização no novo sistema. As mudanças nos atributos do SELinux no novo sistema não
afetam a etapa pós-instalação. Se o programa pós-instalação precisar de permissões extras, elas deverão
ser adicionadas ao contexto pós-instalação.
Após a reinicialização
Após a reinicialização, 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 um rollback seguro. 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 for concluída, o
update_verifier
vai marcar a inicialização como bem-sucedida.
O update_verifier
vai ler apenas os blocos listados em
/data/ota_package/care_map.txt
, que é incluído em um pacote OTA A/B ao
usar o código 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 é inicializado com sucesso na nova versão.