- PVSM.RU - https://www.pvsm.ru -

Бесшовные A-B-обновления в Android: как они устроены

image

Всем привет. В SberDevices наша команда занимается разработкой различных железок и прошивок для них на базе AOSP.

Начиная с Android 8 (у некоторых вендоров с 7.1) в системе появился новый механизм накатки OTA-обновлений, т. н. Seamless A/B OTA Updates — бесшовные обновления. В этом посте я опишу общие принципы его работы, рассмотрю механизм с точки зрения разработчика, а также проведу сравнение со старым (будем его называть recovery-based) подходом применения обновлений. Всё нижесказанное будет справедливо только для чистого AOSP, т. к. конкретная реализация зависит от вендора.

Recovery-based OTA

Обновления для Android поставляются в виде zip-архива с образами обновляемых разделов (block-based updates). Во времена KitKat это был просто набор файлов, которые копировались на устройство прилагаемым скриптом. Я не стану подробно останавливаться на этом режиме [1], кратко опишу основные принципы его работы:

  • zip-архив скачивается системой на устройство;
  • система перезагружается в режим recovery;
  • проверяется совместимость обновления с устройством, его подпись;
  • если всё OK, выполняется updater-script из zip-архива;
  • в процессе обновления устройство может несколько раз перезагрузиться (например, для обновления device tree);
  • если всё прошло успешно, загружаемся в новую прошивку.

Какие минусы в данной схеме?

  • Необходимость резервировать достаточное количество встроенной памяти для OTA-архива. Для этого служит раздел /cache. Некоторые вендоры используют /data, но это редкость. В итоге пользователю остаётся меньше места (да, приложения всё ещё могут использовать место в разделе /cache, но с некоторыми ограничениями).
  • Перезагрузка и применение обновления занимает время, что может быть критично для некоторых видов устройств, например, для Smart TV.
  • Прерывание процесса обновления может привести к boot loop.
  • Нет возможности откатиться на старую версию прошивки.

Эти неудобства позволяет обойти способ бесшовного обновления. Давайте посмотрим, как он устроен.

Seamless A/B OTA

Ключевые компоненты и механизмы, необходимые для реализации бесшовных A/B-обновлений [2]:

  • слотовая разметка флеш-памяти; 
  • взаимодействие с загрузчиком, управление состоянием слотов;
  • системный демон update_engine;
  • генерация zip-архива с обновлением. В рамках данной статьи этот аспект рассматриваться не будет.

Слотирование

Основным принципом работы A/B OTA является слотирование. Все разделы, которые необходимо обновлять (это могут быть любые разделы, а не только системные), должны находиться в двух копиях или, иначе, в слотах. В Android-реализации поддерживается 2 слота, которые именуются соответственно A и B. Система загружается и работает из текущего слота, второй используется только в момент обновления. К имени раздела добавляется суффикс с именем слота.

Ниже приведена таблица сравнения двух вариантов организации разделов на устройстве.
image [3]

Все слотируемые разделы помечаются опцией монтирования slotselect, чтобы система могла выбрать правильный слот. В зависимости от того, где они описаны, это может быть fstab либо dts.

Изменения в таблице разделов

  • В /сache больше нет необходимости. Теперь обновление может сохраняться либо в /data, либо сразу прошиваться в неактивный слот (об этом ниже). 
  • Раздел recovery также больше не используется. Однако режим recovery всё ещё существует, он необходим, например, для сброса устройства на заводские настройки (к этому может привести rescue party [4]). Или для т. н. ручного обновления (sideload) через adbRecovery ramdisk теперь лежит внутри boot-раздела, ядро общее.
  • Для переключения режима загрузки (android/recovery) появилась новая опция в cmdline ‑ skip_initramfs.

На первый взгляд кажется, что такая схема не оптимальна, т. к. под систему необходимо выделить в два раза больше места. Но ведь мы избавились от /cache, а значит уже сэкономили большое количество памяти. Таким образом, система будет занимать немного больше, чем в варианте с recovery.

