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
装载点下的堆栈是什么样子。
图 1. /system 装载点下的堆栈
dm-snapshot
虚拟 A/B 依赖于 dm-snapshot
,这是一种设备映射器模块,用于捕获存储设备状态的快照。使用 dm-snapshot
时,会用到以下四个设备:
- 基础设备是被捕获快照的设备。在此页面上,基础设备始终是动态分区,例如 system 或 vendor。
- 写入时复制 (COW) 设备,用于向基础设备记录更改。 该设备的大小没有限制,只要足够容纳对基础设备的所有更改即可。
- 快照设备,这是使用
snapshot
目标创建的设备。需向快照设备写入的内容将写入 COW 设备。需从快照设备读取的内容将从基础设备或 COW 设备读取,具体取决于所访问的数据是否经过快照更改。 - 源设备,这是使用
snapshot-origin
目标创建的设备。需从源设备读取的内容将直接从基础设备读取。需向源设备写入的内容将直接写入基础设备,但原始数据将通过写入 COW 设备进行备份。
图 2. dm-snapshot 的设备映射
压缩快照
在 Android 12 及更高版本中,由于 /data
分区上的空间要求可能较高,因此您可以在 build 中启用压缩快照,以满足 /data
分区更高的空间要求。
虚拟 A/B 压缩快照基于 Android 12 及更高版本中提供的以下组件构建而成:
这些组件可实现上述压缩功能。下文将给出实现压缩快照功能所需的其他必要更改:压缩快照的 COW 格式、dm-user 和 Snapuserd。
压缩快照的 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-user
的 snapuserd
用户空间组件实现了虚拟 A/B 压缩。
在虚拟 A/B 的未压缩版本(在 Android 11 及更低版本中,或者不带压缩快照选项的 Android 12)中,COW 设备是原始文件。启用压缩功能后,COW 会充当 dm-user
设备,连接到 snapuserd
守护程序的实例。
内核不使用新的 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 路径序列图如下所示。
图 4. 元数据构造中 IO 路径的序列流程
合并 (Android 12)
启动过程完成后,更新引擎会将槽标记为启动成功,并通过将 dm-snapshot
目标切换为 dm-snapshot-merge
目标来启动合并。
dm-snapshot
会浏览元数据,并为每个磁盘异常启动合并 IO 路径。下面简要概述了合并 IO 路径。
图 5. 合并 IO 路径概览
如果设备在合并过程中重新启动,则合并将在下一次重新启动时继续并完成。
设备映射器分层
对于发布时搭载 Android 13 及更高版本的设备,虚拟 A/B 压缩中的快照和快照合并过程由 snapuserd
用户空间组件执行。对于升级到 Android 13 及更高版本的设备,必须启用此功能。如需了解详情,请参阅用户空间合并。
下面介绍了虚拟 A/B 压缩过程:
- 框架会在
dm-verity
设备之外装载/system
分区,该设备堆叠在dm-user
设备上。也就是说,根文件系统中的每个 I/O 都会路由到dm-user
。 dm-user
将 I/O 路由到用户空间snapuserd
守护程序,该守护程序会处理 I/O 请求。- 合并操作完成后,框架会收起
dm-linear
(system_base
) 顶部的dm-verity
,并移除dm-user
。
图 6. 虚拟 A/B 压缩过程
快照合并过程可能会中断。如果设备在合并过程中重新启动,合并过程将在重新启动后继续进行。
Init 转换
使用压缩快照启动时,第一阶段 init 必须启动 snapuserd
才能装载分区。这会产生一个问题:当加载并强制执行 sepolicy
时,snapuserd
会被放入错误的上下文中,其读取请求失败,并会导致 SELinux 拒绝事件。
为解决此问题,snapuserd
与 init
同步转换,如下所示:
- 第一阶段
init
从 ramdisk 启动snapuserd
,并将一个打开文件描述符以环境变量的形式保存到其中。 - 第一阶段
init
将根文件系统切换到系统分区,然后执行init
的系统副本。 init
的系统副本会将合并后的 sepolicy 读入字符串中。Init
会在所有由 ext4 支持的网页上调用mlock()
。然后,它会停用快照设备的所有设备映射器表,并停止snapuserd
。在此之后,禁止从分区读取数据,因为这样做会导致死锁。- 对
snapuserd
的 ramdisk 副本使用打开描述符,init
会使用正确的 SElinux 上下文重新启动守护程序。快照设备的设备映射器表被重新激活。 - 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