Las actualizaciones del sistema A/B heredadas, también conocidas como actualizaciones sin interrupciones , garantizan que un sistema de inicio 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 menos reemplazos y restablecimientos de la memoria flash en los centros de reparación y garantía. Otros sistemas operativos de grado comercial, como ChromeOS, también usan las actualizaciones A/B de forma correcta.
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 se pueden realizar mientras el sistema se está ejecutando, sin interrumpir al usuario. Los usuarios pueden seguir usando sus dispositivos durante una actualización inalámbrica. 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 demora más que un reinicio normal.
- Si no se aplica una actualización OTA (por ejemplo, debido a un error de actualización), el usuario no se verá afectado. El usuario seguirá ejecutando el SO 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á siendo utilizable. El cliente puede volver a intentar la actualización.
- Cualquier error (como los errores de E/S) solo afecta al conjunto de particiones sin usar y se puede volver a intentar. Es menos probable que ocurran estos errores porque la carga de E/S es deliberadamente baja para evitar que se deteriore 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. 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 la partición de caché sea lo suficientemente grande para actualizaciones futuras.
- dm-verity garantiza que un dispositivo arranque una imagen sin daños. Si un dispositivo no se inicia debido a un problema de actualización inalámbrica o de dm-verity, puede reiniciarse en una imagen anterior. (El inicio verificado de Android no requiere actualizaciones A/B).
Información acerca de las actualizaciones del sistema A/B
Las actualizaciones A/B requieren cambios en el cliente y 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. En el caso de los dispositivos que usan la infraestructura OTA de Google, los cambios del sistema están todos en AOSP, y los Servicios de Google Play proporcionan el código del cliente. Los OEMs que no usen la infraestructura de OTA de Google podrán volver a usar el código del sistema 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 tomar una actualización. Debido a que las actualizaciones A/B se realizan 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, por ejemplo, durante la noche, y con Wi-Fi. Sin embargo, tu cliente puede usar cualquier heurística que desees.
- Consulta tus servidores de paquetes OTA y determina si hay una actualización disponible. Esto debería ser, en su mayoría, igual que tu código de cliente existente, excepto que querrás indicar que el dispositivo admite A/B. (El cliente de Google también incluye un botón Verificar ahora para que los usuarios verifiquen la actualización más reciente).
-
Llama a
update_engine
con la URL HTTPS de tu paquete de actualización, siempre que haya uno disponible.update_engine
actualizará los bloques sin procesar en la partición que no se está usando mientras transmite el paquete de actualización. -
Informa los éxitos o las fallas de la instalación a tus servidores, según el código de resultado
update_engine
. Si la actualización se aplica correctamente,update_engine
le indicará al bootloader que inicie el nuevo SO en el próximo reinicio. El bootloader recurrirá al SO anterior si el nuevo no se inicia, por lo que no se requiere ningún trabajo 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 falla un paquete OTA parcial ("diff") y, en su lugar, intentar un paquete OTA completo.
De manera opcional, el cliente puede hacer lo siguiente:
- Muestra 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 le aliente al usuario a actualizar de forma periódica, esta notificación se puede agregar a tu cliente. Si el cliente no les solicita a los usuarios que la realicen, estos recibirán la actualización la próxima vez que reinicien el dispositivo. (El cliente de Google tiene un retraso configurable por actualización).
- Muestra una notificación que les indique a los usuarios si se inició una versión nueva del SO o si se esperaba que lo hicieran, pero se volvió a la versión anterior. (por lo general, el cliente de Google no hace ninguna de estas acciones).
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 bootloader (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 denominados ranuras (por lo general, ranura A y ranura B). El sistema se ejecuta desde el puerto actual, mientras que el sistema en ejecución no accede a las particiones del puerto sin usar durante el funcionamiento normal. Este enfoque hace que las actualizaciones sean resistentes a fallas, ya que mantiene el zócalo sin usar como resguardo: si se produce un error durante una actualización o inmediatamente después de ella, el sistema puede revertir al zócalo anterior y seguir teniendo un sistema en funcionamiento. Para lograr este objetivo, no se debe actualizar ninguna partición que use el espacio actual como parte de la actualización OTA (incluidas las particiones de las que solo hay una copia).
Cada ranura tiene un atributo bootable que indica si contiene un sistema correcto desde el que se puede iniciar el dispositivo. El zócalo actual se puede iniciar cuando el sistema está en funcionamiento, pero el otro zócalo puede tener una versión antigua (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 activa (la que se iniciará desde el bootloader en el próximo inicio) o la preferida.
Cada ranura también tiene un atributo correcto establecido por el espacio de usuario, que es relevante solo si la ranura también se puede iniciar. Un slot exitoso debería poder iniciarse, ejecutarse y actualizarse por sí solo. El bootloader debe marcar como no inicializable un espacio de inicio que no se haya marcado como correcto (después de varios intentos de inicio desde él), lo que incluye cambiar el espacio activo a otro espacio de inicio (por lo general, al espacio que se ejecuta inmediatamente antes del intento de inicio en el nuevo espacio activo). 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 de las particiones del zócalo A/B actuales y escribe los datos en las particiones del zócalo A/B que no se usen según las instrucciones del paquete OTA.
- Llama a la interfaz
boot_control
en un flujo de trabajo predefinido. - Ejecuta un programa posterior a la instalación desde la partición nueva después de escribir todas las particiones de ranura sin usar, según las instrucciones del paquete OTA. (para obtener más información, consulta Posinstalación).
Como el daemon update_engine
no participa en el proceso de inicio, está limitado en lo que puede hacer durante una actualización por las políticas y funciones de SELinux en el puerto actual (estas políticas y funciones no se pueden actualizar hasta que el sistema se inicie 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 que no se pueden borrar con un restablecimiento de la configuración de fábrica.
Actualiza la fuente del motor
La fuente de update_engine
se encuentra en system/update_engine
. Los archivos dexopt OTA de 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 movimiento de artefactos posterior a la OTA y el archivo rc para la secuencia de comandos de movimiento. -
frameworks/base/services/core/java/com/android/server/pm/OtaDexoptService.java
(másOtaDexoptShellCommand
) es el administrador de paquetes que prepara los comandos de dex2oat para las aplicaciones.
Para ver un ejemplo funcional, consulta /device/google/marlin/device-common.mk
.
Actualiza los registros del motor
En 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 en tu 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 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 bootloader desde dónde iniciar. Estos son 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 aplicaron actualizaciones. La ranura actual del sistema se puede iniciar, es correcta y es la ranura activa.
- Actualización en curso: El sistema se ejecuta desde la ranura B, por lo que esta es la ranura activa, correcta y que se puede iniciar. El zócalo A se marcó como no inicializable, ya que el contenido del zócalo A se está actualizando, pero aún no se completa. Un reinicio en este estado debería continuar el inicio desde la ranura B.
- Se aplicó la actualización, se espera el reinicio: El sistema se ejecuta desde la ranura B, que se puede iniciar y se inicia correctamente, pero la ranura A se marcó como activa (y, por lo tanto, se marca como se puede iniciar). El zócalo A aún no se marca como correcto, y el bootloader debe realizar algunos intentos de inicio desde el zócalo A.
-
El sistema se reinició en una nueva actualización: El sistema se ejecuta desde la ranura A por primera vez, la ranura B aún se puede iniciar y se inicia correctamente, mientras que la ranura A solo se puede iniciar y aún está activa, pero no se inicia correctamente. Un daemon de espacio de usuario,
update_verifier
, debe marcar el espacio A como correcto 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 la transmisión de 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 A/B de transmisión casi no necesitan almacenamiento temporal y solo requieren suficiente almacenamiento 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.
- Se agregó una prueba de unidad 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 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 un paquete OTA (al que se hace referencia en el código como una carga útil) está disponible para descargarse. Las políticas del dispositivo pueden aplazar la descarga y aplicación de la carga útil según el nivel de batería, la actividad del usuario, el estado de carga y otras políticas. Además, como la actualización se ejecuta en segundo plano, es posible que los usuarios no sepan que se está realizando una actualización. Todo esto significa que el proceso de actualización puede interrumpirse en cualquier momento debido a políticas, reinicios inesperados o acciones del usuario.
De manera opcional, los metadatos del paquete inalámbrico 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 contenido para que este le entregue la OTA a update_engine
de forma correcta. Los fabricantes de dispositivos que tienen su propio servidor y cliente pueden habilitar las actualizaciones de transmisión asegurándose de que el servidor identifique que la actualización se está transmitiendo (o asuma que todas las actualizaciones se están transmitiendo) 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 entrega al framework como transmisión.
Una vez que una carga útil está disponible, el proceso de actualización es el siguiente:
Paso | Actividades |
---|---|
1 |
El espacio actual (o "espacio de origen") se marca como correcto (si aún no lo está) con markBootSuccessful() .
|
2 |
El zócalo sin usar (o "zócalo de destino") se marca como no inicializable llamando a la función setSlotAsUnbootable() . El espacio actual siempre se marca como correcto al comienzo de la actualización para evitar que el bootloader vuelva al espacio sin usar, que pronto tendrá datos no válidos. Si el sistema llegó al punto en el que puede comenzar a aplicar una actualización, el espacio actual se marca como exitoso, incluso si otros componentes principales están dañados (como la IU en un bucle de falla), 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 consiste en 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 | Se vuelven a leer todas las particiones y se verifican en función del hash esperado. |
6 | Se ejecuta el paso posterior a la instalación (si corresponde). En caso de que se produzca un error durante la ejecución de cualquier paso, la actualización fallará y se volverá a intentar con una carga útil diferente. Si todos los pasos hasta el momento se ejecutaron correctamente, la actualización se realiza correctamente y se ejecuta el último paso. |
7 |
El espacio sin usar se marca como activo llamando a setActiveBootSlot() .
Marcar el zócalo sin usar como activo no significa que se terminará de iniciar. El bootloader (o el mismo sistema) puede cambiar el zócalo activo si no lee un estado correcto.
|
8 |
La postinstalación (que se describe a continuación) implica ejecutar un programa desde la versión de la “actualización nueva” mientras se ejecuta en 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 ranura nueva y finaliza las
verificaciones posteriores al reinicio, se marca como
correcta la ranura actual (antes llamada “ranura de destino”) 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 partición nueva 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 de postinstalación se define como usr/bin/postinstall
en la partición del sistema, esta partición del zócalo sin usar se activará en una ubicación fija (como /postinstall_mount
) y se ejecutará el comando /postinstall_mount/usr/bin/postinstall
.
Para que la postinstalación 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 usas un archivo binario de formato ejecutable y vinculable (ELF), este debe ser compatible con el kernel anterior (p.ej., un programa nuevo de 64 bits que se ejecuta en un kernel 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 de acceso o compile un objeto binario estático, las bibliotecas se cargarán desde la imagen del sistema anterior y no desde la nueva.
Por ejemplo, puedes usar una secuencia de comandos de shell como un programa posterior a la instalación que interpreta el binario de shell del sistema anterior con un marcador #!
en la parte superior y, luego, configurar las rutas de acceso a la biblioteca desde el entorno nuevo para ejecutar un programa posterior a la instalación binario 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 incurrir en problemas de retrocompatibilidad ni actualizaciones de paso. 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 que se requieren de forma predeterminada en un dispositivo determinado o en otras tareas de mejor esfuerzo. El paso posterior a la instalación no es adecuado para correcciones de errores únicas antes del reinicio que requieran permisos imprevistos.
El programa posterior a la instalación seleccionado se ejecuta en el contexto de SELinux postinstall
. Todos los archivos de la nueva partición activada se etiquetarán con postinstall_file
, independientemente de cuáles sean sus atributos después de reiniciarse en ese 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 del reinicio, update_verifier
activa la verificación de integridad con dm-verity.
Esta verificación se inicia antes del zygote para evitar que los servicios de Java realicen cambios irreversibles que impidan 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 algún daño. Una vez que se complete la verificación,
update_verifier
marcará que el inicio se realizó correctamente.
update_verifier
solo leerá los bloques que se enumeran en /data/ota_package/care_map.txt
, que se incluye en un paquete OTA A/B cuando se usa el código de 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 borra el archivo extraído después de que el sistema se inicie correctamente en la versión nueva.