OTA 更新

Over-The-Air(OTA)是更新 Fuchsia 操作系统的一种途径。本文档将描述 Fuchsia 是如何通过 OTA 进行系统更新的。

升级过程被分为下列几个步骤:

检查是否需要更新 {#checking-for-update}

系统更新的入口有两个,omaha-clientsystem-update-checker。这两个组件目的相同,都是为了检查是否有系统更新并启动更新进程。

注意:Omaha 是一个更新可用性管理协议。若要了解更多 Omaha 相关,详见 Omaha

一般来说,如果设备想要使用 Omaha 来决定更新是否可用,此时就需要使用 omaha-client

在任何 Fuchsia 设备上,以下两个组件只会有一个在运行:

使用 omaha-client 检查更新 {#update-omaha}

在系统启动时,omaha-client 就开始运行并开始周期性检查更新。在检查的过程中,omaha-client 会轮询 Omaha 服务器来检查是否存在可用更新。

使用 Omaha 的好处在于:

  • 它可以只允许一部分 Fushsia 设备更新。例如,它能够被配置为只允许 10% 的设备能够更新。这表示只有 10% 的设备在 Omaha 轮询时能够看到有可用更新,而剩余的 90% 设备则无法得知存在更新。
  • 它允许不同渠道的更新。例如,测试用设备能从测试渠道获取到最新(也最不稳定)的更新。同样,也可以通过生产渠道获取最稳定的版本。渠道信息可以选择性地与产品和版本一起提供给 Omaha。

图:使用 omaha-client 检查更新

图 1。简化后的 omaha-client 的更新流程图。图中给出了 omaha-client 是否检查更新或是否应用更新的限制策略。

一旦 omaha-client 从 Omaha 服务器获取到了更新包的 URL,omaha-client 将会通知 system-updater 启动更新进程。

使用 system-update-checker 检查更新 {#update-system}

那些不使用 omaha-client 的设备就需要使用 system-update-checker 来进行更新检查。system-update-checker 会按照其 配置,周期性检查是否存在可用更新。如果没有指定 auto_update,这些检查默认为禁用状态。

system-update-checker 会根据以下条件来确认是否存在可用更新:

  • 当前运行中的系统镜像的哈希值(位于 /pkgfs/system/meta)是否与更新包镜像的哈希值(位于 packages.json)不一致?
  • 如果系统镜像相同,当前系统的 vbmeta 是否与更新包 vbmeta 不一致?
  • 如果不存在 vbmeta,当前系统的 ZBI 是否与更新包 ZBI 不一致?

如果上述问题中有一个答案为不一致,system-update-checker 便会得知更新包已经被改变。一旦获知更新包改变,system-update-checker 会便触发 system-updater 来使用默认更新包(fuchsia-pkg://fuchsia.com/update)进行系统更新。

图:使用 system-update-checker 检查更新

图 2。简化后的 system-update-checker 的更新流程图。

注意:目前还无法检测仅针对 bootloader 的更新,因为没有 paver API 能读取固件。在修正这项之前,即使有可用更新也不会触发更新。你需要使用 update force-install <update-pkg-url> 来强制更新。

如果不需要更新,system-update-checker 会保存已知的上次更新包的哈希值,在后来的更新检查中,system-update-checker 会首先获取更新包的哈希值并与上次保存的哈希值进行对比。如果哈希值相同,则不存在更新也不会触发更新进程。如果不一致,将会继续检查 vbmeta 和 ZBI 来确认该更新的必要性。

进行更新 {#staging-update}

不论是 omaha-clientsystem-update-checker,还是是强制更新,最终该升级都需要被写进设备硬盘中。

更新进程被分为如下几步:

图:启动更新

图 3。假设目前该设备正运行在 v1 版本的系统上(Slot A),并即将更新到 v2 版本(Slot B)。注意:实际中的硬盘分区可能并非如此。

初始化垃圾回收 {#initial-garbage-collection}

注意:此时并不会回收旧的更新包,因为旧更新包正被动态索引引用。

system-updater 指示 pkg-cache 来进行垃圾回收。垃圾回收会删除不被静态以及动态索引引用的全部 BLOB。这会清理掉被旧系统使用的绝大多数 BLOB。

图:初始化垃圾回收

图 4system-updater 指示 pkg-cache 来回收 Slot B 引用的所有 BLOB。由于 Slot B 目前引用着 v0 的系统,因此 v0 版本的所有 BLOB 都被回收了。

获取更新包 {#fetch-update-package}

system-updater 会按照提供给它的 URL 来获取 更新包。之后,动态索引会被更新以指向新的更新包。简化后的更新包结构如下所示:

  1. /board
  2. /epoch.json
  3. /firmware
  4. /fuchsia.vbmeta
  5. /meta
  6. /packages.json
  7. /recovery.vbmeta
  8. /version
  9. /zbi.signed
  10. /zedboot.signed

图:获取更新包

图 5system-updater 指示 pkg-resolver 来解析版本为 2 的更新包。

可选地,更新包可能会包含一个 update-mode 文件。这个文件决定了此次系统更新是在 Normal 还是 ForceRecovery 模式下进行。如果没有这个文件,则 system-updater 默认使用 Normal 模式。

当处于 ForceRecovery 模式下时,system-updater 会将镜像写入到恢复分区,并标记 Slot A 与 B 为不可启动,之后启动到恢复模式。获取更多信息,请查看 ForceRecovery 的实现

第二轮垃圾回收 {#secondary-garbage-collection}

当旧更新包不再被动态索引引用时,会启动另一轮垃圾回收来删除旧更新包。这一步骤为新软件包腾出来额外的空间。

图:第二轮垃圾回收

图 6system-updater 指示 pkg-cache 回收 v1 的更新包以腾出空间。

确认主板匹配 {#verify-board}

当前系统的主板信息文件位于 /config/build-info/boardsystem-updater 会确认更新包中主板文件与当前系统的主板文件是否一致。

图:确认主板匹配

图 7system-updater 校验更新包中的主板信息与 Slot A 中的是否匹配。

确认 epoch 受支持 {#verify-epoch}

更新包包含一个 epoch 文件(epoch.json)。如果更新包的 epoch (目标 epoch)小于 system-updater(源 epoch),OTA 更新会失败。更多内容详见 RFC-0071

图:确认 epoch 受支持

图 8system-updater 对比当前系统与更新包的 epoch,以此确认更新包 epoch 是否受支持。

获取附加软件包 {#fetch-reamaining-packages}

system-updater 解析更新包中的 packages.json 文件。packages.json 的结构如下所示:

  1. {
  2. "version": 1”,
  3. "content": [
  4. "fuchsia-pkg://fuchsia.com/sshd-host/0?hash=123..abc",
  5. "fuchsia-pkg://fuchsia.com/system-image/0?hash=456..def"
  6. ...
  7. ]
  8. }

system-updater 指示 pkg-resolver 解析所有包的 URL。在获取包时,包管理系统只会下载此次更新所需要的 BLOBs。这表示包管理系统只会下载系统中不存在或需要被更新的 BLOBs。其它包即使与当前系统中的 BLOBs 不一致,但只要与系统更新无关也不会被下载。

一旦所有包都下载完成,会触发一次 BlobFS 同步来将 BLOBs 刷新到持久储存中。这一步骤确保了系统更新所必须的全部 BLOBs 都能通过 BlobFS 访问。

图:获取附加软件包

图 9system-updater 指示 pkg-resolver 解析 v2 版本的 packages.json 中引用的包。

将镜像写入块设备 {#write-images-block-device}

哪个镜像将被写入块设备由 system-updater 决定。有两种镜像,一是资源二是固件。

注意:如要了解更多信息,可参阅 update.rs 文件。要了解资源与固件镜像的差异,请查阅 paver.rs 文件。

之后,system-updater 指示铺设程序对 bootloader 和固件进行写入。这些镜像最终的位置与该设备是否支持 ABR 无关。为了防止闪存损耗,只有当映像与块设备上已经存在的映像不同时,才会将映像写入分区

注意:欲了解更多有关 Fuchsia 铺设程序是如何为 bootloader 工作的,请查阅 fuchsia.paver

其后,system-updater 指示铺设程序写入 Fuchsia ZBI 以及它的 vbmeta。这些镜像的最终位置由该设备是否支持 ABR 决定。如果设备支持 ABR,铺设程序会将 Fuchsia ZBI 及其 vbmeta 写入到目前没有启动的分区上(备份分区)。否则,铺设程序会将它们同时写入 A、B 两个 Slot(如果存在 Slot B 的话)。

注意:欲了解 Fuchsia 铺设程序是如何为资源镜像工作的,请查阅 fuchsia.paver 以了解更多。

最终,system-updater 指示铺设程序写入 recovery ZBI 及其 vbmeta。与 bootloader 和固件相似,其最终位置与设备是否支持 ABR 无关。

图:将镜像写入块设备

图 10system-updater 通过铺设程序将 v2 版本的镜像写入 Slot B。

将备份分区设为活动分区 {#set-alternate-active}

如果设备支持 ABR,system-updater 会使用铺设程序将备份分区设置为活动分区。由此,设备下次启动时将会启动到备份分区当中。

有很多种方法来表示分区状态。例如,内置的铺设程序使用 成功FIDL service 使用 健康,还有诸如活动、非活动、可启动的、不可启动的、可选的等众多表示方法。

注意:可查阅 data.h 了解更多有关上述表示方法的实现方式。

每个分区都储存着一份重要的元数据,它被分为三个部分。这些信息帮助决定每个分区的状态。例如,在 Slot B 被标注为活动之前,元数据大致如下:

元数据 Slot A Slot B
优先级 15 0
剩余尝试次数 0 0
健康* 1 0

在 Slot B 设置为活动之后,元数据如下:

元数据 Slot A Slot B
优先级 14 15**
剩余尝试次数 0 7**
健康 1 0

注意:这些有关优先级以及剩余尝试次数的数字都是基于 data.h 中硬编码的值。

如果设备不支持 ABR,在没有备份分区时,这些检查都会被跳过。除此之外,每次更新都有一个活动分区被写入。

图:将备份分区设置为活动分区

图 11system-updater 将 Slot B 设置为活动分区,因此设备下次启动将会进入 Slot B。

重启 {#reboot}

基于更新的配置信息,设备可能重启也可能不会。重启之后,设备会进入新的分区。

图:重启

图 12。设备重启到 Slot B,并开始运行 v2 版本的系统。

验证更新

当更新被系统验证之后,系统将会提交一次更新。

系统按以下路径验证更新:

重启进入新的版本 {#reboot-update}

注意:在本例中假设更新被写入了 Slot B。

在下一次启动时,bootloader 需要决定从哪个分区启动。在本例中,bootloader 决定从 Slot B 启动,因为它有更高的优先级并且有多于 0 次的剩余尝试次数。(详见 将备份分区设为活动分区)。之后,bootloader 验证 B 的 ZBI 与 B 的 vbmeta 匹配,并最终启动进入 Slot B。

注意:更多有关 bootloader 是如何决定启动至哪个分区的细节,请查阅 flow.c

在早先的启动中,fshost 使用新的系统镜像包加载了 pkgfs。这是进行更新时,由 packages.json 中所引用的系统镜像包。在系统镜像包中有一个 static_packages 文件,列出了新系统所需的基础包。例如:

  1. pkg-resolver/0 = new-version-hash-pkg-resolver
  2. foo/0 = new-version-hash-foo
  3. bar/0 = new-version-hash-bar
  4. ...
  5. // 注意:系统镜像包并未在 `static_packages` 中引用,
  6. // 因为它不可能引用它自己的哈希。

pkgfs 之后会将这些包当作基础包全部加载。这些包会出现在 /pkgfs/{packages, versions} 中,这表示这些包已经被安装或激活。其后,appmgr 启动,并相继启动 pkg-resolverpkg-cachenetstack 等组件。

提交更新 {#commiting-update}

system-update-committer 组件会运行各种检查以验证新更新是否成功。例如,它指示 BlobFS 任意读取 1 MiB 数据。如果系统已经提交了更新,这些检查会被跳过。如果这些检查失败,基于系统的配置,system-update-committer 可能会触发一次重启。

在更新被验证之后,当前的分区会被标注为 健康。使用 将备份分区设为活动分区 中的例子,启动的元数据可能是如下的样子:

元数据 Slot A Slot B
优先级 14 15
剩余尝试次数 7 0
健康 0 1

然后,备份分区(Slot A)会被标注为不可启动。现在,其元数据可能如下:

元数据 Slot A Slot B
优先级 0 15
剩余尝试次数 0 0
健康 0 1

在此之后,更新被认为已经提交。这表示:

  • 在下次更新之前,系统将永远从 Slot B 启动。
  • 除非下次更新时重写了 Slot A,系统将永远放弃从 Slot A 启动。
  • 被 Slot A 引用的 BLOBs 现在允许被垃圾回收。
  • 允许后续更新。只要 update checker 发现了新的更新,以上更新流程会被再次启动。