Actualizaciones del sistema A/B (sin interrupciones)

Las actualizaciones del sistema A/B, también conocidas como actualizaciones integradas, garantizan que un sistema de arranque viable permanezca en el disco durante una actualización inalámbrica (OTA) . Este enfoque reduce la probabilidad de que un dispositivo quede inactivo después de una actualización, lo que significa menos reemplazos y actualizaciones de dispositivos en los centros de reparación y garantía. Otros sistemas operativos de nivel comercial, como ChromeOS, también utilizan 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 partición (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 del 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 antiguo y el cliente podrá volver a intentar la actualización.
  • Si se aplica una actualización OTA pero no se inicia, el dispositivo se reiniciará nuevamente en la partición anterior y seguirá siendo utilizable. El cliente es libre de volver a intentar la actualización.
  • Cualquier error (como errores de E/S) afecta sólo al conjunto de particiones no utilizado y se puede volver a intentar. Estos 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, eliminando 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 utiliza 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á con una imagen no dañada. Si un dispositivo no arranca debido a un problema de OTA o dm-verity, el dispositivo puede reiniciarse en una imagen antigua. ( El arranque verificado de Android 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 entregan a través de HTTPS. Para los dispositivos que utilizan la infraestructura OTA de Google, todos los cambios del sistema se realizan en AOSP y el código de cliente lo proporcionan los servicios de Google Play. 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 sus propios clientes, el cliente debe:

  • Decide cuándo realizar una actualización. Como las actualizaciones A/B se realizan en segundo plano, ya no las inicia el usuario. Para evitar molestar a los usuarios, se recomienda programar las actualizaciones cuando el dispositivo esté en modo de mantenimiento inactivo, como durante la noche, y en Wi-Fi. Sin embargo, su cliente puede utilizar cualquier heurística que desee.
  • Consulte con sus servidores de paquetes OTA y determine si hay una actualización disponible. Esto debería ser prácticamente el mismo que su código de cliente existente, excepto que querrá indicar que el dispositivo admite A/B. (El cliente de Google también incluye un botón Verificar ahora para que los usuarios busquen la última actualización).
  • Llame update_engine con la URL HTTPS de su paquete de actualización, suponiendo que haya uno disponible. update_engine actualizará los bloques sin formato en la partición actualmente no utilizada mientras transmite el paquete de actualización.
  • Informe los éxitos o errores de instalación a sus servidores, según el código de resultado update_engine . Si la actualización se aplica correctamente, update_engine le indicará al gestor de arranque que inicie en el nuevo sistema operativo en el próximo reinicio. El gestor de arranque recurrirá al sistema operativo antiguo si el nuevo sistema operativo no arranca, por lo que no es necesario realizar ningún trabajo por parte del cliente. Si la actualización falla, el cliente debe decidir cuándo (y si) intentarlo nuevamente, 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 con un paquete OTA completo en su lugar.

Opcionalmente, el cliente puede:

  • Muestra una notificación pidiendo al usuario que reinicie. Si desea implementar una política en la que se aliente al usuario a actualizar de forma rutinaria, entonces esta notificación se puede agregar a su cliente. Si el cliente no avisa a los usuarios, estos 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 cosas).

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

  • Selección de partición (ranuras), el demonio update_engine y las interacciones del gestor de arranque (descritas a continuación)
  • Proceso de compilación y generación de paquetes de actualización OTA (descritos 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 respaldo: si ocurre un error durante o inmediatamente después de una actualización, el sistema puede retroceder 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 cual el dispositivo puede arrancar. La ranura actual se puede iniciar cuando el sistema se está ejecutando, pero la otra ranura puede tener una versión antigua (aún 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 (desde la que arrancará el gestor 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 es de arranque. Una ranura exitosa debería 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). al nuevo y activo). Los detalles específicos de la interfaz se definen en boot_control.h .

Actualizar demonio del motor

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

  • Lea desde las particiones A/B de la ranura actual y escriba cualquier dato en las particiones A/B de la ranura 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 ranuras no utilizadas, según las instrucciones del paquete OTA. (Para obtener más información, consulte Postinstalación ).

Como el demonio update_engine no participa en el proceso de arranque en sí, está limitado en lo que puede hacer durante una actualización por las políticas y características de SELinux en la ranura actual (dichas políticas y características no se pueden actualizar hasta que el sistema se inicie 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 ni el contenido de las particiones que no sean A/B que no se puedan borrar con un restablecimiento de fábrica.

Actualizar fuente del motor

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

Para ver un ejemplo práctico, consulte /device/google/marlin/device-common.mk .

Actualizar registros del motor

Para las versiones de Android 8.x y anteriores, los registros update_engine se pueden encontrar en logcat y en el informe de errores. Para que los registros 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 del grupo de registros podrán acceder a los registros del sistema de archivos.

Interacciones del cargador de arranque

El HAL boot_control es utilizado por update_engine (y posiblemente por otros demonios) para indicarle al gestor de arranque desde qué arrancar. Los escenarios de ejemplo comunes y sus estados asociados incluyen los siguientes:

  • Caso normal : el sistema se está ejecutando desde su ranura actual, ya sea la ranura A o B. Hasta el momento no se han aplicado actualizaciones. La ranura actual del sistema es de arranque, exitosa y 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, exitosa y activa. La ranura A se marcó como no arrancable ya que 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 se puede iniciar y se realiza correctamente, pero la ranura A se marcó como activa (y, por lo tanto, está marcada como de inicio). La ranura A aún no está marcada como exitosa y el gestor de arranque debe realizar una cierta cantidad de intentos de inicio desde la ranura A.
  • El sistema se reinició con una nueva actualización : el sistema se está ejecutando desde la ranura A por primera vez, la ranura B aún se puede iniciar y es exitosa, mientras que la ranura A solo se puede iniciar y aún está activa pero no se puede iniciar correctamente. Un demonio de espacio de usuario, update_verifier , debe marcar la ranura A como exitosa 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 . La transmisión de actualizaciones A/B casi no necesita almacenamiento temporal y solo requiere almacenamiento suficiente para aproximadamente 100 KiB de metadatos.

Para habilitar las actualizaciones de transmisión en Android 7.1, seleccione 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 se utilicen los servicios móviles de Google (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 (denominado en el código carga útil ) está disponible para descargar. Las políticas del 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, debido a 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 podría 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 utilizar para instalaciones 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 que el cliente realiza la llamada correcta a update_engine para la transmisión. Los fabricantes pueden utilizar el hecho de que el paquete sea de la variante de transmisión para enviar un indicador 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 gestor 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 importantes están rotos (como la interfaz de usuario en un bucle de falla), ya que es posible impulsar un nuevo software para solucionar estos problemas. problemas.

La carga útil de la actualización es un blob opaco con instrucciones para actualizar a la nueva versión. La carga útil de actualización consta de lo siguiente:
  • Metadatos . Los metadatos, una porción relativamente pequeña de la carga útil de la actualización, 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 en una partición de destino, o leer desde una partición de origen, aplicar un parche binario y escribir en ciertos bloques en una partición de destino.
  • Datos adicionales . Como la mayor parte de la carga útil de la 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 Todas las particiones se vuelven a leer y verificar con el hash esperado.
6 Se ejecuta el paso posterior a la instalación (si corresponde). En caso de un error durante la ejecución de cualquier paso, la actualización falla y se vuelve a intentar con posiblemente una carga útil diferente. Si todos los pasos hasta el momento han sido exitosos, 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 iniciarse. El gestor de arranque (o el sistema mismo) puede volver a cambiar la ranura activa si no lee un estado exitoso.
8 La postinstalación (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 se define 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 Después de que el sistema se inicia con éxito en la nueva ranura y finaliza las comprobaciones posteriores al reinicio, la ranura actual (anteriormente la "ranura de destino") se marca como exitosa llamando a markBootSuccessful() .

Posterior a la instalación

Para cada partición donde 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 en relación con 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 postinstalación sea exitosa, el kernel antiguo debe poder:

  • 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 utiliza un binario de formato ejecutable y vinculable (ELF), debería ser compatible con el núcleo antiguo (por ejemplo, un nuevo programa de 64 bits que se ejecuta en un núcleo antiguo de 32 bits si la arquitectura cambió de versiones de 32 a 64 bits). A menos que se indique al cargador ( ld ) que utilice otras rutas o cree un binario estático, las bibliotecas se cargarán desde la imagen anterior del sistema y no desde la nueva.

Por ejemplo, podría utilizar un script de shell como programa posterior a la instalación interpretado por el binario de shell del sistema anterior 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 principal del sistema se actualice sin incurrir en problemas de compatibilidad con versiones anteriores o actualizaciones preliminares; 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 tareas requeridas por el diseño en un dispositivo determinado u otras tareas de mejor esfuerzo (es decir, actualizar el firmware o el gestor de arranque con capacidad 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 correcciones de errores puntuales antes del reinicio que requieren permisos imprevistos.

El programa posterior a la instalación seleccionado se ejecuta en el contexto postinstall de 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, estos deben agregarse al contexto posterior a la instalación.

Después de reiniciar

Después de reiniciar, update_verifier activa la verificación de integridad usando dm-verity. Esta verificación comienza antes del cigoto para evitar que los servicios Java realicen cambios irreversibles que impidan una reversión segura. Durante este proceso, el gestor de arranque y el kernel también pueden provocar un reinicio si el arranque verificado o dm-verity detectan algún daño. Una vez completada la verificación, update_verifier marca el inicio 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 , configura el permiso de acceso antes de reiniciar el dispositivo y elimina el archivo extraído después de que el sistema arranca correctamente en la nueva versión.