虚拟 A/B 概览

Android 有两种更新机制:A/B(无缝)更新和非 A/B 更新。为了降低代码复杂度并增强更新流程,在 Android 11 中,这两种机制通过虚拟 A/B 进行统一,以最低的存储开销对所有设备进行无缝更新。Android 12 提供了虚拟 A/B 压缩选项以压缩快照分区。 在 Android 11 和 Android 12 中,以下各项都适用:

  • 虚拟 A/B 更新与 A/B 更新一样,都是无缝更新。虚拟 A/B 更新可最大限度地缩短设备离线和不可用的时间。
  • 虚拟 A/B 更新可以回滚。如果新操作系统无法启动,设备将自动回滚到先前版本。
  • 通过仅复制引导加载程序使用的分区,虚拟 A/B 更新使用极少的额外空间。对于其他可更新分区,将会拍摄快照

背景信息和术语

本部分介绍了相关术语和支持虚拟 A/B 的技术。

设备映射器

设备映射器是 Android 中常用的一个 Linux 虚拟块层。使用动态分区时,类似 /system 的分区是分层设备的堆栈:

  • 堆栈的底部是物理 super 分区(例如 /dev/block/by-name/super)。
  • 中间是 dm-linear 设备,指定了 super 分区中的哪些块构成给定的分区。这在 A/B 设备上显示为 /dev/block/mapper/system_[a|b],在非 A/B 设备上显示为 /dev/block/mapper/system
  • 顶部有一个为已验证分区创建的 dm-verity 设备。此设备会验证 dm-linear 设备上的块是否已正确签名。 它显示为 /dev/block/mapper/system-verity,是 /system 装载点的来源。

图 1 显示了 /system 装载点下的堆栈是什么样子。

system 下的分区堆栈

图 1. /system 装载点下的堆栈

dm-snapshot

虚拟 A/B 依赖于 dm-snapshot,这是一种设备映射器模块,用于捕获存储设备状态的快照。使用 dm-snapshot 时,会用到以下四个设备:

  • 基础设备是被捕获快照的设备。在此页面上,基础设备始终是动态分区,例如 system 或 vendor。
  • 写入时复制 (COW) 设备,用于向基础设备记录更改。 该设备的大小没有限制,只要足够容纳对基础设备的所有更改即可。
  • 快照设备,这是使用 snapshot 目标创建的设备。需向快照设备写入的内容将写入 COW 设备。需从快照设备读取的内容将从基础设备或 COW 设备读取,具体取决于所访问的数据是否经过快照更改。
  • 源设备,这是使用 snapshot-origin 目标创建的设备。需从源设备读取的内容将直接从基础设备读取。需向源设备写入的内容将直接写入基础设备,但原始数据将通过写入 COW 设备进行备份。

dm-snapshot 的设备映射

图 2. dm-snapshot 的设备映射

压缩快照

在 Android 12 及更高版本中,由于 /data 分区上的空间要求可能较高,因此您可以在 build 中启用压缩快照,以满足 /data 分区更高的空间要求。

虚拟 A/B 压缩快照基于 Android 12 及更高版本中提供的以下组件构建而成:

  • dm-user:一个类似于 FUSE 的内核模块,可以让用户空间实现块存储设备。
  • snapuserd:用于实现新快照格式的用户空间守护程序。

这些组件可实现上述压缩功能。下文将给出实现压缩快照功能所需的其他必要更改:压缩快照的 COW 格式dm-userSnapuserd

压缩快照的 COW 格式

在 Android 12 及更高版本中,压缩快照使用 COW 格式。与内核用于未压缩快照的内置格式类似,压缩快照的 COW 格式具有交替的元数据和数据部分。原始格式的元数据只能用于替换操作:将基础映像中的块 X 替换为快照中块 Y 的内容。压缩快照的 COW 格式更具表现力,且支持以下操作:

  • 复制:基础设备中的块 X 应替换为基础设备中的块 Y。
  • 替换:基础设备中的块 X 应替换为快照中块 Y 的内容。其中每个块都经过 gz 压缩。
  • 零:基础设备中的块 X 应全部替换为零。
  • XOR:COW 设备在块 X 与块 Y 之间存储 XOR 压缩字节(适用于 Android 13 及更高版本)。

完整的 OTA 更新仅包含替换和零操作。增量 OTA 更新还可以包含复制操作。

Android 12 中的 dm-user

dm-user 内核模块让 userspace 可实现设备映射器块存储设备。dm-user 表条目会在 /dev/dm-user/<control-name> 下创建其他设备。userspace 进程可以轮询设备,以接收来自内核的读写请求。每个请求都有一个关联的缓冲区,供用户空间进行填充(读取)或传播(写入)。

dm-user 内核模块为内核提供了新的用户可见接口,该内核不在上游 kernel.org 代码库中。在此之前,Google 保留修改 Android 中的 dm-user 接口的权利。

snapuserd

用于 dm-usersnapuserd 用户空间组件实现了虚拟 A/B 压缩。

在虚拟 A/B 的未压缩版本(在 Android 11 及更低版本中,或者不带压缩快照选项的 Android 12)中,COW 设备是原始文件。启用压缩功能后,COW 会充当 dm-user 设备,连接到 snapuserd 守护程序的实例。

内核不使用新的 COW 格式。因此,snapuserd 组件会在 Android COW 格式和内核的内置格式之间转换请求:

Snapuserd 组件在 Android COW 格式和内核的内置格式之间转换请求

图 3. Snapuserd 作为 Android 和内核 COW 格式之间的转换程序的流程图

