A/B(无缝)系统更新

A/B 系统更新,也称为无缝更新,用于确保可运行的启动系统在无线 (OTA) 更新期间能够保留在磁盘上。这样可以降低更新之后设备无法启动的可能性,也就是说,用户需要将设备送到维修/保修中心进行更换和刷机的情况将有所减少。

用户在 OTA 期间可以继续使用设备。在更新过程中,仅当设备重新启动到更新后的磁盘分区时,会发生一次宕机情况。即使 OTA 失败,设备也仍然可以使用,因为它会启动到 OTA 之前的磁盘分区。您可以再次尝试下载 OTA。建议仅针对新设备通过 OTA 实现 A/B 系统更新。

A/B 系统更新将影响:

  • 与引导加载程序的交互
  • 分区选项
  • 构建流程
  • OTA 更新软件包的生成

现有的 dm-verity 功能可确保设备会启动未损坏的映像。如果设备因糟糕的 OTA 或 dm-verity 问题而无法启动,则可以重新启动到原来的映像。

A/B 系统之所以非常强大,是因为任何错误(如 I/O 错误)都只能影响未使用的分区集,并且可以进行重试。由于 I/O 负载被特意控制在较低水平,以免影响用户体验,因此此类错误不太可能会发生。

OTA 更新可以在系统运行时进行,而不会打断用户,更新的内容包括重新启动后进行的应用优化。此外,缓存分区不再用于存储 OTA 更新软件包;无需调整缓存分区的大小。

概览

A/B 系统更新使用称为 update_engine 的后台守护进程以及两组分区。这两组分区称为插槽,通常为插槽 A 和插槽 B。系统从其中一个插槽(“当前插槽”)运行,但运行的系统不会访问“未使用的”插槽中的分区(用于正常操作)。

此功能的目标是将未使用的插槽保留为后备插槽,从而使更新具有抗故障性。如果更新期间或更新后立即出现错误,则系统可以回滚至原来的插槽并继续正常运行。为实现这一目标,“当前”插槽所使用的所有分区(包括只有一个副本的分区)都不应作为 OTA 更新的一部分进行更新。

每个插槽都有“可启动”属性,该属性会声明相应插槽是否包含设备可以从中启动的正确系统。系统运行时,当前插槽肯定可以启动,但是另一个插槽中可能包含旧(仍然正确)版本的系统,也可能包含较新版本或无效的数据。无论当前插槽是哪一个,都有一个插槽是活动插槽或首选插槽。下次启动时,引导加载程序将从活动插槽中启动。最后,每个插槽都有由用户空间设置的“成功”属性,该属性只有在相应插槽也可以启动时才具有相关性。

成功的插槽应该能够自行启动、运行和更新。未标记为成功的可启动插槽(尝试从其启动几次之后)应由引导加载程序标记为不可启动,包括将活动插槽更改为其他可启动插槽(通常更改为在尝试启动到新的活动插槽之前运行的插槽)。界面的具体详细信息在 boot_control.h 中进行了定义。

引导加载程序状态示例

update_engine(以及其他可能的守护进程)使用 boot_control HAL 指示引导加载程序从何处启动。以下是常见的示例情景及其相关的状态:

  • 正常情况:系统正在从其当前插槽(插槽 A 或插槽 B)运行。目前为止尚未应用任何更新。系统的当前插槽是可启动、成功且活动的插槽。
  • 正在更新:系统正在从插槽 B 运行,因此,插槽 B 是可启动、成功且活动的插槽。由于插槽 A 中的内容正在更新,但是尚未完成,因此插槽 A 标记为不可启动。在此状态下,应继续从插槽 B 重新启动。
  • 已应用更新,正在等待重新启动:系统正在从插槽 B 运行,插槽 B 的状态为可启动且成功,但是插槽 A 过去标记为活动(因此现在标记为可启动)。插槽 A 尚未被标记为成功,引导加载程序应该尝试从插槽 A 启动几次。
  • 系统重新启动到新的更新:系统首次从插槽 A 运行,插槽 B 的状态仍为可启动且成功,而插槽 A 仅可启动,且仍然处于活动但不成功的状态。在进行几次检查之后,用户空间守护进程应将插槽 A 标记为成功。

更新引擎功能

守护进程 update_engine 在后台运行,并会使系统做好启动到已更新的新版本的准备。守护进程 update_engine 本身不会参与到启动流程中,且更新期间可以执行的操作会受到限制。守护进程 update_engine 可以执行以下操作:

  • 根据 OTA 软件包的指示,从当前插槽 A/B 分区读取数据,然后向未使用的插槽 A/B 分区中写入数据
  • 在预定义的工作流程中调用 boot_control 界面
  • 根据 OTA 软件包的指示,在将数据写入所有未使用的插槽分区之后,从新分区运行安装后的程序