Главным достоинством A/B-обновлений является возможность стриминга прошивки. Именно она обеспечивает бесшовность и прозрачность обновлений для пользователя: для обновления устройству достаточно перезагрузиться в новый слот. В этом режиме нет необходимости заранее скачивать zip-архив, занимая место в /data. Вместо этого система сразу пишет блоки данных из специально подготовленного файла (payload, см. ниже) в каждый раздел неактивного слота. С точки зрения реализации нет разницы, скачиваем ли мы предварительно обновление либо сразу стримим его в слот.

Слоты имеют следующие состояния:

  • active – активный слот, из него будет загружена система при следующей перезагрузке;
  • bootable – обновление успешно прошито в слот, прошло валидацию, совпали хеш-суммы и т. д.;
  • successful – система смогла успешно загрузиться в новый слот;
  • unbootable – слот поврежден. Система всегда помечает слот как unbootable перед началом процесса обновления.

Оба слота могут быть bootable и successful, но только один — active.

Алгоритм работы загрузчика при выборе слота:
image [5]

  • Загрузчик определяет, что имеется один или более слотов с флагом bootable.
  • Из них выбирается активный слот (либо слот с наибольшим приоритетом).
  • Если система загрузилась успешно, слот помечается как successful и active.
  • Иначе слот помечается как unbootable и система перезагружается.

Изменение состояний слотов во время обновления:
image [6]

Необходимые компоненты для работы с Seamless A/B.

boot_control

Для поддержки A/B-обновлений вендор должен реализовать специальный HAL-интерфейс — boot_control [7]. Он позволяет изменять состояния слотов и получать о них информацию. Для внешней работы (например, через adb shell) используется утилита – bootctl [8]. Интерфейс используется как средство взаимодействия между ОС и загрузчиком.

update_engine

Основной компонент [9] всей A/B-схемы. Занимается загрузкой, стримингом обновлений, проверкой подписи и многим другим. Изменяет состояния слотов через boot_control. Позволяет контролировать процесс обновления устройства: приостанавливать, возобновлять, отменять.
Компонент пришёл в Android из ChromeOS, где уже используется некоторое время. AOSP поддерживает update_engine в виде статической sideload-сборки. Именно она используется в recovery, т.к данный режим не поддерживает динамическую линковку.

Процесс работы данного компонента можно разделить на следующие шаги:

  • загрузка обновления в слот. Загружать можно как из предварительно скачанного пакета с обновлением, так и напрямую по Сети через http/https. В процессе загрузки проверяется подпись, открытый ключ уже находится на устройстве (/system/etc/update_engine/update-payload-key.pub.pem);
  • верификация загруженного обновления и сравнение хеш-сумм;
  • выполнение post-install скриптов. 

Структура пакета обновления:

2009-01-01 00:00:00 .....          360          360  META-INF/com/android/metadata
2009-01-01 00:00:00 .....          107          107  care_map.txt
2009-01-01 00:00:00 .....    384690699    384690699  payload.bin
2009-01-01 00:00:00 .....          154          154  payload_properties.txt
2009-01-01 00:00:00 .....         1675          943  META-INF/com/android/otacert

  • care_map.txt — используется update_verifier-ом (см. ниже);
  • payload_properties.txt — содержит хеши и размеры данных внутри payload;
  • payload.bin — пакет обновления, содержит блоки всех разделов, метаданные, подпись.

update_engine_client

Клиент для управления демоном update_engine. Может напрямую вызываться вендором для применения обновления.

update_verifier

Утилита для проверки целостности системы при первом запуске (слот с флагом active, но еще не successful). Контроль целостности реализуется с помощью модуля ядра dm-verity [10]. Если проверка закончилась успешно, утилита помечает текущий слот как successful. Иначе система перезагружается в старый слот. Верифицируются только блоки, указанные в файле care_map.txt.

UpdateEngineApi

Для реализации vendor-сервисов обновлений существует Java API [11]. Также имеется пример реализации [12] такого сервиса.

Рассмотрим пример сборки A/B update в AOSP. Для этого отредактируем Makefile целевой платформы:

