Actualizaciones del sistema A/B (continuas)

Las actualizaciones del sistema A/B, también conocidas como actualizaciones continuas, garantizan que un sistema de arranque funcional permanezca en el disco durante una actualización inalámbrica (OTA) . Este enfoque reduce la probabilidad de que un dispositivo esté inactivo después de una actualización, lo que significa menos reemplazos de dispositivos y actualizaciones de dispositivos en los centros de reparación y garantía. Otros sistemas operativos de nivel comercial, como ChromeOS , también usan actualizaciones A/B con éxito.

Para obtener más información sobre las actualizaciones del sistema A/B y cómo funcionan, consulte Selección de particiones (ranuras) .

Las actualizaciones del sistema A/B brindan los siguientes beneficios:

  • Las actualizaciones OTA pueden ocurrir mientras el sistema está funcionando, sin interrumpir al usuario. Los usuarios pueden continuar usando sus dispositivos durante una OTA; el único tiempo de inactividad durante una actualización es cuando el dispositivo se reinicia en la partición de disco actualizada.
  • Después de una actualización, el reinicio no lleva más tiempo que un reinicio normal.
  • Si una OTA no se aplica (por ejemplo, debido a un mal flash), el usuario no se verá afectado. El usuario continuará ejecutando el sistema operativo anterior y el cliente puede volver a intentar la actualización.
  • Si se aplica una actualización OTA pero no se inicia, el dispositivo se reiniciará en la partición anterior y seguirá usable. El cliente es libre de volver a intentar la actualización.
  • Cualquier error (como los errores de E/S) afecta solo al conjunto de particiones no utilizado y se puede volver a intentar. Dichos errores también se vuelven menos probables porque la carga de E/S es deliberadamente baja para evitar degradar la experiencia del usuario.
  • Las actualizaciones se pueden transmitir a dispositivos A/B, lo que elimina la necesidad de descargar el paquete antes de instalarlo. Streaming significa que no es necesario que el usuario tenga suficiente espacio libre para almacenar el paquete de actualización en /data o /cache .
  • La partición de caché ya no se usa para almacenar paquetes de actualización OTA, por lo que no es necesario asegurarse de que la partición de caché sea lo suficientemente grande para futuras actualizaciones.
  • dm-verity garantiza que un dispositivo arrancará una imagen no dañada. Si un dispositivo no arranca debido a un problema de OTA o dm-verity incorrecto, el dispositivo puede reiniciarse con una imagen antigua. (Android Verified Boot no requiere actualizaciones A/B).

Acerca de las actualizaciones del sistema A/B

Las actualizaciones A/B requieren cambios tanto en el cliente como en el sistema. Sin embargo, el servidor de paquetes OTA no debería requerir cambios: los paquetes de actualización aún se sirven a través de HTTPS. Para los dispositivos que utilizan la infraestructura OTA de Google, todos los cambios del sistema están en AOSP y los servicios de Google Play proporcionan el código de cliente. Los OEM que no utilicen la infraestructura OTA de Google podrán reutilizar el código del sistema AOSP, pero deberán proporcionar su propio cliente.

Para los OEM que suministran a su propio cliente, el cliente debe:

  • Decide cuándo tomar una actualización. Debido a que las actualizaciones A/B ocurren en segundo plano, ya no las inicia el usuario. Para evitar interrumpir a los usuarios, se recomienda que las actualizaciones se programen cuando el dispositivo esté en modo de mantenimiento inactivo, como durante la noche, y con Wi-Fi. Sin embargo, su cliente puede usar cualquier heurística que desee.
  • Verifique con sus servidores de paquetes OTA y determine si hay una actualización disponible. Esto debería ser casi igual que su código de cliente existente, excepto que querrá indicar que el dispositivo es compatible con A/B. (El cliente de Google también incluye un botón Verificar ahora para que los usuarios busquen la última actualización).
  • Llame a update_engine con la URL HTTPS para su paquete de actualización, suponiendo que haya uno disponible. update_engine actualizará los bloques sin procesar en la partición actualmente no utilizada a medida que transmite el paquete de actualización.
  • Informe los éxitos o errores de instalación a sus servidores, según el código de resultado de update_engine . Si la actualización se aplica con éxito, update_engine le indicará al cargador de arranque que inicie el nuevo sistema operativo en el próximo reinicio. El cargador de arranque volverá al sistema operativo anterior si el nuevo sistema operativo no se inicia, por lo que no se requiere ningún trabajo por parte del cliente. Si la actualización falla, el cliente debe decidir cuándo (y si) volver a intentarlo, según el código de error detallado. Por ejemplo, un buen cliente podría reconocer que un paquete OTA parcial ("diff") falla y probar un paquete OTA completo en su lugar.

