OTA 更新
Over-The-Air(OTA)是更新 Fuchsia 操作系统的一种途径。本文档将描述 Fuchsia 是如何通过 OTA 进行系统更新的。
升级过程被分为下列几个步骤:
检查是否需要更新 {#checking-for-update}
系统更新的入口有两个,omaha-client
和 system-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。
图 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)进行系统更新。
图 2。简化后的 system-update-checker
的更新流程图。
注意:目前还无法检测仅针对 bootloader 的更新,因为没有 paver API 能读取固件。在修正这项之前,即使有可用更新也不会触发更新。你需要使用 update force-install <update-pkg-url>
来强制更新。
如果不需要更新,system-update-checker
会保存已知的上次更新包的哈希值,在后来的更新检查中,system-update-checker
会首先获取更新包的哈希值并与上次保存的哈希值进行对比。如果哈希值相同,则不存在更新也不会触发更新进程。如果不一致,将会继续检查 vbmeta 和 ZBI 来确认该更新的必要性。
进行更新 {#staging-update}
不论是 omaha-client
、system-update-checker
,还是是强制更新,最终该升级都需要被写进设备硬盘中。
更新进程被分为如下几步:
图 3。假设目前该设备正运行在 v1 版本的系统上(Slot A),并即将更新到 v2 版本(Slot B)。注意:实际中的硬盘分区可能并非如此。
初始化垃圾回收 {#initial-garbage-collection}
注意:此时并不会回收旧的更新包,因为旧更新包正被动态索引引用。
system-updater
指示 pkg-cache
来进行垃圾回收。垃圾回收会删除不被静态以及动态索引引用的全部 BLOB。这会清理掉被旧系统使用的绝大多数 BLOB。
图 4。system-updater
指示 pkg-cache
来回收 Slot B 引用的所有 BLOB。由于 Slot B 目前引用着 v0 的系统,因此 v0 版本的所有 BLOB 都被回收了。
获取更新包 {#fetch-update-package}
system-updater
会按照提供给它的 URL 来获取 更新包。之后,动态索引会被更新以指向新的更新包。简化后的更新包结构如下所示:
/board
/epoch.json
/firmware
/fuchsia.vbmeta
/meta
/packages.json
/recovery.vbmeta
/version
/zbi.signed
/zedboot.signed
图 5。system-updater
指示 pkg-resolver
来解析版本为 2 的更新包。
可选地,更新包可能会包含一个 update-mode
文件。这个文件决定了此次系统更新是在 Normal 还是 ForceRecovery 模式下进行。如果没有这个文件,则 system-updater
默认使用 Normal 模式。
当处于 ForceRecovery 模式下时,system-updater
会将镜像写入到恢复分区,并标记 Slot A 与 B 为不可启动,之后启动到恢复模式。获取更多信息,请查看 ForceRecovery 的实现。
第二轮垃圾回收 {#secondary-garbage-collection}
当旧更新包不再被动态索引引用时,会启动另一轮垃圾回收来删除旧更新包。这一步骤为新软件包腾出来额外的空间。
图 6。system-updater
指示 pkg-cache
回收 v1 的更新包以腾出空间。
确认主板匹配 {#verify-board}
当前系统的主板信息文件位于 /config/build-info/board
。system-updater
会确认更新包中主板文件与当前系统的主板文件是否一致。
图 7。system-updater
校验更新包中的主板信息与 Slot A 中的是否匹配。
确认 epoch 受支持 {#verify-epoch}
更新包包含一个 epoch 文件(epoch.json
)。如果更新包的 epoch (目标 epoch)小于 system-updater
(源 epoch),OTA 更新会失败。更多内容详见 RFC-0071。
图 8。system-updater
对比当前系统与更新包的 epoch,以此确认更新包 epoch 是否受支持。
获取附加软件包 {#fetch-reamaining-packages}
system-updater
解析更新包中的 packages.json
文件。packages.json
的结构如下所示:
{
"version": “1”,
"content": [
"fuchsia-pkg://fuchsia.com/sshd-host/0?hash=123..abc",
"fuchsia-pkg://fuchsia.com/system-image/0?hash=456..def"
...
]
}
system-updater
指示 pkg-resolver
解析所有包的 URL。在获取包时,包管理系统只会下载此次更新所需要的 BLOBs。这表示包管理系统只会下载系统中不存在或需要被更新的 BLOBs。其它包即使与当前系统中的 BLOBs 不一致,但只要与系统更新无关也不会被下载。
一旦所有包都下载完成,会触发一次 BlobFS 同步来将 BLOBs 刷新到持久储存中。这一步骤确保了系统更新所必须的全部 BLOBs 都能通过 BlobFS 访问。
图 9。system-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 无关。
图 10。system-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,在没有备份分区时,这些检查都会被跳过。除此之外,每次更新都有一个活动分区被写入。
图 11。system-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
文件,列出了新系统所需的基础包。例如:
pkg-resolver/0 = new-version-hash-pkg-resolver
foo/0 = new-version-hash-foo
bar/0 = new-version-hash-bar
...
// 注意:系统镜像包并未在 `static_packages` 中引用,
// 因为它不可能引用它自己的哈希。
pkgfs
之后会将这些包当作基础包全部加载。这些包会出现在 /pkgfs/{packages, versions}
中,这表示这些包已经被安装或激活。其后,appmgr
启动,并相继启动 pkg-resolver
、pkg-cache
、netstack
等组件。
提交更新 {#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
发现了新的更新,以上更新流程会被再次启动。