这种转换和解压缩从不发生在磁盘上。snapuserd 组件会拦截内核中发生的 COW 读取和写入操作,并使用 Android COW 格式实现这些操作。

XOR 压缩

对于发布时搭载 Android 13 及更高版本的设备,XOR 压缩功能(默认处于启用状态)会启用用户空间快照,以在旧块和新块之间存储 XOR 压缩字节。如果虚拟 A/B 更新只更改了某个块中的一些字节,XOR 压缩存储方案所用的空间会少于默认存储方案,因为快照不会存储所有 4K 字节。这样可以缩减快照大小,因为 XOR 数据包含许多零,并且比原始块数据更易于压缩。在 Pixel 设备上,XOR 压缩可以将快照大小缩减 25% 到 40%。

对于升级到 Android 13 及更高版本的设备,必须启用 XOR 压缩。如需了解详情,请参阅 XOR 压缩

虚拟 A/B 压缩过程

本部分详细介绍了 Android 13 和 Android 12 中使用的虚拟 A/B 压缩过程。

读取元数据 (Android 12)

元数据由 snapuserd 守护程序构造。元数据主要是 2 个 ID(每个 ID 8 个字节)的映射,代表要合并的扇区。在 dm-snapshot 中,它被称为 disk_exception

struct disk_exception {
    uint64_t old_chunk;
    uint64_t new_chunk;
};

如果用新数据块替换旧数据块,会使用磁盘异常。

snapuserd 守护程序通过 COW 库读取内部 COW 文件,并为 COW 文件中存在的每个 COW 操作构建元数据。

在创建 dm- snapshot 设备时,从内核中的 dm-snapshot 启动元数据读取。

元数据构造的 IO 路径序列图如下所示。

元数据构造中的 IO 路径序列图

图 4. 元数据构造中 IO 路径的序列流程

合并 (Android 12)

启动过程完成后,更新引擎会将槽标记为启动成功,并通过将 dm-snapshot 目标切换为 dm-snapshot-merge 目标来启动合并。

dm-snapshot 会浏览元数据,并为每个磁盘异常启动合并 IO 路径。下面简要概述了合并 IO 路径。

合并 IO 路径

图 5. 合并 IO 路径概览

如果设备在合并过程中重新启动,则合并将在下一次重新启动时继续并完成。

设备映射器分层

对于发布时搭载 Android 13 及更高版本的设备,虚拟 A/B 压缩中的快照和快照合并过程由 snapuserd 用户空间组件执行。对于升级到 Android 13 及更高版本的设备,必须启用此功能。如需了解详情,请参阅用户空间合并

下面介绍了虚拟 A/B 压缩过程:

  1. 框架会在 dm-verity 设备之外装载 /system 分区,该设备堆叠在 dm-user 设备上。也就是说,根文件系统中的每个 I/O 都会路由到 dm-user
  2. dm-user 将 I/O 路由到用户空间 snapuserd 守护程序,该守护程序会处理 I/O 请求。
  3. 合并操作完成后,框架会收起 dm-linear (system_base) 顶部的 dm-verity,并移除 dm-user

虚拟 A/B 压缩过程

图 6. 虚拟 A/B 压缩过程

快照合并过程可能会中断。如果设备在合并过程中重新启动,合并过程将在重新启动后继续进行。

Init 转换

使用压缩快照启动时,第一阶段 init 必须启动 snapuserd 才能装载分区。这会产生一个问题:当加载并强制执行 sepolicy 时,snapuserd 会被放入错误的上下文中,其读取请求失败,并会导致 SELinux 拒绝事件。

为解决此问题,snapuserdinit 同步转换,如下所示:

  1. 第一阶段 init 从 ramdisk 启动 snapuserd,并将一个打开文件描述符以环境变量的形式保存到其中。
  2. 第一阶段 init 将根文件系统切换到系统分区,然后执行 init 的系统副本。
  3. init 的系统副本会将合并后的 sepolicy 读入字符串中。
  4. Init 会在所有由 ext4 支持的网页上调用 mlock()。然后,它会停用快照设备的所有设备映射器表,并停止 snapuserd。在此之后,禁止从分区读取数据,因为这样做会导致死锁。
  5. snapuserd 的 ramdisk 副本使用打开描述符,init 会使用正确的 SElinux 上下文重新启动守护程序。快照设备的设备映射器表被重新激活。
  6. Init 调用 munlockall(),再次安全地执行 IO 是安全的。

存储空间使用情况

下表比较了使用 Pixel 的操作系统和 OTA 大小的不同 OTA 机制的存储空间使用情况。

对应用大小的影响 非 A/B A/B 虚拟 A/B 虚拟 A/B(压缩)
原始出厂映像 4.5GB 超级数据(3.8G 图片 + 700M 预留)1 9GB 超级数据(3.8G + 700M 预留,两个插槽) 4.5GB 超级数据(3.8G 图片 + 700M 预留) 4.5GB 超级数据(3.8G 图片 + 700M 预留)
其他静态分区 /cache
OTA 期间的额外存储空间(应用 OTA 后返回的空间) /data 上的存储空间为 1.4GB 0 /data 上的存储空间为 3.8GB2 /data 上的存储空间为 2.1GB2
应用 OTA 所需的总存储空间 5.9GB3(超级数据和数据) 9GB(超级数据) 8.3GB3(超级数据和数据) 6.6GB3(超级数据和数据)

1表示基于像素映射的假设布局。

2假设新系统映像与原始系统映像的大小相同。

3在重新启动之前,空间要求是暂时的。

如需实现虚拟 A/B 或使用压缩快照功能,请参阅实现虚拟 A/B