Opcionalmente, el cliente puede:

  • Muestra una notificación que le pide al usuario que reinicie. Si desea implementar una política en la que se aliente al usuario a actualizar periódicamente, puede agregar esta notificación a su cliente. Si el cliente no pregunta a los usuarios, los usuarios obtendrán la actualización la próxima vez que reinicien de todos modos. (El cliente de Google tiene un retraso configurable por actualización).
  • Muestre una notificación que indique a los usuarios si iniciaron una nueva versión del sistema operativo o si se esperaba que lo hicieran pero volvieron a la versión anterior del sistema operativo. (El cliente de Google normalmente no hace ninguna de las dos).

En el lado del sistema, las actualizaciones del sistema A/B afectan lo siguiente:

  • Selección de particiones (ranuras), el daemon update_engine y las interacciones del gestor de arranque (descrito a continuación)
  • Proceso de compilación y generación de paquetes de actualización OTA (descrito en Implementación de actualizaciones A/B )

Selección de partición (ranuras)

Las actualizaciones del sistema A/B utilizan dos conjuntos de particiones denominadas ranuras (normalmente ranura A y ranura B). El sistema se ejecuta desde la ranura actual , mientras que el sistema en ejecución no accede a las particiones en la ranura no utilizada durante el funcionamiento normal. Este enfoque hace que las actualizaciones sean resistentes a fallas al mantener la ranura no utilizada como alternativa: si ocurre un error durante o inmediatamente después de una actualización, el sistema puede volver a la ranura anterior y continuar teniendo un sistema en funcionamiento. Para lograr este objetivo, ninguna partición utilizada por la ranura actual debe actualizarse como parte de la actualización OTA (incluidas las particiones para las que solo hay una copia).

Cada ranura tiene un atributo de arranque que indica si la ranura contiene un sistema correcto desde el que puede arrancar el dispositivo. La ranura actual se puede iniciar cuando el sistema se está ejecutando, pero la otra ranura puede tener una versión anterior (todavía correcta) del sistema, una versión más nueva o datos no válidos. Independientemente de cuál sea la ranura actual , hay una ranura que es la ranura activa (aquella en la que arrancará el cargador de arranque en el siguiente arranque) o la ranura preferida .

Cada ranura también tiene un atributo exitoso establecido por el espacio del usuario, que es relevante solo si la ranura también se puede iniciar. Una ranura exitosa debe poder iniciarse, ejecutarse y actualizarse por sí misma. Una ranura de arranque que no se marcó como exitosa (después de varios intentos de arrancar desde ella) debe ser marcada como no arrancable por el gestor de arranque, incluido el cambio de la ranura activa a otra ranura de arranque (normalmente a la ranura que se ejecuta inmediatamente antes del intento de arranque). en el nuevo, activo). Los detalles específicos de la interfaz se definen en boot_control.h .

Actualizar el demonio del motor

Las actualizaciones del sistema A/B utilizan un demonio en segundo plano llamado update_engine para preparar el sistema para iniciar una versión nueva y actualizada. Este demonio puede realizar las siguientes acciones:

  • Lea de las particiones de ranura A/B actuales y escriba cualquier dato en las particiones de ranura A/B no utilizadas según las instrucciones del paquete OTA.
  • Llame a la interfaz boot_control en un flujo de trabajo predefinido.
  • Ejecute un programa posterior a la instalación desde la nueva partición después de escribir todas las particiones de ranura no utilizadas, según las instrucciones del paquete OTA. (Para más detalles, consulte Posterior a la instalación ).

