
Всем привет. В SberDevices наша команда занимается разработкой различных железок и прошивок для них на базе AOSP.
Начиная с Android 8 (у некоторых вендоров с 7.1) в системе появился новый механизм накатки OTA-обновлений, т. н. Seamless A/B OTA Updates — бесшовные обновления. В этом посте я опишу общие принципы его работы, рассмотрю механизм с точки зрения разработчика, а также проведу сравнение со старым (будем его называть recovery-based) подходом применения обновлений. Всё нижесказанное будет справедливо только для чистого AOSP, т. к. конкретная реализация зависит от вендора.
Recovery-based OTA
Обновления для Android поставляются в виде zip-архива с образами обновляемых разделов (block-based updates). Во времена KitKat это был просто набор файлов, которые копировались на устройство прилагаемым скриптом. Я не стану подробно останавливаться на этом режиме, кратко опишу основные принципы его работы:
- zip-архив скачивается системой на устройство;
- система перезагружается в режим recovery;
- проверяется совместимость обновления с устройством, его подпись;
- если всё OK, выполняется updater-script из zip-архива;
- в процессе обновления устройство может несколько раз перезагрузиться (например, для обновления device tree);
- если всё прошло успешно, загружаемся в новую прошивку.
Какие минусы в данной схеме?
- Необходимость резервировать достаточное количество встроенной памяти для OTA-архива. Для этого служит раздел /cache. Некоторые вендоры используют /data, но это редкость. В итоге пользователю остаётся меньше места (да, приложения всё ещё могут использовать место в разделе /cache, но с некоторыми ограничениями).
- Перезагрузка и применение обновления занимает время, что может быть критично для некоторых видов устройств, например, для Smart TV.
- Прерывание процесса обновления может привести к boot loop.
- Нет возможности откатиться на старую версию прошивки.
Эти неудобства позволяет обойти способ бесшовного обновления. Давайте посмотрим, как он устроен.
Seamless A/B OTA
Ключевые компоненты и механизмы, необходимые для реализации бесшовных A/B-обновлений:
- слотовая разметка флеш-памяти;
- взаимодействие с загрузчиком, управление состоянием слотов;
- системный демон update_engine;
- генерация zip-архива с обновлением. В рамках данной статьи этот аспект рассматриваться не будет.
Слотирование
Основным принципом работы A/B OTA является слотирование. Все разделы, которые необходимо обновлять (это могут быть любые разделы, а не только системные), должны находиться в двух копиях или, иначе, в слотах. В Android-реализации поддерживается 2 слота, которые именуются соответственно A и B. Система загружается и работает из текущего слота, второй используется только в момент обновления. К имени раздела добавляется суффикс с именем слота.
Ниже приведена таблица сравнения двух вариантов организации разделов на устройстве.

Все слотируемые разделы помечаются опцией монтирования slotselect, чтобы система могла выбрать правильный слот. В зависимости от того, где они описаны, это может быть fstab либо dts.
Изменения в таблице разделов
- В /сache больше нет необходимости. Теперь обновление может сохраняться либо в /data, либо сразу прошиваться в неактивный слот (об этом ниже).
- Раздел recovery также больше не используется. Однако режим recovery всё ещё существует, он необхо��им, например, для сброса устройства на заводские настройки (к этому может привести rescue party). Или для т. н. ручного обновления (sideload) через adb. Recovery ramdisk теперь лежит внутри boot-раздела, ядро общее.
- Для переключения режима загрузки (android/recovery) появилась новая опция в cmdline ‑ skip_initramfs.
На первый взгляд кажется, что такая схема не оптимальна, т. к. под систему необходимо выделить в два раза больше места. Но ведь мы избавились от /cache, а значит уже сэкономили большое количество памяти. Таким образом, система будет занимать немного больше, чем в варианте с recovery.
Главным достоинством A/B-обновлений является возможность стриминга прошивки. Именно она обеспечивает бесшовность и прозрачность обновлений для пользователя: для обновления устройству достаточно перезагрузиться в новый слот. В этом режиме нет необходимости заранее скачивать zip-архив, занимая место в /data. Вместо этого система сразу пишет блоки данных из специально подготовленного файла (payload, см. ниже) в каждый раздел неактивного слота. С точки зрения реализации нет разницы, скачиваем ли мы предварительно обновление либо сразу стримим его в слот.
Слоты имеют следующие состояния:
- active – активный слот, из него будет загружена система при следующей перезагрузке;
- bootable – обновление успешно прошито в слот, прошло валидацию, совпали хеш-суммы и т. д.;
- successful – система смогла успешно загрузиться в новый слот;
- unbootable – слот поврежден. Система всегда помечает слот как unbootable перед началом процесса обновления.
Оба слота могут быть bootable и successful, но только один — active.
Алгоритм работы загрузчика при выборе слота:

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

Необходимые компоненты для работы с Seamless A/B.
boot_control
Для поддержки A/B-обновлений вендор должен реализовать специальный HAL-интерфейс — boot_control. Он позволяет изменять состояния слотов и получать о них информацию. Для внешней работы (например, через adb shell) используется утилита – bootctl. Интерфейс используется как средство взаимодействия между ОС и загрузчиком.
update_engine
Основной компонент всей 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. Если проверка закончилась успешно, утилита помечает текущий слот как successful. Иначе система перезагружается в старый слот. Верифицируются только блоки, указанные в файле care_map.txt.
UpdateEngineApi
Для реализации vendor-сервисов обновлений существует Java API. Также имеется пример реализации такого сервиса.
Рассмотрим пример сборки 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 становится зависимой от текущей разметки диска, т. к. обновление происходит во время работы системы. Т. е. становится невозможно накатить обновление с изменёнными разделами;
- относительная сложность реализации.
И все же, на мой взгляд, плюсы перевешивают. Кстати, в нашем недавно анонсированном устройстве мы используем A/B OTA обновления.
