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 的技术。
设备映射器
Device-mapper 是 Android 中经常使用的 Linux 虚拟块层。使用动态分区,像/system
这样的分区是一堆分层设备:
- 堆栈的底部是物理超级分区(例如
/dev/block/by-name/super
)。 - 中间是一个
dm-linear
设备,指定超级分区中的哪些块形成给定的分区。这在 A/B 设备上显示为/dev/block/mapper/system_[a|b]
,在非 A/B 设备上显示为 /dev//dev/block/mapper/system
。 - 顶部是一个
dm-verity
设备,为已验证的分区创建。该设备验证dm-linear
设备上的块是否已正确签名。它显示为/dev/block/mapper/system-verity
并且是/system
安装点的来源。
图 1 显示了/system
挂载点下的堆栈的样子。
图 1. /system 挂载点下的堆栈
dm-快照
虚拟 A/B 依赖于dm-snapshot
,这是一个设备映射器模块,用于对存储设备的状态进行快照。使用dm-snapshot
时,有四个设备在运行:
- 基本设备是快照的设备。在此页面上,基本设备始终是动态分区,例如系统或供应商。
- 写时复制(COW) 设备,用于记录对基本设备的更改。它可以是任意大小,但必须足够大以适应对基本设备的所有更改。
- 快照设备是使用
snapshot
目标创建的。对快照设备的写入将写入 COW 设备。从快照设备读取从基本设备或 COW 设备读取,具体取决于正在访问的数据是否已被快照更改。 - 源设备是使用
snapshot-origin
目标创建的。直接从基础设备读取到源设备。写入源设备直接写入基础设备,但通过写入COW设备备份原始数据。
图 2. dm-snapshot 的设备映射
压缩快照
在 Android 12 中,由于/data
分区的空间要求可能很高,您可以在构建中启用压缩快照来解决/data
分区的更高空间要求。
虚拟 A/B 压缩快照基于 Android 12 中可用的两个新组件构建:
这些组件启用压缩。为实现压缩快照功能所做的其他必要更改将在下一节中给出:压缩快照的 COW 格式、 dm-user和Snapuserd 。
压缩快照的 COW 格式
在 Android 12 中,压缩快照使用新的 COW 格式。与用于未压缩快照的内核内置格式类似,压缩快照的 COW 格式具有元数据和数据的交替部分。原始格式的元数据只允许“替换”操作:将基础映像中的块X替换为快照中块Y的内容。压缩快照 COW 格式更具表现力,支持三种操作:
- 复制- 基础设备中的块X应替换为基础设备中的块Y。
- 替换- 基础设备中的块X应替换为快照中块Y的内容。这些块中的每一个都经过 gz 压缩。
- 零- 基本设备中的块X应替换为全零。
完整的 OTA 更新仅包括替换和归零操作。增量 OTA 更新还可以具有复制操作。
Android 12 中的 dm 用户
dm-user 内核模块使userspace
能够实现设备映射器块设备。 dm-user 表条目在/dev/dm-user/<control-name>
下创建了一个杂项设备。 userspace
进程可以轮询设备以接收来自内核的读写请求。每个请求都有一个关联的缓冲区供用户空间填充(用于读取)或传播(用于写入)。
dm-user
内核模块为内核提供了一个新的用户可见界面,该界面不属于上游 kernel.org 代码库的一部分。在此之前,Google 保留修改 Android 中的dm-user
界面的权利。
快照用户
dm-user
的snapuserd
用户空间组件实现了虚拟 A/B 压缩。
在 Virtual A/B 的未压缩版本中(在 Android 11 及更低版本中,或在没有压缩快照选项的 Android 12 中),COW 设备是原始文件。启用压缩后,COW 将作为dm-user
设备运行,该设备连接到snapuserd
守护程序的实例。
内核不使用新的 COW 格式。所以snapuserd
组件在 Android COW 格式和内核内置格式之间转换请求:
图 3. snapuserd 作为 Android 和 Kernel COW 格式之间的转换器的流程图
这种转换和解压缩永远不会发生在磁盘上。 snapuserd
组件拦截发生在内核中的 COW 读取和写入,并使用 Android COW 格式实现它们。
虚拟 A/B 压缩过程
这些部分提供了有关虚拟 A/B 压缩中使用的过程的详细信息:读取元数据、合并和执行初始化转换。
读取元数据
元数据由snapuserd
守护进程构建。元数据主要是 2 个 ID 的映射,每个 8 字节,表示要合并的扇区。在dm-snapshot
中,它被称为disk_exception
。
struct disk_exception {
uint64_t old_chunk;
uint64_t new_chunk;
};
当旧数据块被新数据替换时,会使用磁盘异常。
Snapuserd
守护程序通过 COW 库读取内部 COW 文件,并为 COW 文件中存在的每个 COW 操作构造元数据。
创建 dm- dm- snapshot
设备时,从内核中的dm-snapshot
启动元数据读取。
下图提供了元数据构建的 IO 路径的时序图。
图 4.元数据构建中 IO 路径的序列流
合并
引导过程完成后,更新引擎会将插槽标记为引导成功,并通过将dm-snapshot
目标切换到dm-snapshot-merge
目标来启动合并。
dm-snapshot
遍历元数据并为每个磁盘异常启动一个合并 IO。下面显示了合并 IO 路径的高级概述。
图 5.合并 IO 路径概览
如果设备在合并过程中重新启动,则合并在下次重新启动时恢复,合并完成。
初始化转换
使用压缩快照启动时,第一阶段 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(压缩) |
---|---|---|---|---|
原厂图片 | 4.5GB超级(3.8G图片+700M预留) 1 | 9GB 超级(3.8G + 700M 预留,两个插槽) | 4.5GB超级(3.8G图片+700M预留) | 4.5GB超级(3.8G图片+700M预留) |
其他静态分区 | /缓存 | 没有任何 | 没有任何 | 没有任何 |
OTA期间的额外存储(应用OTA后返回的空间) | /data 1.4GB | 0 | /data 3.8GB 2 | /data 2.1GB 2 |
应用 OTA 所需的总存储空间 | 5.9GB 3 (超级和数据) | 9GB(超级) | 8.3GB 3 (超级和数据) | 6.6GB 3 (超级和数据) |
1表示基于像素映射的假定布局。
2假设新系统映像与原始系统映像大小相同。
3空间需求是暂时的,直到重新启动。
要实施虚拟 A/B,或使用压缩快照功能,请参阅实施虚拟 A/B