Como el daemon update_engine no está involucrado en el proceso de arranque en sí, está limitado en lo que puede hacer durante una actualización por las políticas y funciones de SELinux en la ranura actual (dichas políticas y funciones no pueden actualizarse hasta que el sistema arranque en un nueva versión). Para mantener un sistema sólido, el proceso de actualización no debe modificar la tabla de particiones, el contenido de las particiones en la ranura actual o el contenido de las particiones que no son A/B que no se pueden borrar con un restablecimiento de fábrica.

Actualizar la fuente del motor

La fuente update_engine se encuentra en system/update_engine . Los archivos dexopt A/B OTA se dividen entre installd y un administrador de paquetes:

Para ver un ejemplo funcional, consulte /device/google/marlin/device-common.mk .

Actualizar registros del motor

Para las versiones de Android 8.x y anteriores, los registros de update_engine se pueden encontrar en logcat y en el informe de errores. Para que los registros de update_engine estén disponibles en el sistema de archivos, parchee los siguientes cambios en su compilación:

Estos cambios guardan una copia del registro update_engine más reciente en /data/misc/update_engine_log/update_engine. YEAR - TIME . Además del registro actual, los cinco registros más recientes se guardan en /data/misc/update_engine_log/ . Los usuarios con el ID de grupo de registros podrán acceder a los registros del sistema de archivos.

Interacciones del cargador de arranque

El boot_control HAL es utilizado por update_engine (y posiblemente otros demonios) para indicarle al cargador de arranque desde dónde arrancar. Los escenarios de ejemplo comunes y sus estados asociados incluyen lo siguiente:

  • Caso normal : el sistema se está ejecutando desde su ranura actual, ya sea la ranura A o la B. Hasta el momento no se han aplicado actualizaciones. La ranura actual del sistema es arrancable, exitosa y la ranura activa.
  • Actualización en curso : el sistema se está ejecutando desde la ranura B, por lo que la ranura B es la ranura de arranque, correcta y activa. La ranura A se marcó como no arrancable porque el contenido de la ranura A se está actualizando pero aún no se ha completado. Un reinicio en este estado debería continuar arrancando desde la ranura B.
  • Actualización aplicada, reinicio pendiente : el sistema se está ejecutando desde la ranura B, la ranura B es arrancable y exitosa, pero la ranura A se marcó como activa (y por lo tanto está marcada como arrancable). La ranura A aún no está marcada como satisfactoria y el gestor de arranque debe realizar algunos intentos de arranque desde la ranura A.
  • El sistema se reinició en una nueva actualización : el sistema se está ejecutando desde la ranura A por primera vez, la ranura B todavía se puede iniciar y funciona correctamente, mientras que la ranura A solo se puede iniciar y sigue activa pero no correctamente. Un daemon de espacio de usuario, update_verifier , debe marcar la ranura A como satisfactoria después de realizar algunas comprobaciones.

Soporte de actualización de transmisión

Los dispositivos de los usuarios no siempre tienen suficiente espacio en /data para descargar el paquete de actualización. Como ni los OEM ni los usuarios quieren desperdiciar espacio en una partición /cache , algunos usuarios se quedan sin actualizaciones porque el dispositivo no tiene dónde almacenar el paquete de actualización. Para solucionar este problema, Android 8.0 agregó soporte para transmitir actualizaciones A/B que escriben bloques directamente en la partición B a medida que se descargan, sin tener que almacenar los bloques en /data . Las actualizaciones de transmisión A/B casi no necesitan almacenamiento temporal y requieren solo el almacenamiento suficiente para aproximadamente 100 KiB de metadatos.

Para habilitar las actualizaciones de transmisión en Android 7.1, elija los siguientes parches:

Estos parches son necesarios para admitir la transmisión de actualizaciones A/B en Android 7.1 y versiones posteriores, ya sea que utilice Google Mobile Services (GMS) o cualquier otro cliente de actualización.

Vida de una actualización A/B

