虚拟 A/B 是 Android 的主要更新机制。虚拟 A/B 是在旧版 A/B 更新(请参阅 A/B 系统更新)和非 A/B 的基础上构建的;后者已在 Android 15 中废弃,以减少更新的空间开销。
虚拟 A/B 实际上并没有额外的动态分区槽位,请参阅动态分区。相反,增量会写入快照,然后在确认成功启动后合并到基本分区。虚拟 A/B 使用 Android 专用的快照格式。请参阅压缩快照的 COW 格式,该格式支持压缩快照并最大限度地减少磁盘可用空间用量。在完整 OTA 中,快照大小会通过压缩减小约 45%;在增量 OTA 中,快照大小可减小约 55%。
Android 12 提供了虚拟 A/B 压缩选项以压缩快照分区。虚拟 A/B 提供以下好处
- 虚拟 A/B 更新与 A/B 更新一样,都是无缝更新(更新完全在后台进行,不会影响设备运行)。虚拟 A/B 更新可最大限度地缩短设备离线和不可用的时间。
- 虚拟 A/B 更新可以回滚。如果新操作系统无法启动,设备将自动回滚到先前版本。
- 通过仅复制引导加载程序使用的分区,虚拟 A/B 更新使用极少的额外空间。对于其他可更新分区,将会拍摄快照。
背景信息和术语
本部分介绍了相关术语和支持虚拟 A/B 的技术。 在 OTA 安装期间,新的操作系统数据会写入物理分区的新槽位,或写入 Android 专用的 COW 设备。设备重新启动后,动态分区数据会通过使用 dm-user 和 snapuserd 守护程序重新合并到其基础设备中。此过程完全在用户空间中进行。
设备映射器
设备映射器是 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 装载点下的堆栈
压缩快照
在 Android 12 及更高版本中,由于 /data
分区上的空间要求可能较高,因此您可以在 build 中启用压缩快照,以满足 /data
分区更高的空间要求。
虚拟 A/B 压缩快照基于 Android 12 及更高版本中提供的以下组件构建而成:
这些组件可实现上述压缩功能。下文将给出实现压缩快照功能所需的其他必要更改:压缩快照的 COW 格式、dm-user 和 snapuserd。
压缩快照的 COW 格式
在 Android 12 及更高版本中,压缩的快照采用 Android 专用的 COW 格式。COW 格式包含关于 OTA 的元数据,并且具有存放 COW 操作和新操作系统数据的不同缓冲区。 与仅允许执行替换操作(将基础映像中的块 X 替换为快照中块 Y 的内容)的内核快照格式相比,Android 压缩快照 COW 格式更具表现力,且支持以下操作:
- 复制:基础设备中的块 X 应替换为基础设备中的块 Y。
- 替换:基础设备中的块 X 应替换为快照中块 Y 的内容。其中每个块都经过 gz 压缩。
- 零:基础设备中的块 X 应全部替换为零。
- XOR:COW 设备在块 X 与块 Y 之间存储 XOR 压缩字节(适用于 Android 13 及更高版本)。
完整的 OTA 更新仅包含替换和零操作。增量 OTA 更新还可以包含复制操作。
磁盘上的完整快照布局如下所示:
图 2. 磁盘上的 Android COW 格式
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 压缩。 snapuserd 是一个用户空间守护程序,负责写入和读取 Android COW 设备。对快照的所有 I/O 都必须通过此服务进行。在 OTA 安装期间,新的操作系统数据由 snapuserd 写入快照(数据会经过压缩)。此处还会处理元数据的解析和新块数据的解压缩。
XOR 压缩
对于发布时搭载 Android 13 及更高版本的设备,XOR 压缩功能(默认处于启用状态)会启用用户空间快照,以在旧块和新块之间存储 XOR 压缩字节。如果虚拟 A/B 更新只更改了某个块中的一些字节,XOR 压缩存储方案所用的空间会少于默认存储方案,因为快照不会存储所有 4K 字节。这样可以缩减快照大小,因为 XOR 数据包含许多零,并且比原始块数据更易于压缩。在 Pixel 设备上,XOR 压缩可以将快照大小缩减 25% 到 40%。
对于升级到 Android 13 及更高版本的设备,必须启用 XOR 压缩。如需了解详情,请参阅 XOR 压缩。
快照合并
对于发布时搭载 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-verity
堆叠在dm-linear
(system_base
) 上,并移除dm-user
。
图 3. 虚拟 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在重新启动之前,空间要求是暂时的。
Android 11 虚拟 A/B
使用虚拟 A/B 的 Android 11 利用内核 COW 格式写入动态分区。此方法最终被废弃,因为内核 COW 格式不支持压缩。
Android 12 虚拟 A/B
在 Android 12 中,支持以 Android 专用的 COW 格式进行压缩。虚拟 A/B 的此版本需要将 Android 专用的 COW 格式转换为内核 COW 格式。最终,在 Android 13 中,此方法被替换,移除了对内核 COW 格式和 dm-snapshot
的依赖。
如需实现虚拟 A/B 或使用压缩快照功能,请参阅实现虚拟 A/B