#Включим поддержку A/B
AB_OTA_UPDATER := true
#Укажем необходимые разделы для слотирования:
AB_OTA_PARTITIONS := boot system vendor
#Добавим необходимые пакеты
PRODUCT_PACKAGES := update_engine update_engine_client update_verifier
#Отключим раздел recovery
TARGET_NO_RECOVERY := true
#Убедимся, что НЕ определяются переменные для раздела cache:
#BOARD_CACHEIMAGE_PARTITION_SIZE := ...
#BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ...

После вызова make otapackage получаем zip-архив с обновлением. В таком виде он уже подходит для sideload-режима. Можем выполнить перезагрузку в recovery и вызвать adb sideload ota.zip. Этот способ удобен для отладки.

Применение обновления изнутри рабочей системы обычно определяется вендором. Самый простой способ — выложить payload.bin на http-сервер и напрямую вызвать update_engine_client.

Пример вызова:

update_engine_client 
--payload=http://path/to/payload.bin 
--update 
--headers=" 
FILE_HASH=ozGgyQEddkI5Zax+Wbjo6I/PCR8PEZka9gGd0nWa+oY= 
FILE_SIZE=282344983 
METADATA_HASH=GLIKfE6KRwylWMHsNadG/Q8iy5f786WTatvMdBlpOPg= 
METADATA_SIZE=26723"

В параметр headers передается содержимое файла payload_properties.txt. В logcat можно наблюдать прогресс обновления. Если передать ключ --follow, прогресс будет дублироваться в stdout.

Заключение

Плюсы нового механизма обновлений очевидны:

  • обновление системы происходит в фоне, не прерывая работу пользователя. Да, всё так же потребуется перезагрузка (в новый слот), но пройдёт она значительно быстрее, чем перезагрузка в recovery для применения обновления;
  • минимизируется вероятность boot loop (от ошибок в реализации никто не застрахован). Процесс обновления можно прерывать, на активный слот это никак не повлияет;
  • появляется возможность отката на предыдущую версию прошивки. Даже если по каким-то причинам обновление прошло неуспешно, система просто вернётся к старой версии;
  • благодаря стримингу устройство обновится быстрее;
  • в зависимости от реализации можно полностью исключить пользователя из процесса обновления.

Из минусов я бы выделил два момента:

  • A/B OTA становится зависимой от текущей разметки диска, т. к. обновление происходит во время работы системы. Т. е. становится невозможно накатить обновление с изменёнными разделами;
  • относительная сложность реализации.

И все же, на мой взгляд, плюсы перевешивают. Кстати, в нашем недавно анонсированном устройстве [13] мы используем A/B OTA обновления.

Автор: Меняев Константин

Источник [14]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android/357405

Ссылки в тексте:

[1] этом режиме: https://source.android.com/devices/tech/ota/nonab

[2] бесшовных A/B-обновлений: https://source.android.com/devices/tech/ota/ab

[3] Image: https://habrastorage.org/webt/0h/5o/fz/0h5ofzon-7tbjo-yu09aiwzimpc.png

[4] rescue party: https://source.android.com/devices/tech/debug/rescue-party

[5] Image: https://habrastorage.org/webt/8u/q4/9x/8uq49xpfsieqyjpebw96yxwigfq.png

[6] Image: https://habrastorage.org/webt/-r/7s/4u/-r7s4ucdozhivxpw9jkv-sjyqcm.png

[7] boot_control: https://android.googlesource.com/platform/hardware/libhardware/+/master/include/hardware/boot_control.h

[8] bootctl: https://android.googlesource.com/platform/system/extras/+/master/bootctl/

[9] компонент: https://android.googlesource.com/platform/system/update_engine/

[10] dm-verity: https://source.android.com/security/verifiedboot/dm-verity

[11] Java API: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/UpdateEngine.java

[12] пример реализации: https://android.googlesource.com/platform/packages/apps/Car/SystemUpdater/+/refs/heads/master

[13] устройстве: https://www.sber.ru/salute/sberportal

[14] Источник: https://habr.com/ru/post/521036/?utm_source=habrahabr&utm_medium=rss&utm_campaign=521036