下文对安装后的步骤进行了详细介绍。注意,守护进程 update_engineSELinux 策略及当前插槽中的功能限制;在系统启动到新版本之前,这些策略和功能无法更新。要实现稳健性目标,更新流程不应执行以下操作:

  • 修改分区表
  • 修改当前插槽中分区的内容
  • 修改恢复出厂设置时无法擦除的非 A/B 分区的内容

A/B 更新过程

当 OTA 软件包(在代码中称为有效负荷)可供下载时,更新流程便开始了。设备中的策略可能会基于电池电量、用户活动、是否连接到充电器或其他策略延迟有效负荷的下载和应用。不过,由于更新在后台运行,因此用户可能不知道更新正在进行,而且更新流程可能随时会由于策略或意外重新启动而被中断。

有效负荷可用后更新流程中的步骤将如下所示:

第 1 步:通过 markBootSuccessful() 将当前插槽(或“源插槽”)标记为成功(如果尚未标记)。

第 2 步:通过调用函数 setSlotAsUnbootable() 将未使用的插槽(或“目标插槽”)标记为不可启动。

在更新开始时,当前插槽会始终标记为成功,以防引导加载程序回退至未使用的插槽(很快将有无效数据)。如果系统已可以开始应用更新,即使其他主要组件已受损(如崩溃循环中的界面),当前插槽也会标记为成功,因为可以通过推送新软件来修复这些主要问题。

更新有效负荷是不透明的 Blob,其中包含更新到新版本的相应指令。更新有效负荷主要由两部分组成:元数据以及与指令相关的额外数据。元数据相对较小,其中包含在目标插槽上生成和验证新版本的操作的列表。例如,某个操作可能会解压缩特定 Blob 并将其写入目标分区中的特定块,或者从源分区读取数据、向其应用二进制补丁程序,然后写入目标分区中的特定块。与操作相关联的额外数据并未包含在元数据中,此类数据在更新有效负荷中所占比重较大,其中将包含这些示例中的已压缩 Blob 或二进制补丁程序。

第 3 步:下载有效负荷元数据。

第 4 步:对于元数据中定义的每项操作,将按顺序发生以下行为:关联的数据(如果有)下载到内存中、操作得到应用、关联的内存被舍弃。

这两个步骤占用了大部分更新时间,因为它们涉及写入和下载大量数据,并且可能会因策略或重新启动等原因而中断。

第 5 步:针对预期哈希重新读取并验证整个分区。

第 6 步:运行安装后步骤(如果有)。

如果在执行任一步骤的过程中出现错误,则更新失败,系统可能会通过其他有效负荷重新尝试更新。如果上述所有步骤均成功完成,则更新成功,系统会执行最后一个步骤。

第 7 步:通过调用 setActiveBootSlot() 将未使用的插槽标记为活动。

将未使用的插槽标记为活动并不意味着它会完成启动。如果它未读取到成功的状态,引导加载程序或系统本身可以将插槽的活动状态切换回来。

安装后的步骤

安装后的步骤包括从“新更新”版本中运行仍在原来版本中运行的程序。如果此步骤已在 OTA 软件包中定义,则为强制性步骤,且程序必须返回退出代码 0,否则更新失败。

对于其中已定义安装后步骤的每个分区,update_engine 会将新分区装载到特定位置,并执行与装载的分区对应的 OTA 中指定的程序。例如,如果安装后程序在相应系统分区中定义为 usr/bin/postinstall,则系统会将此来自未使用插槽的分区装载到一个固定位置(如 /postinstall_mount 中),然后执行 /postinstall_mount/usr/bin/postinstall 命令。注意,要使此步骤生效,需要满足以下条件:

  • 旧内核需要能够装载新的文件系统格式。文件系统类型不能更改,除非旧内核中有为其提供的支持(包括使用 SquashFS 等压缩文件系统时所用的压缩算法等详细信息)。
  • 旧内核需要了解新分区的安装后程序格式。如果使用的是 ELF 二进制文件,则该文件应该与旧内核兼容(例如,如果弃用 32 位版本架构,并改为使用 64 位版本架构,则 64 位的新程序应该可以在旧版 32 位内核上运行)。此外,库将会从旧系统映像而非新系统映像加载,除非加载程序 (ld) 收到使用其他路径或构建静态二进制文件的指令。
  • 新的安装后程序将受到旧系统中定义的 SELinux 策略的限制。