El proceso de actualización comienza cuando un paquete OTA (al que se hace referencia en el código como carga útil ) está disponible para descargar. Las políticas en el dispositivo pueden diferir la descarga de la carga útil y la aplicación según el nivel de la batería, la actividad del usuario, el estado de carga u otras políticas. Además, dado que la actualización se ejecuta en segundo plano, es posible que los usuarios no sepan que hay una actualización en curso. Todo esto significa que el proceso de actualización puede interrumpirse en cualquier momento debido a políticas, reinicios inesperados o acciones del usuario.

Opcionalmente, los metadatos en el propio paquete OTA indican que la actualización se puede transmitir; el mismo paquete también se puede usar para la instalación sin transmisión. El servidor puede usar los metadatos para decirle al cliente que está transmitiendo para que el cliente entregue la OTA a update_engine correctamente. Los fabricantes de dispositivos con su propio servidor y cliente pueden habilitar la transmisión de actualizaciones asegurándose de que el servidor identifique que la actualización se está transmitiendo (o asume que todas las actualizaciones se están transmitiendo) y el cliente realiza la llamada correcta a update_engine para la transmisión. Los fabricantes pueden usar el hecho de que el paquete es de la variante de transmisión para enviar una bandera al cliente para activar la transferencia al lado del marco como transmisión.

Una vez que una carga útil está disponible, el proceso de actualización es el siguiente:

Paso Actividades
1 La ranura actual (o "ranura de origen") se marca como exitosa (si aún no está marcada) con markBootSuccessful() .
2 La ranura no utilizada (o "ranura de destino") se marca como no arrancable llamando a la función setSlotAsUnbootable() . La ranura actual siempre se marca como exitosa al comienzo de la actualización para evitar que el cargador de arranque vuelva a la ranura no utilizada, que pronto tendrá datos no válidos. Si el sistema ha llegado al punto en el que puede comenzar a aplicar una actualización, la ranura actual se marca como exitosa incluso si otros componentes principales están dañados (como la interfaz de usuario en un bucle de bloqueo), ya que es posible impulsar un nuevo software para solucionar estos problemas. problemas.

La carga útil de actualización es un blob opaco con las instrucciones para actualizar a la nueva versión. La carga útil de actualización consta de lo siguiente:
  • Metadatos . Una porción relativamente pequeña de la carga de actualización, los metadatos contienen una lista de operaciones para producir y verificar la nueva versión en la ranura de destino. Por ejemplo, una operación podría descomprimir un determinado blob y escribirlo en bloques específicos de una partición de destino, o leer desde una partición de origen, aplicar un parche binario y escribir en determinados bloques de una partición de destino.
  • Datos adicionales . Como la mayor parte de la carga útil de actualización, los datos adicionales asociados con las operaciones consisten en el blob comprimido o el parche binario en estos ejemplos.
3 Se descargan los metadatos de la carga útil.
4 Para cada operación definida en los metadatos, en orden, los datos asociados (si los hay) se descargan a la memoria, se aplica la operación y se descarta la memoria asociada.
5 Las particiones completas se vuelven a leer y se verifican con el hash esperado.
6 Se ejecuta el paso posterior a la instalación (si corresponde). En el caso de un error durante la ejecución de cualquier paso, la actualización falla y se vuelve a intentar posiblemente con una carga diferente. Si todos los pasos hasta el momento se han realizado correctamente, la actualización se realiza correctamente y se ejecuta el último paso.
7 La ranura no utilizada se marca como activa llamando a setActiveBootSlot() . Marcar la ranura no utilizada como activa no significa que terminará de arrancar. El gestor de arranque (o el propio sistema) puede volver a cambiar la ranura activa si no lee un estado correcto.
8 La instalación posterior (descrita a continuación) implica ejecutar un programa desde la versión de "nueva actualización" mientras aún se ejecuta en la versión anterior. Si está definido en el paquete OTA, este paso es obligatorio y el programa debe regresar con el código de salida 0 ; de lo contrario, la actualización falla.
9 Una vez que el sistema se inicia correctamente en la nueva ranura y finaliza las comprobaciones posteriores al reinicio, la ranura ahora actual (anteriormente, la "ranura de destino") se marca como exitosa llamando a markBootSuccessful() .

