Las actualizaciones heredadas del sistema A/B, también conocidas como actualizaciones sin interrupciones , 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 quede inactivo después de una actualización, lo que significa que habrá menos reemplazos y reflasheos 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, consulta Selección de particiones (ranuras).
Las actualizaciones del sistema A/B proporcionan los siguientes beneficios:
- Las actualizaciones OTA pueden ocurrir mientras el sistema está en funcionamiento, sin interrumpir al usuario. Los usuarios pueden seguir usando sus dispositivos durante una actualización 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 tarda más que un reinicio normal.
- Si no se puede aplicar una OTA (por ejemplo, debido a una escritura incorrecta en la memoria flash), el usuario no se verá afectado. El usuario seguirá ejecutando el SO anterior, y el cliente podrá volver a intentar la actualización.
- Si se aplica una actualización OTA, pero no se puede iniciar, el dispositivo se reiniciará en la partición anterior y seguirá siendo utilizable. El cliente puede volver a intentar la actualización.
- Cualquier error (como errores de E/S) afecta solo al conjunto de particiones sin usar y se puede volver a intentar. Estos errores también son menos probables porque la carga de E/S es deliberadamente baja para evitar degradar la experiencia del usuario.
-
Las actualizaciones se pueden transmitir a los dispositivos A/B, lo que elimina la necesidad de descargar el paquete antes de instalarlo. La transmisión 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 sea lo suficientemente grande para futuras actualizaciones.
- dm-verity garantiza que un dispositivo arranque una imagen sin daños. Si un dispositivo no se inicia debido a un problema con la actualización inalámbrica o con dm-verity, se puede reiniciar con una imagen anterior. (El inicio verificado de Android no requiere actualizaciones A/B).
Información sobre 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 se siguen publicando a través de HTTPS. En el caso de los dispositivos que usan la infraestructura de OTA de Google, todos los cambios del sistema se encuentran en AOSP, y los Servicios de Google Play proporcionan el código del cliente. Los OEM que no usen la infraestructura de OTA de Google podrán reutilizar el código del sistema de AOSP, pero deberán proporcionar su propio cliente.
En el caso de los OEM que proporcionan su propio cliente, este debe hacer lo siguiente:
- Decide cuándo realizar una actualización. Debido a que las actualizaciones A/B se realizan en segundo plano, ya no las inician los usuarios. Para evitar interrumpir a los usuarios, se recomienda programar las actualizaciones cuando el dispositivo esté en modo de mantenimiento inactivo, por ejemplo, durante la noche y con Wi-Fi. Sin embargo, tu cliente puede usar cualquier heurística que desees.
- Comprueba los servidores de paquetes OTA y determina si hay una actualización disponible. En su mayor parte, debería ser igual al código del cliente existente, excepto que querrás indicar que el dispositivo admite pruebas A/B. (El cliente de Google también incluye un botón Verificar ahora para que los usuarios comprueben si hay actualizaciones recientes).
-
Llama a
update_engine
con la URL HTTPS de tu paquete de actualización, suponiendo que haya uno disponible.update_engine
actualizará los bloques sin procesar en la partición que no se usa actualmente a medida que transmite el paquete de actualización. -
Informa a tus servidores sobre las instalaciones correctas o fallidas según el código de resultado
update_engine
. Si la actualización se aplica correctamente,update_engine
le indicará al cargador de arranque que arranque en el nuevo SO en el próximo reinicio. El cargador de arranque volverá al SO anterior si el nuevo SO no se inicia, por lo que el cliente no necesita realizar ninguna acción. Si falla la actualización, 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 falla un paquete OTA parcial ("diff") y, en su lugar, probar con un paquete OTA completo.
De manera opcional, el cliente puede hacer lo siguiente:
- Mostrar una notificación en la que se le solicite al usuario que reinicie el dispositivo Si deseas implementar una política en la que se recomiende al usuario que realice actualizaciones de rutina, puedes agregar esta notificación a tu cliente. Si el cliente no les solicita a los usuarios que realicen la actualización, estos la recibirán la próxima vez que reinicien el dispositivo. (El cliente de Google tiene una demora configurable por actualización).
- Mostrar una notificación que les indique a los usuarios si iniciaron una nueva versión del SO o si se esperaba que lo hicieran, pero volvieron a la versión anterior del SO (Por lo general, el cliente de Google no hace ninguna de las dos cosas).
En el sistema, las actualizaciones del sistema A/B afectan lo siguiente:
-
Selección de particiones (ranuras), el daemon
update_engine
y las interacciones del cargador de arranque (que se describen a continuación) - Proceso de compilación y generación de paquetes de actualización inalámbrica (se describe en Implementación de actualizaciones A/B)
Selección de particiones (espacios)
Las actualizaciones del sistema A/B usan 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 de la ranura sin usar durante el funcionamiento normal. Este enfoque hace que las actualizaciones sean resistentes a errores, ya que mantiene la ranura no utilizada como respaldo: si se produce un error durante una actualización o inmediatamente después, el sistema puede revertir a la ranura anterior y seguir teniendo un sistema en funcionamiento. Para lograr este objetivo, no se debe actualizar ninguna partición que use la ranura actual como parte de la actualización OTA (incluidas las particiones para las que solo hay una copia).
Cada ranura tiene un atributo bootable que indica si la ranura contiene un sistema correcto desde el cual se puede iniciar el dispositivo. La ranura actual es de arranque cuando el sistema se está ejecutando, pero la otra ranura puede tener una versión anterior (pero correcta) del sistema, una versión más reciente o datos no válidos. Independientemente de cuál sea la ranura actual, hay una ranura que es la ranura activa (la que el cargador de arranque usará en el próximo arranque) o la ranura preferida.
Cada ranura también tiene un atributo successful establecido por el espacio del usuario, que solo es relevante si la ranura también es de arranque. Una ranura exitosa debe poder iniciarse, ejecutarse y actualizarse por sí sola. El cargador de arranque debe marcar como no arrancable una ranura de arranque que no se haya marcado como correcta (después de varios intentos de arrancar desde ella), lo que incluye cambiar la ranura activa a otra ranura de arranque (normalmente, a la ranura que se ejecutaba inmediatamente antes del intento de arrancar en la nueva ranura activa). Los detalles específicos de la interfaz se definen en
boot_control.h
.
Actualiza el daemon del motor
Las actualizaciones del sistema A/B usan un daemon en segundo plano llamado update_engine
para preparar el sistema para que se inicie en una versión nueva y actualizada. Este daemon puede realizar las siguientes acciones:
- Lee las particiones A/B de la ranura actual y escribe los datos en las particiones A/B de la ranura no utilizada según las instrucciones del paquete OTA.
- Llama a la interfaz
boot_control
en un flujo de trabajo predefinido. - Ejecuta un programa de postinstalación desde la partición nueva después de escribir todas las particiones de ranura no utilizadas, según las instrucciones del paquete OTA. (Para obtener más información, consulta Después de la instalación).
Como el daemon 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 funciones de SELinux en la ranura actual (estas políticas y funciones no se pueden actualizar hasta que el sistema arranque en una versión nueva). 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 son A/B y que no se pueden borrar con un restablecimiento de la configuración de fábrica.
Actualiza la fuente del motor
La fuente update_engine
se encuentra en system/update_engine
. Los archivos dexopt de OTA A/B se dividen entre installd
y un administrador de paquetes:
-
frameworks/native/cmds/installd/
ota* incluye la secuencia de comandos posterior a la instalación, el objeto binario para chroot, el clon de installd que llama a dex2oat, la secuencia de comandos de posimplementación de OTA para mover artefactos y el archivo rc para la secuencia de comandos de movimiento. -
frameworks/base/services/core/java/com/android/server/pm/OtaDexoptService.java
(además deOtaDexoptShellCommand
) es el administrador de paquetes que prepara los comandos dex2oat para las aplicaciones.
Para ver un ejemplo funcional, consulta /device/google/marlin/device-common.mk
.
Actualiza los registros del motor
En el caso de 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, aplica los siguientes cambios a tu compilación:
Estos cambios guardan una copia del registro de 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 log podrán acceder a los registros del sistema de archivos.
Interacciones con el bootloader
update_engine
(y posiblemente otros daemons) usan el HAL de boot_control
para indicarle al cargador de arranque desde dónde debe arrancar. A continuación, se incluyen algunos ejemplos de situaciones comunes y sus estados asociados:
- Caso normal: El sistema se ejecuta desde su ranura actual, ya sea la ranura A o la B. Hasta el momento, no se aplicó ninguna actualización. La ranura actual del sistema es de arranque, correcta y activa.
- Actualización en curso: El sistema se ejecuta desde la ranura B, por lo que esta es la ranura activa, correcta y de arranque. La ranura A se marcó como no iniciable, ya que se está actualizando el contenido de la ranura A, pero aún no se completó la actualización. Un reinicio en este estado debería continuar el arranque desde la ranura B.
- Actualización aplicada, reinicio pendiente: El sistema se ejecuta desde la ranura B, la ranura B es de arranque y se ejecutó correctamente, pero la ranura A se marcó como activa (y, por lo tanto, se marcó como de arranque). La ranura A aún no se marcó como correcta y el cargador de arranque debería intentar arrancar desde la ranura A varias veces.
-
El sistema se reinició en la actualización nueva: El sistema se ejecuta desde la ranura A por primera vez, la ranura B sigue siendo de arranque y exitosa, mientras que la ranura A solo es de arranque y sigue activa, pero no es exitosa. Un daemon de espacio del usuario,
update_verifier
, debería marcar la ranura A como exitosa después de realizar algunas verificaciones.
Compatibilidad con actualizaciones 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 no reciben actualizaciones porque el dispositivo no tiene dónde almacenar el paquete de actualización. Para abordar este problema, Android 8.0 agregó compatibilidad con las actualizaciones A/B de transmisión 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 A/B de transmisión no necesitan casi nada de almacenamiento temporal y solo requieren el almacenamiento suficiente para aproximadamente 100 KiB de metadatos.
Para habilitar las actualizaciones de transmisión en Android 7.1, selecciona los siguientes parches:
- Permite cancelar una solicitud de resolución de proxy
- Se corrigió la finalización de una transferencia mientras se resolvían los proxies
- Agrega una prueba de unidades para TerminateTransfer entre rangos
- Limpia RetryTimeoutCallback()
Estos parches son necesarios para admitir actualizaciones A/B de transmisión en Android 7.1 y versiones posteriores, ya sea que se usen los Servicios de Google para dispositivos móviles (GMS) o cualquier otro cliente de actualización.
Ciclo de vida de una actualización de A/B
El proceso de actualización comienza cuando hay disponible un paquete OTA (al que se hace referencia en el código como una carga útil) para descargar. Las políticas del dispositivo pueden diferir la descarga y la aplicación de la carga útil según el nivel de batería, la actividad del usuario, el estado de carga o cualquier otra política. Además, como 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.
De manera opcional, los metadatos del paquete de actualización inalámbrica 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 indicarle al cliente que está transmitiendo, de modo que el cliente entregue la OTA a update_engine
correctamente. Los fabricantes de dispositivos con su propio servidor y cliente pueden habilitar las actualizaciones de transmisión si se aseguran de que el servidor identifique que la actualización se está transmitiendo (o suponga que todas las actualizaciones se transmiten) y de que el cliente realice 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 marca al cliente y activar la transferencia al lado del framework como transmisión.
Una vez que hay una carga útil 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 se marcó) con markBootSuccessful() .
|
2 |
La ranura sin usar (o "ranura de destino") se marca como no iniciable llamando a la función
setSlotAsUnbootable() . La ranura actual siempre se marca como correcta 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 llegó 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 IU en un bucle de fallas), ya que es posible enviar software nuevo para solucionar estos problemas. La carga útil de actualización es un BLOB opaco con las instrucciones para actualizar a la versión nueva. La carga útil de actualización consta de lo siguiente:
|
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 en 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 hay alguno). En caso de que se produzca un error durante la ejecución de cualquier paso, la actualización falla y se vuelve a intentar con una carga útil posiblemente diferente. Si todos los pasos anteriores se completaron correctamente, la actualización se realizará correctamente y se ejecutará el último paso. |
7 |
La ranura sin usar se marca como activa llamando a setActiveBootSlot() .
Marcar la ranura no utilizada como activa no significa que se completará el inicio. El cargador de arranque (o el sistema en sí) puede volver a cambiar la ranura activa si no lee un estado correcto.
|
8 |
La etapa posterior a la instalación (que se describe a continuación) implica ejecutar un programa desde la versión de la "nueva actualización" mientras se sigue ejecutando la versión anterior. Si se define en el paquete OTA, este paso es obligatorio y el programa debe mostrar el código de salida 0 ; de lo contrario, la actualización fallará.
|
9 |
Después de que el sistema se inicia correctamente en la nueva ranura y finaliza las verificaciones posteriores al reinicio, la ranura actual (antes la "ranura de destino") se marca como exitosa llamando a markBootSuccessful() .
|
Después de la instalación
Para cada partición en la que se define un paso posterior a la instalación, update_engine
activa 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 activada. 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 activará en una ubicación fija (como /postinstall_mount
) y se ejecutará el comando /postinstall_mount/usr/bin/postinstall
.
Para que la instalación posterior se realice correctamente, el kernel anterior debe poder hacer lo siguiente:
- Activa el nuevo formato del sistema de archivos. El tipo de sistema de archivos no puede cambiar, a menos que el kernel anterior lo admita, incluidos detalles como el algoritmo de compresión que se usa si se usa un sistema de archivos comprimido (es decir, SquashFS).
-
Comprende el formato del programa posterior a la instalación de la nueva partición. Si se usa un archivo binario de formato ejecutable y vinculable (ELF), debe ser compatible con el kernel anterior (p.ej., un programa nuevo de 64 bits que se ejecuta en un kernel anterior 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 de acceso o que compile un objeto binario estático, las bibliotecas se cargarán desde la imagen del sistema anterior y no desde la nueva.
Por ejemplo, podrías usar una secuencia de comandos de shell como un programa posterior a la instalación interpretado por el shell binario del sistema anterior con un marcador #!
en la parte superior y, luego, configurar rutas de acceso a la biblioteca desde el nuevo entorno para ejecutar un programa binario posterior a la instalación más complejo. Como alternativa, puedes ejecutar el paso posterior a la instalación desde una partición más pequeña dedicada para permitir que se actualice el formato del sistema de archivos en la partición principal del sistema sin generar problemas de compatibilidad con versiones anteriores ni actualizaciones intermedias. Esto permitiría a los usuarios actualizar directamente a la versión más reciente 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. Por lo tanto, el paso posterior a la instalación es adecuado para realizar tareas requeridas por diseño en un dispositivo determinado o cualquier otra tarea de mejor esfuerzo. El paso posterior a la instalación no es adecuado para correcciones de errores únicas antes del reinicio que requieren permisos imprevistos.
El programa seleccionado posterior a la instalación se ejecuta en el contexto de postinstall
SELinux. Todos los archivos de la nueva partición montada se etiquetarán con postinstall_file
, independientemente de cuáles sean sus atributos después de reiniciar el sistema nuevo. 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 se deben agregar al contexto posterior a la instalación.
Después del reinicio
Después de reiniciar, update_verifier
activa la verificación de integridad con dm-verity.
Esta verificación comienza antes de zygote para evitar que los servicios de Java realicen cambios irreversibles que impedirían una reversión segura. Durante este proceso, el bootloader y el kernel también pueden activar un reinicio si el inicio verificado o dm-verity detectan alguna corrupción. Una vez que se completa la verificación, update_verifier
marca el inicio como exitoso.
update_verifier
solo leerá los bloques que se indican en /data/ota_package/care_map.txt
, que se incluye en un paquete OTA de A/B cuando se usa el código de AOSP. El cliente de actualización del sistema de Java, como GmsCore, extrae care_map.txt
, configura el permiso de acceso antes de reiniciar el dispositivo y borra el archivo extraído después de que el sistema se inicia correctamente en la nueva versión.