一种示例情况是,将 Shell 脚本用作安装后程序(由旧系统中顶部带有 #! 标记的 Shell 二进制文件解析), 然后从新环境设置库路径,用于执行更复杂的二进制安装后程序。

另一种示例情况是,从专用的较小分区执行安装后步骤,以便主系统分区中的文件系统格式可以得到更新,同时不会产生向后兼容问题或引发 stepping-stone 更新,这样一来,用户便可以从出厂映像直接更新到最新版本。

根据 SELinux 策略,安装后步骤适用于在指定设备上执行设计所需的任务或其他需要尽可能完成的任务:更新支持 A/B 的固件或引导加载程序、为新版本准备部分数据库的副本等等。该步骤不适用于重新启动之前的一次性错误修复(此类修复需要无法预见的权限)。

所选的安装后程序在 postinstall SELinux 环境中运行。新装载的分区中的所有文件都将使用 postinstall_file 进行标记,无论它们在重新启动到新系统后的属性如何,都是如此。对新系统中 SELinux 属性实施的更改不会影响安装后步骤。如果安装后的程序需要额外的权限,则必须将这些权限添加到安装后的环境中。

实现

希望实现该功能的 OEM 和 SoC 供应商必须向其引导加载程序中添加以下支持:

图 1. 引导加载程序状态机

可使用 bootctl 实用工具测试启动控件 HAL。

已针对 Brillo 实施了一些测试:

内核补丁程序

内核命令行参数

内核命令行参数必须包含以下额外参数:

skip_initramfs rootwait ro init=/init root="/dev/dm-0 dm=system none ro,0 1 \
  android-verity <public-key-id> <path-to-system-partition>"

<public-key-id> 是用于验证 verity 表签名的公钥 ID(请参阅 dm-verity)。

要将包含相应公钥的 .X509 证书添加到系统密钥环,请执行以下操作:

  1. 将设置为 .der 格式的 .X509 证书复制到 kernel 的根目录。您可以使用以下 openssl 命令将证书格式从 .pem 转换为 .der(如果 .X509 证书采用 .pem 格式):
    openssl x509 -in <x509-pem-certificate> -outform der -out <x509-der-certificate>
  2. 复制到内核版本根目录后,构建 zImage 以将该证书添加为系统密钥环的一部分。您可以通过以下 procfs 条目(需要启用 KEYS_CONFIG_DEBUG_PROC_KEYS)验证该步骤:
    angler:/# cat /proc/keys
    
    1c8a217e I------     1 perm 1f010000     0     0 asymmetri
    Android: 7e4333f9bba00adfe0ede979e28ed1920492b40f: X509.RSA 0492b40f []
    2d454e3e I------     1 perm 1f030000     0     0 keyring
    .system_keyring: 1/4

如果 .X509 证书添加成功,则表示系统密钥环中存在相应公钥。突出显示部分表示公钥 ID。

下一步是将空格替换为“#”,并将其作为 <public-key-id> 在内核命令行中传递。例如,在上述示例中,以下证书在 <public-key-id> 的位置传递:Android:#7e4333f9bba00adfe0ede979e28ed1920492b40f

恢复

恢复 RAM 磁盘现已包含在 boot.img 文件中。进入恢复模式时,引导加载程序无法在内核命令行中添加 skip_initramfs 选项。

构建变量

必须针对 A/B 目标定义以下变量:
  • AB_OTA_UPDATER := true
  • AB_OTA_PARTITIONS := \
      boot \
      system \
      vendor
    以及通过 update_engine 更新的其他分区(无线装置、引导加载程序等)。
  • BOARD_BUILD_SYSTEM_ROOT_IMAGE := true
  • TARGET_NO_RECOVERY := true
  • BOARD_USES_RECOVERY_AS_BOOT := true
  • PRODUCT_PACKAGES += \
      update_engine \
      update_verifier
(可选)针对调试版本定义以下变量:
  • PRODUCT_PACKAGES_DEBUG += update_engine_client
无法针对 A/B 目标定义以下变量:
  • BOARD_RECOVERYIMAGE_PARTITION_SIZE
  • BOARD_CACHEIMAGE_PARTITION_SIZE
  • BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE

分区

  • A/B 设备不需要恢复分区或缓存分区,因为 Android 已不再使用这些分区。数据分区现在用于存储下载的 OTA 软件包,而恢复映像代码位于启动分区。
  • A/B 化的所有分区应命名如下(插槽始终被命名为 ab 等):boot_aboot_bsystem_asystem_bvendor_avendor_b

Fstab

参数 slotselect 必须位于 A/B 化分区的行中。例如:

<path-to-block-device>/vendor  /vendor  ext4  ro
wait,verify=<path-to-block-device>/metadata,slotselect

请注意,不应选择名称为 vendor 的分区,而应选择分区 vendor_avendor_b 并将其装载到 /vendor 装载点上。

内核插槽参数

应通过特定的 DT 节点 (/firmware/android/slot_suffix) 或 androidboot.slot_suffix 命令行参数传递当前插槽后缀。

或者,如果引导加载程序实现 fastboot,则应支持以下命令和变量:

命令

  • set_active <slot> - 将当前活动插槽设置为指定插槽。此外,还必须清除该插槽的不可启动标记,并将重试计数重置为默认值。

变量

  • has-slot:<partition-base-name-without-suffix> - 如果指定分区支持插槽,则返回“yes”,否则,返回“no”。
  • current-slot - 返回接下来将从中启动的插槽后缀。
  • slot-count - 返回一个表示可用插槽数量的整数。目前支持两个插槽,因此,该值为 2
  • slot-successful:<slot-suffix> - 如果指定插槽已标记为成功启动,则返回“yes”,否则,返回“no”。
  • slot-unbootable:<slot-suffix> - 如果指定插槽标记为不可启动,则返回“yes”,否则,返回“no”。
  • slot-retry-count: - 可以尝试启动指定插槽的剩余重试次数。
  • 这些变量都应显示在 fastboot getvar all

生成 OTA 软件包

OTA 软件包工具遵循与非 A/B 设备一样的命令。target_files.zip 文件必须通过为 A/B 目标定义版本变量生成。OTA 软件包工具会自动识别并生成格式适用于 A/B 更新程序的软件包。

例如,使用以下命令生成完整 OTA:

./build/tools/releasetools/ota_from_target_files \
  dist_output/tardis-target_files.zip ota_update.zip

或者生成增量 OTA:

./build/tools/releasetools/ota_from_target_files \
  -i PREVIOUS-tardis-target_files.zip \
  dist_output/tardis-target_files.zip incremental_ota_update.zip

配置

分区

更新引擎可以更新同一磁盘中定义的任何一对 A/B 分区。

一对分区有一个公共前缀(例如 systemboot)及按插槽划分的后缀(例如 _a)。有效负荷生成器为其定义更新的分区列表由 AB_OTA_PARTITIONS make 变量配置。例如,如果磁盘中有一对分区 bootloader_abooloader_b_a_b 为插槽后缀),则可以通过在产品或单板配置中指定以下变量来更新这些分区:

