原文:https://source.android.google.cn/devices/tech/ota/ab/
A/B 系统更新(也称为无缝更新)的目标是确保在无线下载 (OTA) 更新期间在磁盘上保留一个可正常启动和使用的系统。采用这种方式可以降低更新之后设备无法启动的可能性,这意味着用户需要将设备送到维修和保修中心进行更换和刷机的情况将会减少。其他某些商业级操作系统(例如 ChromeOS)也成功使用了 A/B 更新机制。
要详细了解 A/B 系统更新,请参见分区选择(槽位)一节。
A/B 系统更新可带来以下好处:
/data
或 /cache
上留出足够的可用空间来存储更新包。进行 A/B 更新时,客户端和系统都需要进行更改。不过,OTA 更新包服务器应该不需要进行更改:更新包仍通过 HTTPS 提供。对于使用 Google OTA 基础架构的设备,系统更改全部是在 AOSP 中进行,并且客户端代码由 Google Play 服务提供。不使用 Google OTA 基础架构的原始设备制造商 (OEM) 将能够重复使用 AOSP 系统代码,但需要自行提供客户端。
如果 OEM 自行提供客户端,客户端需要:
update_engine
(使用 HTTPS 网址),以获取更新包(假设有可用的更新包)。update_engine
将在流式传输更新包的同时,在当前未使用的分区上更新原始数据块。update_engine
结果代码向您的服务器报告安装是成功了还是失败了。如果更新已成功应用,update_engine
将会告知引导加载程序在下次重新启动时启动到新的操作系统。如果新的操作系统无法启动,引导加载程序将会回退到旧的操作系统,因此无需在客户端执行任何操作。如果更新失败,客户端将需要根据详细的错误代码确定何时(以及是否)重试。例如,优秀的客户端能够识别出是一部分(“diff”)OTA 更新包失败,并改为尝试完整的 OTA 更新包。客户端可能会:
在系统方面,A/B 系统更新会影响以下各项:
update_engine
守护进程,以及引导加载程序交互(如下所述)注意:只有对于新设备,才建议通过 OTA 实现 A/B 系统更新。
A/B 系统更新使用两组称为槽位(通常是槽位 A 和槽位 B)的分区。系统从“当前”槽位运行,但在正常操作期间,运行中的系统不会访问未使用的槽位中的分区。这种方法通过将未使用的槽位保留为后备槽位,来防范更新出现问题:如果在更新期间或更新刚刚完成后出现错误,系统可以回滚到原来的槽位并继续正常运行。为了实现这一目标,当前槽位使用的任何分区(包括只有一个副本的分区)都不应在 OTA 更新期间进行更新。
每个槽位都有一个“可启动”属性,该属性用于表明相应槽位存储的系统正确无误,设备可从相应槽位启动。系统运行时,当前槽位处于可启动状态,但另一个槽位则可能包含旧版本(仍然正确)的系统、包含更新版本的系统,或包含无效的数据。无论当前槽位是哪一个,都有一个槽位是活动槽位(引导加载程序在下次启动时将使用的槽位,也称为首选槽位)。
此外,每个槽位还都有一个由用户空间设置的“成功”属性,仅当相应槽位处于可启动状态时,该属性才具有相关性。被标记为成功的槽位应该能够自行启动、运行和更新。未被标记为成功的可启动槽位(多次尝试使用它启动之后)应由引导加载程序标记为不可启动,其中包括将活动槽位更改为另一个可启动的槽位(通常是更改为在尝试启动到新的活动槽位之前正在运行的槽位)。关于相应接口的具体详细信息在 boot_control.h
中进行了定义。
A/B 系统更新过程会使用名为 update_engine
的后台守护进程来使系统做好准备,以启动到更新后的新版本。该守护进程可以执行以下操作:
boot_control
接口。由于 update_engine
守护进程本身不会参与到启动流程中,因此该守护进程在更新期间可执行的操作受限于当前槽位中的 SELinux 政策和功能(在系统启动到新版本之前,此类政策和功能无法更新)。为了维持一个稳定可靠的系统,更新流程不应修改分区表、当前槽位中各个分区的内容,以及无法通过恢复出厂设置擦除的非 A/B 分区的内容。
更新引擎源代码
update_engine
源代码位于 system/update_engine
中。A/B OTA dexopt 文件分开放到了 installd
和一个程序包管理器中:
frameworks/native/cmds/installd/
ota* 包括安装后脚本、用于 chroot 的二进制文件、负责调用 dex2oat 的已安装克隆、OTA 后 move-artifacts 脚本,以及 move 脚本的 rc 文件。frameworks/base/services/core/java/com/android/server/pm/OtaDexoptService.java
(加上 OtaDexoptShellCommand
)是负责为应用准备 dex2oat 命令的程序包管理器。如需实际示例,请参阅 /device/google/marlin/device-common.mk
。
更新引擎日志
对于 Android 8.x 及更低版本,可在 logcat
及错误报告中找到 update_engine
日志。要使 update_engine
日志可在文件系统中使用,请将以下更改添加到您的细分版本中:
这些更改会将最新的 update_engine
日志的副本保存到 /data/misc/update_engine_log/update_engine.YEAR-TIME
。除当前日志以外,最近的五个日志也会保存在 /data/misc/update_engine_log/
下。拥有日志组 ID 的用户将能够访问相应的文件系统日志。
boot_control
HAL 供 update_engine
(可能还有其他守护进程)用于指示引导加载程序从何处启动。常见的示例情况及其相关状态包括:
update_verifier
应将槽位 A 标记为成功。用户设备并非在 /data
上总是有足够的空间来下载更新包。由于 OEM 和用户都不想浪费 /cache
分区上的空间,因此有些用户会因为设备上没有空间来存储更新包而不进行更新。为了解决这个问题,Android 8.0 中添加了对流式 A/B 更新(下载数据块后直接将数据块写入 B 分区,而无需将数据块存储在 /data
上)的支持。流式 A/B 更新几乎不需要临时存储空间,并且只需要能够存储大约 100KiB 元数据的存储空间即可。
要在 Android 7.1 中实现流式更新,请选择以下补丁程序:
无论是使用 Google 移动服务 (GMS),还是使用任何其他更新客户端,都需要安装这些补丁程序,才能在 Android 7.1 中支持流式传输 A/B 更新包。
当有 OTA 更新包(在代码中称为有效负载)可供下载时,更新流程便开始了。设备中的政策可以根据电池电量、用户活动、充电状态或其他政策来延迟下载和应用有效负载。此外,由于更新是在后台运行,因此用户可能并不知道正在进行更新。所有这些都意味着,更新流程可能随时会由于政策、意外重新启动或用户操作而中断。
OTA 更新包本身所含的元数据可能会指示可进行流式更新,在这种情况下,相应更新包也可采用非流式安装方式。服务器可以利用这些元数据告诉客户端正在进行流式更新,以便客户端正确地将 OTA 移交给 update_engine
。如果设备制造商具有自己的服务器和客户端,便可以通过确保以下两项来实现流式更新:确保服务器能够识别出更新是流式更新(或假定所有更新都是流式更新),并确保客户端能够正确调用 update_engine
来进行流式更新。制造商可以根据更新包是流式更新变体这一事实向客户端发送一个标记,以便在进行流式更新时触发向框架端的移交工作。
有可用的有效负载后,更新流程将遵循如下步骤:
步骤 | 操作 |
---|---|
1 | 通过 markBootSuccessful() 将当前槽位(或“源槽位”)标记为成功(如果尚未标记)。 |
2 | 调用函数 setSlotAsUnbootable() ,将未使用的槽位(或“目标槽位”)标记为不可启动。当前槽位始终会在更新开始时被标记为成功,以防止引导加载程序回退到未使用的槽位(该槽位中很快将会有无效数据)。如果系统已做好准备,可以开始应用更新,那么即使其他主要组件出现损坏(例如界面陷入崩溃循环),当前槽位也会被标记为成功,因为可以通过推送新软件来解决这些问题。 更新有效负载是不透明的 Blob,其中包含更新到新版本的指示。更新有效负载由以下部分组成:
|
3 | 下载有效负载元数据。 |
4 | 对于元数据中定义的每项操作,都将按顺序发生以下行为:将相关数据(如果有)下载到内存中、应用操作,然后释放关联的内存。 |
5 | 对照预期的哈希重新读取并验证所有分区。 |
6 | 运行安装后步骤(如果有)。如果在执行任何步骤期间出现错误,则更新失败,系统可能会通过其他有效负载重新尝试更新。如果上述所有步骤均已成功完成,则更新成功,系统会执行最后一个步骤。 |
7 | 调用 setActiveBootSlot() ,将未使用的槽位标记为活动槽位。将未使用的槽位标记为活动槽位并不意味着它将完成启动。如果引导加载程序(或系统本身)未读取到“成功”状态,则可以将活动槽位切换回来。 |
8 | 安装后步骤(如下所述)包括从“新更新”版本中运行仍在旧版本中运行的程序。如果此步骤已在 OTA 更新包中定义,则为强制性步骤,且程序必须返回并显示退出代码 0 ,否则更新会失败。 |
9 | 在系统足够深入地成功启动到新槽位并完成重新启动后检查之后,系统会调用 markBootSuccessful() ,将现在的当前槽位(原“目标槽位”)标记为成功。 |
注意:第 3 步和第 4 步占用了大部分更新时间,因为这两个步骤涉及写入和下载大量数据,并且可能会因政策或重新启动等原因而中断。
对于定义了安装后步骤的每个分区,update_engine
都会将新分区装载到特定位置,并执行与装载的分区相关的 OTA 中指定的程序。例如,如果安装后程序被定义为相应系统分区中的 usr/bin/postinstall
,则系统会将未使用槽位中的这个分区装载到一个固定位置(例如 /postinstall_mount
),然后执行 /postinstall_mount/usr/bin/postinstall
命令。
为确保成功执行安装后步骤,旧内核必须能够:
ld
) 收到使用其他路径或编译静态二进制文件的指令,否则将会从旧系统映像而非新系统映像加载各种库。例如,您可以使用 shell 脚本作为安装后程序(由旧系统中顶部包含 #!
标记的 shell 二进制文件解析),然后从新环境设置库路径,以便执行更复杂的二进制安装后程序。或者,您可以从专用的较小分区执行安装后步骤,以便主系统分区中的文件系统格式可以得到更新,同时不会产生向后兼容问题或引发 stepping-stone 更新;这样一来,用户便可以从出厂映像直接更新到最新版本。
新的安装后程序将受旧系统中定义的 SELinux 政策限制。因此,安装后步骤适用于在指定设备上执行设计所要求的任务或其他需要尽可能完成的任务(例如,更新支持 A/B 更新的固件或引导加载程序、为新版本准备数据库副本,等等)。安装后步骤不适用于重新启动之前的一次性错误修复(此类修复需要无法预见的权限)。
所选的安装后程序在 postinstall
SELinux 环境中运行。新装载的分区中的所有文件都将带有 postinstall_file
标记,无论在重新启动到新系统后它们的属性如何,都是如此。在新系统中对 SELinux 属性进行的更改不会影响安装后步骤。如果安装后程序需要额外的权限,则必须将这些权限添加到安装后环境中。
重新启动后,update_verifier
会触发利用 dm-verity 进行完整性检查。系统会先启动该检查,然后再启动 zygote,以避免 Java 服务进行任何无法撤消且会导致无法进行安全回滚的更改。在此过程中,如果验证启动功能或 dm-verity 检测到任何损坏,引导加载程序和内核还可能会触发重新启动。检查完成后,update_verifier
会将启动标记为成功。
update_verifier
只会读取 /data/ota_package/care_map.txt
(在使用 AOSP 代码时,该文件会包含在 A/B OTA 更新包中)中列出的数据块。Java 系统更新客户端(例如 GmsCore)会在重新启动设备前提取 care_map.txt
并设置访问权限,在系统成功启动到新版本后会删除所提取的文件。