Posterior a la instalación

Para cada partición en la que se define un paso posterior a la instalación, update_engine monta la nueva partición en una ubicación específica y ejecuta el programa especificado en la OTA relativo a la partición montada. Por ejemplo, si el programa posterior a la instalación se define como usr/bin/postinstall en la partición del sistema, esta partición de la ranura no utilizada se montará en una ubicación fija (como /postinstall_mount ) y /postinstall_mount/usr/bin/postinstall se ejecuta el comando /postinstall_mount/usr/bin/postinstall .

Para que la post-instalación tenga éxito, el kernel antiguo debe ser capaz de:

  • Monte el nuevo formato del sistema de archivos . El tipo de sistema de archivos no puede cambiar a menos que haya soporte para él en el kernel anterior, incluidos detalles como el algoritmo de compresión utilizado si se usa un sistema de archivos comprimido (es decir, SquashFS).
  • Comprenda el formato del programa posterior a la instalación de la nueva partición . Si usa un binario de formato ejecutable y vinculable (ELF), debe ser compatible con el núcleo anterior (por ejemplo, un nuevo programa de 64 bits que se ejecuta en un núcleo antiguo de 32 bits si la arquitectura cambió de compilaciones de 32 a 64 bits). A menos que se le indique al cargador ( ld ) que use otras rutas o cree un binario estático, las bibliotecas se cargarán desde la imagen del sistema anterior y no desde la nueva.

Por ejemplo, podría usar un script de shell como un programa posterior a la instalación interpretado por el binario de shell del sistema antiguo con un #! marcador en la parte superior), luego configure las rutas de la biblioteca desde el nuevo entorno para ejecutar un programa binario posterior a la instalación más complejo. Alternativamente, puede ejecutar el paso posterior a la instalación desde una partición más pequeña dedicada para permitir que el formato del sistema de archivos en la partición del sistema principal se actualice sin incurrir en problemas de compatibilidad con versiones anteriores o actualizaciones de trampolín; esto permitiría a los usuarios actualizar directamente a la última versión desde una imagen de fábrica.

El nuevo programa posterior a la instalación está limitado por las políticas de SELinux definidas en el sistema anterior. Como tal, el paso posterior a la instalación es adecuado para realizar las tareas requeridas por el diseño en un dispositivo determinado u otras tareas de mejor esfuerzo (es decir, actualizar el firmware o el cargador de arranque compatible con A/B, preparar copias de bases de datos para la nueva versión, etc. ). El paso posterior a la instalación no es adecuado para corregir errores únicos antes de reiniciar que requieren permisos imprevistos.

El programa posterior a la instalación seleccionado se ejecuta en el contexto posterior a la postinstall SELinux. Todos los archivos en la nueva partición montada se etiquetarán con postinstall_file , independientemente de cuáles sean sus atributos después de reiniciar en ese nuevo sistema. Los cambios en los atributos de SELinux en el nuevo sistema no afectarán el paso posterior a la instalación. Si el programa posterior a la instalación necesita permisos adicionales, se deben agregar al contexto posterior a la instalación.

Después de reiniciar

Después de reiniciar, update_verifier activa la verificación de integridad mediante dm-verity. Esta comprobación comienza antes del cigoto para evitar que los servicios de Java realicen cambios irreversibles que impidan una reversión segura. Durante este proceso, el cargador de arranque y el kernel también pueden desencadenar un reinicio si el arranque verificado o dm-verity detectan algún daño. Una vez completada la verificación, update_verifier marca el arranque como exitoso.

update_verifier leerá solo los bloques enumerados en /data/ota_package/care_map.txt , que se incluye en un paquete A/B OTA cuando se usa el código AOSP. El cliente de actualización del sistema Java, como GmsCore, extrae care_map.txt , establece el permiso de acceso antes de reiniciar el dispositivo y elimina el archivo extraído después de que el sistema se inicia correctamente en la nueva versión.