AB_OTA_PARTITIONS := \
  boot \
  system \
  bootloader

由更新引擎更新的所有分区不得由系统的其余部分修改。在增量更新期间,来自当前插槽的二进制数据将用于在新插槽中生成数据。任何修改都可能导致新插槽数据在更新过程中无法通过验证,从而导致更新失败。

安装后

对于每个已更新的分区,都可以使用一组键值对配置不同的安装后步骤。

要在新映像中运行位于 /system/usr/bin/postinst 的程序,请指定与系统分区中相应文件系统的根目录对应的路径。例如,usr/bin/postinst 的对应路径为 system/usr/bin/postinst(如果未使用 RAM 磁盘)。此外,请指定要传递到 mount(2) 系统调用的文件系统类型。将以下内容添加到产品或设备的 .mk 文件(如果适用):

AB_OTA_POSTINSTALL_CONFIG += \
  RUN_POSTINSTALL_system=true \
  POSTINSTALL_PATH_system=usr/bin/postinst \
  FILESYSTEM_TYPE_system=ext4

后台中的应用编译

要在 A/B 更新的后台编译应用,需要对产品的设备配置(位于产品的 device.mk 中)进行以下两项补充:

  1. 向版本中添加原生组件。这样可以确保编译脚本和二进制文件能够编译并添加到系统映像中。
      # A/B OTA dexopt package
      PRODUCT_PACKAGES += otapreopt_script
    
  2. 将编译脚本与 update_engine 相关联,以便它可以作为安装后步骤运行。
      # A/B OTA dexopt update_engine hookup
      AB_OTA_POSTINSTALL_CONFIG += \
        RUN_POSTINSTALL_system=true \
        POSTINSTALL_PATH_system=system/bin/otapreopt_script \
        FILESYSTEM_TYPE_system=ext4 \
        POSTINSTALL_OPTIONAL_system=true
      

请参阅 DEX_PREOPT 文件的首次启动安装,以将预选文件安装到未使用的第二个系统分区中。