ota升级-1-/system/update_engine/README.md 翻译
基于Android-14
# Chrome OS Update Process
[目录]
在 Chrome OS 和 Android 等较现代的操作系统中,系统更新被称为 A/B 更新、无线 ([OTA]) 更新、无缝更新或简称为自动更新。与更原始的系统更新(例如 Windows 或 macOS)相比,A/B 更新具有以下几个优势:系统会启动到特殊模式,用较新的更新覆盖系统分区,并且可能需要几分钟或几小时。
(1) 更新可在更新期间和更新后维护磁盘上的可运行系统。因此,降低了设备损坏到无法使用状态的可能性,并减少了手动刷新设备或在维修和保修中心等处刷新设备的需要。
(2) 更新可以在系统运行时进行(通常开销最小),而不会中断用户操作。对用户来说,唯一的缺点是需要重新启动(或者,在 Chrome OS 中,如果执行了更新,则需要注销,这会导致自动重新启动,而重新启动时间约为 10 秒,与正常重新启动无异)。
(3) 用户无需(尽管可以)请求更新。更新检查会在后台定期进行。
(4) 如果更新应用失败,用户不会受到影响。用户将继续使用旧版系统,系统稍后会尝试再次应用更新。
(5) 如果更新应用正确但无法启动,系统将回滚到旧分区,用户仍可照常使用系统。
(6) 用户无需为更新预留足够的空间。系统已预留了足够的空间,相当于一个分区的两个副本(A 和 B)。系统甚至不需要磁盘上的任何缓存空间,从网络到内存再到非活动分区,一切都无缝衔接。
## Life of an A/B Update
在支持 A/B 更新的系统中,每个分区(例如内核或根分区,或其他类似 [DLC] 的构件)都有两个副本。我们将这两个副本称为活动副本 (A) 和非活动副本 (B)。系统启动时会进入活动分区(取决于启动时哪个副本的优先级更高),当有新的更新可用时,会将其写入非活动分区。成功重启后,之前的非活动分区将变为活动分区,而之前的活动分区将变为非活动分区。
### Generation
但一切始于在 (Google) 服务器上为每个新系统映像生成 OTA 软件包。这通过调用
[ota_from_target_files](https://cs.android.com/android/platform/superproject/+/master:build/make/tools/releasetools/ota_from_target_files.py)
并传入源和目标构建文件来完成。此脚本需要 target_file.zip 才能运行,映像文件是不够的。
### Distribution/Configuration
OTA 软件包生成后,会使用特定密钥进行签名,并存储在更新服务器 (GOTA) 已知的位置。GOTA 会通过公共 URL 提供此 OTA 软件包的访问权限。运营商可以选择将此 OTA 更新仅提供给特定的部分设备。
### Installation
当设备的更新客户端发起更新(无论是定期更新还是用户主动发起)时,它会首先查询不同的设备策略,以确定是否允许进行更新检查。例如,设备策略可以阻止在一天中的某些时间段进行更新检查,或者要求将更新检查时间随机分散在一天中,等等。
一旦策略允许进行更新检查,更新客户端就会向更新服务器发送请求(所有这些通信都通过 HTTPS 进行),并识别其参数,例如应用程序 ID、硬件 ID、版本、主板等。
服务器上的某些策略可能会阻止设备获取特定的 OTA 更新,这些服务器端策略通常由运营商设置。例如,运营商可能希望仅向部分设备提供 Beta 版软件。
但是,如果更新服务器决定提供更新负载,它将响应执行更新所需的所有参数,例如下载负载的 URL、元数据签名、负载大小和哈希值等。更新客户端会在发生各种状态变化后继续与更新服务器通信,例如报告已开始下载负载或更新完成,或者报告更新失败并显示特定错误代码等。
然后,设备将继续实际安装 OTA 更新。这大致包含 3 个步骤。
#### Download & Install
每个有效载荷由两个主要部分组成:元数据和额外数据。元数据基本上是更新操作应执行的操作列表。额外数据包含部分或全部操作所需的数据块。更新客户端首先下载元数据,并使用更新服务器响应中提供的签名对其进行加密验证。验证元数据有效后,即可轻松对有效载荷的其余部分进行加密验证(主要通过 SHA256 哈希值)。
接下来,更新客户端将非活动分区标记为不可启动(因为它需要将新的更新写入其中)。此时,系统将无法再回滚到非活动分区。
然后,更新客户端执行元数据中定义的操作(按照它们在元数据中出现的顺序),并在这些操作需要数据时逐步下载剩余的有效负载。操作完成后,其数据将被丢弃。这样就无需在应用整个有效负载之前对其进行缓存。在此过程中,更新客户端会定期检查上次执行的操作,以便在发生故障或系统关闭等情况时,可以从上次错过的点继续执行,而无需从头开始重新执行所有操作。
在下载过程中,更新客户端会对下载的字节进行哈希处理,并在下载完成后检查有效负载签名(位于有效负载的末尾)。如果无法验证签名,则更新将被拒绝。
#### Hash Verification & Verity Computation
非活动分区更新完成后,更新客户端将为每个分区计算
前向纠错码(也称为 FEC,Verity),并将计算出的 Verity 数据写入非活动分区。在某些更新中,Verity 数据包含在额外数据中,因此此步骤将被跳过。
然后,将重新读取整个分区,进行哈希运算,并与元数据中传递的哈希值进行比较,以确保更新已成功写入分区。此步骤计算的哈希值包含上一步写入的 Verity 码。
#### Postintall
下一步,会调用 [Postinstall] 脚本(如果有)。从 OTA 的角度来看,这些安装后脚本只是黑匣子。通常,安装后脚本会优化手机上现有的应用,并运行文件系统垃圾回收,以便设备在 OTA 后能够快速启动。但这些脚本由其他团队管理。
#### Finishing Touches
然后,更新客户端会进入更新已完成的状态,用户需要重启系统。此时,即使有更新的更新可用,更新客户端也不会再进行任何系统更新,直到用户重启(或退出)。但是,它会继续定期执行更新检查,以便我们统计现场活跃设备的数量。
更新成功后,非活动分区将被标记为具有更高的优先级(启动时,优先级较高的分区会优先启动)。用户重启系统后,系统将启动到更新后的分区,并将其标记为活动分区。此时,重启后,[update_verifier](https://cs.android.com/android/platform/superproject/+/master:bootable/recovery/update_verifier/)
程序将运行,读取所有 dm-verity 设备以确保分区未损坏,然后将更新标记为成功。
此时,A/B 更新被视为完成。虚拟 A/B 更新之后还会有一个额外的步骤,称为“合并”。合并通常需要几分钟,之后虚拟 A/B 更新即视为完成。
## Update Engine Daemon
`update_engine` 是一个始终运行的单线程守护进程(但实际不是,有3个线程)。此进程是自动更新的核心。它在后台以较低的优先级运行,并且是系统启动后最后启动的进程之一。不同的客户端(例如 GMS Core 或其他服务)可以向更新引擎发送更新检查请求。请求如何传递给更新引擎的具体方式取决于系统,但在 Chrome OS 中是通过 D-Bus 传递的。请参阅 [D-Bus 接口] 以获取所有可用方法的列表。在 Android 上是通过 Binder 传递的。
更新引擎中嵌入了许多弹性功能,使自动更新更加稳健,包括但不限于:
* 如果更新引擎崩溃,它将自动重启。
* 在主动更新期间,它会定期检查更新状态,如果无法继续更新或中途崩溃,它将从上一个检查点继续。
* 它会重试失败的网络通信。
* 如果增量负载应用失败(由于活动分区的位更改),则会切换到完整负载。
更新客户端将其活动首选项写入 `/data/misc/update_engine/prefs`。这些首选项有助于在更新客户端的生命周期内跟踪更改,并允许在尝试失败或崩溃后继续正常进行更新过程。
/data/misc/update_engine/prefs # ls -la drwx------ 2 root root 3452 2025-08-27 11:43 . drwx------ 3 root root 3452 2010-01-01 08:00 .. -rw------- 1 root root 36 2010-01-01 08:00 boot-id -rw------- 1 root root 1 2025-08-27 11:43 current-bytes-downloaded -rw------- 1 root root 5 2010-01-01 08:00 previous-version -rw------- 1 root root 1 2025-08-27 11:43 total-bytes-downloaded -rw------- 1 root root 2 2010-01-01 08:00 update-state-next-operation
### Interactive vs Non-Interactive vs. Forced Updates
非交互式更新是由更新引擎定期安排并在后台进行的更新。另一方面,交互式更新则发生在用户明确请求检查更新时(例如,点击 Chrome 操作系统“关于”页面中的“检查更新”按钮)。根据更新服务器的策略,交互式更新的优先级高于非交互式更新(通过携带标记提示)。如果服务器负载繁忙等原因,它们可能会决定不提供更新。这两种更新类型之间还有其他内在差异。例如,交互式更新会尝试更快地安装更新。
强制更新类似于交互式更新(由某种用户操作发起),但也可以配置为非交互式更新。由于非交互式更新是定期发生的,因此强制非交互式更新会在请求发出时(而不是稍后)触发非交互式更新。我们可以这样调用强制非交互式更新:
```bash update_engine_client --interactive=false --check_for_update ```
### Network
更新客户端可以使用以太网、WiFi 或蜂窝网络下载有效负载,具体取决于设备所连接的网络。通过蜂窝网络下载需要获得用户的许可,因为它会消耗大量数据。
### Logs
在 Chrome OS 中,`update_engine` 日志位于 `/var/log/update_engine` 目录中。每当 `update_engine` 启动时,它都会创建一个新的日志文件,其名称采用当前数据时间格式(`update_engine.log-DATE-TIME`)。在更新引擎重启几次或系统重启后,可以在 `/var/log/update_engine` 中看到许多日志文件。最新的活动日志符号链接到 `/var/log/update_engine.log`。
在 Android 中,`update_engine` 日志位于 `/data/misc/update_engine_log` 中。
/data/misc/update_engine_log # cat update_engine.20100101-080025 [0101/080025.635772] [INFO:main.cc(84)] A/B Update Engine starting [0101/080025.654204] [INFO:main.cc(56)] set self to idle policy succeed. ...
## Update Payload Generation
更新负载生成是将一组分区/文件转换为更新客户端(尤其是较旧版本的客户端)可理解且可安全验证的格式的过程。此过程包括将输入分区拆分成更小的组件并进行压缩,以便在下载负载时节省网络带宽。
“delta_generator”是一个提供多种选项的工具,可用于生成不同类型的更新负载。其代码位于“update_engine/payload_generator”中。此目录包含与生成更新负载机制相关的所有源代码。此目录中的任何文件都不应包含或用于除“delta_generator”之外的任何其他库/可执行文件,这意味着此目录不会被编译到其他更新引擎工具中。
但是,不建议直接使用“delta_generator”,因为它的参数太多。应使用 [ota_from_target_files](https://cs.android.com/android/platform/superproject/+/master:build/make/tools/releasetools/ota_from_target_files.py)
或 [OTA Generator](https://github.com/google/ota-generator) 等包装器。
### Update Payload File Specification
每个更新有效负载文件均具有下表中定义的特定结构:
| Field | Size (bytes) | Type | Description | | ----------------------- | ------------ | ---------------------------- | ------------------------------------------------------------------------ | | Magic Number | 4 | char[4] | 魔术字符串“CrAU”标识这是一个更新有效负载。 | | Major Version | 8 | uint64 | 有效载荷主版本号。 | | Manifest Size | 8 | uint64 | 清单大小(以字节为单位)。 | | Manifest Signature Size | 4 | uint32 | 清单签名 blob 的大小(以字节为单位)(仅在主版本 2 中)。 | | Manifest | Varies | [DeltaArchiveManifest] | 要执行的操作的列表。 | | Manifest Signature | Varies | [Signatures] | 前五个字段的签名。如果密钥发生变化,则可能会有多个签名。 | | Payload Data | Varies | 原始或压缩数据 blob 列表 | 元数据中操作使用的二进制 blob 列表。 | | Payload Signature Size | Varies | uint64 | 有效载荷签名的大小。 | | Payload Signature | Varies | [Signatures] | 除元数据签名外,整个有效负载的签名。如果密钥已更改,则可能会有多个签名。 |
### Delta vs. Full Update Payloads
有效载荷有两种类型:完整载荷和增量载荷。完整载荷仅由目标镜像(我们要更新到的镜像)生成,并包含更新非活动分区所需的所有数据。因此,完整载荷的大小可能非常大。另一方面,增量载荷是通过比较源镜像(活动分区)和目标镜像并计算这两个镜像之间的差异而生成的差异更新。它本质上是一种类似于“diff”或“bsdiff”等应用程序的差异更新。因此,使用增量载荷更新系统需要系统读取部分活动分区才能更新非活动分区(或重建目标分区)。增量载荷明显小于完整载荷。两种类型的载荷结构相同。
载荷生成非常耗费资源,其工具以高并行度实现。
#### Generating Full Payloads
完整有效载荷的生成方法是将分区拆分成 2MiB(可配置)的块,然后使用 bzip2 或 XZ 算法进行压缩,或者保留为原始数据,具体取决于哪种算法产生的数据量更小。完整有效载荷比增量有效载荷大得多,因此如果网络带宽有限,则需要更长的下载时间。另一方面,完整有效载荷的应用速度更快一些,因为系统不需要从源分区读取数据。
#### Generating Delta Payloads
增量负载是通过基于文件和元数据(更准确地说,是每个相应分区上的文件系统级别)查看源映像和目标映像数据生成的。我们之所以能够生成增量负载,是因为 Chrome OS 分区是只读的。因此,我们可以高度肯定地假设客户端设备上的活动分区逐位等于映像生成/签名阶段生成的原始分区。生成增量负载的过程大致如下:
1. 找到目标分区上所有以零填充的块,并对它们执行“ZERO”操作。“ZERO”操作基本上会丢弃关联的块(取决于具体实现)。
2. 通过直接一对一比较源块和目标块,找到源分区和目标分区之间所有未更改的块,并执行“SOURCE_COPY”操作。
3. 列出源分区和目标分区中的所有文件(及其关联的块),并删除我们在最后两步中已生成操作的块(和文件)。将每个分区的剩余元数据(inode 等)分配为一个文件。
4. 如果文件是新文件,则根据哪个操作生成的数据块较小,为其数据块生成 `REPLACE`、`REPLACE_XZ` 或 `REPLACE_BZ` 操作。
5. 对于其他每个文件,比较源块和目标块,并根据哪个操作生成的数据块较小,生成 `SOURCE_BSDIFF` 或 `PUFFDIFF` 操作。这两个操作会生成源数据块和目标数据块之间的二进制差异。(有关此类二进制差异程序的详细信息,请参阅 [bsdiff] 和 [puffin]!)
6. 根据目标分区的块偏移量对操作进行排序。
7. 可以选择将相邻的相同或相似的操作合并为更大的操作,以提高效率并可能减少有效负载。
完整负载只能包含“REPLACE”、“REPLACE_BZ”和“REPLACE_XZ”操作。增量负载可以包含任何操作。
### Major and Minor versions
主版本号和次版本号分别指定了更新有效负载文件的格式以及更新客户端接受特定类型更新有效负载的能力。这些数字在更新客户端中是[硬编码]的。
主版本号基本上是上述[更新有效负载文件规范](第二个字段)中指定的更新有效负载文件的版本号。每个更新客户端都支持一系列主版本号。目前,只有两个主版本号:1 和 2。Chrome OS 和 Android 都使用主版本号 2(主版本号 1 已被弃用)。每当新增功能无法适应 [Manifest protobuf] 时,我们就需要升级主版本号。升级主版本号时应格外谨慎,因为旧客户端无法处理新版本号。Chrome OS 中的任何主版本升级都应与 GoldenEye 的过渡版本号相关联。
次版本号定义了更新客户端接受特定操作或执行特定操作的能力。每个更新客户端都支持一系列次版本号。例如,次版本号为 4(或更低)的更新客户端无法处理 `PUFFDIFF` 操作。因此,当为包含次版本号为 4(或更低)的更新客户端的镜像生成增量负载时,我们无法为其生成 PUFFDIFF 操作。负载生成过程会查看源镜像的次版本号,以确定其支持的操作类型,并仅生成符合这些限制的负载。同样,如果特定次版本号的客户端存在错误,则升级次版本号有助于避免生成导致该错误显现的负载。然而,升级次版本号在可维护性方面成本也相当高昂,并且容易出错。因此,在进行此类更改时应谨慎行事。
次版本号与完整负载无关。完整负载应该始终能够应用于非常旧的客户端。原因是更新客户端可能不会发送其当前版本,因此如果我们有不同类型的完整有效载荷,我们就不知道要向客户端提供哪个版本。
### Signed vs Unsigned Payloads
更新有效载荷可以签名(使用私钥/公钥对)以用于生产环境,也可以保持未签名状态以用于测试环境。“delta_generator”等工具有助于生成元数据和有效载荷哈希值,或使用私钥对有效载荷进行签名。
## update_payload Scripts
[update_payload] 包含一组 Python 脚本,主要用于验证有效载荷的生成和应用。我们通常使用实际设备(现场测试)来测试更新有效载荷。[`brillo_update_payload`] 脚本可用于在主机设备上生成并测试有效载荷的应用。这些测试可以视为动态测试,无需实际设备。其他 `update_payload` 脚本(例如 [`check_update_payload`])可用于静态检查有效载荷是否处于正确状态及其应用是否正常工作。这些脚本实际上是静态应用有效载荷,而无需运行 Payload_consumer 中的代码。
## Postinstall
[Postinstall] 是在更新客户端将新的映像文件写入非活动分区后调用的进程。Postinstall 的主要职责之一是在根分区末尾重新创建 dm-verity 树形哈希。此外,它还会安装新的固件更新或任何主板特定的进程。Postinstall 在新安装的分区内以单独的 chroot 模式运行。因此,它与正在运行的系统的其余部分完全隔离。任何需要在更新后、设备重启前完成的操作都应该在 Postinstall 中实现。
## Building Update Engine
你可以像构建其他平台应用程序一样构建“update_engine”:
### Setup
在构建任何内容之前,请在 Android 代码库顶部运行以下命令。
每个 shell 只需执行一次。
* `source build/envsetup.sh`
* `lunch aosp_cf_x86_64_only_phone-userdebug`(或将 aosp_cf_x86_64_only_phone-userdebug 替换为您自己的目标)
### Building
`m update_engine update_engine_client delta_generator`
## Running Unit Tests
[运行与其他平台类似的单元测试]:
* `atest update_engine_unittests` 您需要一个连接到笔记本电脑并可通过 ADB 访问的设备来执行此操作。Cuttlefish 也支持此功能。
* `atest update_engine_host_unittests` 在主机上运行部分测试,无需任何设备。
## Initiating a Configured Update
启动更新有多种方法:
* 点击设置“关于”页面中的“检查更新”按钮。此更新检查方式无法配置。
* 使用 [`scripts/update_device.py`] 程序并传入 OTA 压缩包的路径。
## Note to Developers and Maintainers
更改更新引擎源代码时要格外小心以下事项:
### Do NOT Break Backward Compatibility
在每个发布周期中,我们都应该能够生成完整和增量有效负载,以便能够正确应用于运行旧版更新引擎客户端的旧设备。例如,删除或不传递元数据 proto 文件中的参数可能会破坏旧客户端。或者传递旧客户端无法理解的操作也会破坏旧客户端。每当在有效负载生成过程中进行任何更改时,都要问自己这个问题:它能在旧客户端上运行吗?如果不能,我是否需要使用次要版本或其他任何方式来控制它?
尤其是在企业回滚方面,较新的更新程序客户端应该能够接受较旧的更新有效负载。通常情况下,使用完整有效负载即可实现这一点,但应谨慎操作,以免破坏这种兼容性。
### Think About The Future
在更新引擎中进行更改时,请考虑 5 年后的情况:
* 如何实施更改,才能确保 5 年后旧客户端不会崩溃?
* 5 年后如何维护?
* 如何简化未来的更改,同时又不会破坏旧客户端或产生高昂的维护成本?
### Prefer Not To Implement Your Feature In The Updater Client
如果某个功能可以从服务器端实现,请不要在客户端更新程序中实现。因为客户端更新程序有时可能很脆弱,小错误可能会造成灾难性的后果。例如,如果更新程序客户端中出现错误,导致其在检查更新之前崩溃,而我们无法在发布流程的早期阶段及时发现这个错误,那么已经迁移到这个存在缺陷的新系统的生产设备可能将无法再接收自动更新。因此,请务必思考该功能是否可以从服务器端实现(对客户端更新程序的改动可能很小)?或者,是否可以将该功能迁移到其他服务,并尽量减少与更新程序客户端的接口。解答这些问题将在未来带来巨大的回报。
### Be Respectful Of Other Code Bases
~~当前的更新引擎代码库已在许多项目中使用,例如 Android。~~~
Android 和 ChromeOS 代码库已正式分离。
我们会频繁地在这两个项目之间同步代码库。尽量避免破坏 Android 或其他共享更新引擎代码的系统。每当进行任何更改时,请务必思考 Android 是否需要该更改:
* 它会如何影响 Android?
* 是否可以将更改移动到接口并实现存根实现,以免影响 Android?
* 是否可以用宏保护 Chrome OS 或 Android 特定代码?
作为一项基本措施,如果添加/删除/重命名代码,请确保同时更改 `build.gn` 和 `Android.bp`。请勿将 Chrome OS 特定代码(例如,位于 `system_api` 或 `dlcservice` 中的其他库)带入 update_engine 的通用代码中。尝试使用最佳软件工程实践来分离这些问题。
### Merging from Android (or other code bases)
Chrome OS 将 Android 代码作为 [上游分支] 进行跟踪。要将 Android 代码合并到 Chrome OS(或反之),只需将该分支执行 `git merge` 操作并合并到 Chrome OS,使用任意方法进行测试,然后上传合并提交即可。
```bash repo start merge-aosp git merge --no-ff --strategy=recursive -X patience cros/upstream repo upload --cbr --no-verify . ```
[Postinstall]: #postinstall
[update payload file specification]: #update-payload-file-specification
[OTA]: https://source.android.com/devices/tech/ota
[DLC]: https://chromium.googlesource.com/chromiumos/platform2/+/master/dlcservice
[`chromeos-setgoodkernel`]: https://chromium.googlesource.com/chromiumos/platform2/+/master/installer/chromeos-setgoodkernel
[D-Bus interface]: /dbus_bindings/org.chromium.UpdateEngineInterface.dbus-xml
[this repository]: /
[UpdateManager]: /update_manager/update_manager.cc
[update_manager]: /update_manager/
[P2P update related code]: https://chromium.googlesource.com/chromiumos/platform2/+/master/p2p/
[`cros_generate_update_payloads`]: https://chromium.googlesource.com/chromiumos/chromite/+/master/scripts/cros_generate_update_payload.py
[`chromite/lib/paygen`]: https://chromium.googlesource.com/chromiumos/chromite/+/master/lib/paygen/
[DeltaArchiveManifest]: /update_metadata.proto#302
[Signatures]: /update_metadata.proto#122
[hard coded]: /update_engine.conf
[Manifest protobuf]: /update_metadata.proto
[update_payload]: /scripts/
[Postinstall]: https://chromium.googlesource.com/chromiumos/platform2/+/master/installer/chromeos-postinst
[`update_engine` protobufs]: https://chromium.googlesource.com/chromiumos/platform2/+/master/system_api/dbus/update_engine/
[Running unit tests similar to other platforms]: https://chromium.googlesource.com/chromiumos/docs/+/master/testing/running_unit_tests.md
[Nebraska]: https://chromium.googlesource.com/chromiumos/platform/dev-util/+/master/nebraska/
[upstream branch]: https://chromium.googlesource.com/aosp/platform/system/update_engine/+/upstream
[`cros flash`]: https://chromium.googlesource.com/chromiumos/docs/+/master/cros_flash.md
[bsdiff]: https://android.googlesource.com/platform/external/bsdiff/+/master
[puffin]: https://android.googlesource.com/platform/external/puffin/+/master
[`update_engine_client`]: /update_engine_client.cc
[`brillo_update_payload`]: /scripts/brillo_update_payload
[`check_update_payload`]: /scripts/paycheck.py
[Dev Server]: https://chromium.googlesource.com/chromiumos/chromite/+/master/docs/devserver.md
posted on 2025-08-28 17:55 Hello-World3 阅读(60) 评论(0) 收藏 举报