OTA 更新

无线下载 (OTA) 更新是 Fuchsia 上进行操作系统更新的机制。本文档详细介绍了 OTA 更新在 Fuchsia 上的运作方式。

更新过程分为以下阶段:

检查更新

操作系统更新流程的两个入口点是 omaha-clientsystem-update-checker 组件。

omaha-clientsystem-update-checker 具有相同的用途,即查看是否有操作系统更新并启动更新。

通常,如果产品想要使用 Omaha 来确定更新可用性,则应使用 omaha-client。如果产品不想使用 Omaha,而是想直接从软件包仓库检查更新,则应使用 system-update-checker

在任何给定的 Fuchsia 系统上,只能运行以下组件之一:

使用 omaha-client 更新检查

在启动过程中,omaha-client 会启动并开始定期检查更新。在执行这些检查期间,omaha-client 会轮询 Omaha 服务器以检查更新。

使用 Omaha 的好处如下:

  • 它支持在 Fuchsia 设备阵容中分批发布系统更新。例如,您可以配置仅更新设备阵容的 10%。这意味着,在轮询 Omaha 时,只有 10% 的设备会看到有可用的更新。其余 90% 的设备不会看到可用的更新。
  • 它支持不同的更新渠道。例如,测试设备可以从测试渠道获取更新,并获取最新(可能不稳定)的软件。这样,正式版设备就可以从正式版渠道获取更新,并获得最稳定的软件。您可以选择将渠道信息与产品和版本一起提供给 Omaha。

图:使用 omaha-client 检查是否有更新

图 1. 使用 omaha-client 的简化版更新检查流程。有政策会限制 omaha-client 是否可以检查更新或应用更新。

omaha-client 从 Omaha 服务器获取更新软件包网址后,omaha-client 会告知 system-updater 开始更新。

使用 system-update-checker 进行更新检查

不使用 omaha-client 的设备使用 system-update-checkersystem-update-checker 会定期轮询更新软件包,具体取决于其配置方式。如果未指定 auto_update,这些检查默认处于停用状态。

如需检查是否有可用更新,system-update-checker 会检查以下条件:

  • 当前正在运行的系统映像(位于 /pkgfs/system/meta 中)的哈希是否与更新软件包中的系统映像(位于 packages.json 中)的哈希不同?
  • 如果系统映像没有差异,系统上当前运行的 vbmeta 是否与更新软件包的 vbmeta 不同?
  • 如果没有 vbmeta,系统上当前运行的 ZBI 是否与更新软件包的 ZBI 不同?

如果上述任一回答为“是”,则 system-update-checker 会知道更新软件包已发生变化。当 system-update-checker 发现更新软件包已更改后,system-update-checker 会触发 system-updater 使用默认更新软件包 (fuchsia-pkg://fuchsia.com/update) 开始更新。

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

图 2. 使用 system-update-checker 的简化版更新检查流程。

如果不需要更新,更新检查程序会保存其在服务器上看到的上一个已知更新软件包。在后续检查更新时,系统会将提取的更新软件包的哈希与服务器上上次已知的哈希进行比较。如果最新更新软件包的哈希值自上次检查后发生了变化,更新检查程序会将运行系统上的 vbmeta 和 ZBI 与更新软件包中的相应映像进行比较。如果正在运行的映像与更新软件包中的 vbmeta 或 ZBI 不同,则检查程序会启动系统更新。

监控

如果客户端有兴趣监控更新进度和状态,则可以实现 fuchsia.update.AttemptsMonitor 协议,并提供客户端端到 fuchsia.update.Manager FIDL 协议的 MonitorAllUpdateChecks() 方法。只有在通过其他方法启动更新或更新目前正在进行时,fuchsia.update.AttemptsMonitor 实例才会收到消息。这不会触发新的更新。

fuchsia.update.AttemptsMonitor 实例将收到 OnStart 消息,其中包含 fuchsia.update.Monitor 协议的服务器端。这样,客户端就可以接收和处理 OnState 消息,了解更新状态变化。

另一种方法是实现 fuchsia.update.Monitor,并将客户端提供给 fuchsia.update.Manager 协议的 CheckNow() 方法。系统随即会开始检查是否有更新。它只会监控当前正在运行的更新,并会在更新完成后关闭句柄。

暂存更新

无论更新是通过 omaha-clientsystem-update-checker 还是强制更新检查触发的,都需要将更新写入磁盘。

更新过程分为以下步骤:

图:开始状态图

图 3. 设备目前运行的是假想的操作系统版本 1(在槽 A 上),并开始更新到假想的操作系统版本 2(到槽 B)。警告:实际上,磁盘可能并非以这种方式进行分区。

提取更新软件包

system-updater 使用提供的更新软件包网址提取更新软件包。然后,系统会更新动态索引以引用新的更新软件包。更新软件包示例可能如下所示:

/board
/epoch.json
/firmware
/fuchsia.vbmeta
/packages.json
/recovery.vbmeta
/version
/zbi.signed
/zedboot.signed
/meta/contents
/meta/package

如果提取失败是因为空间不足,system-updater 将触发垃圾回收,以删除静态或动态索引或保留的软件包集中未引用的所有 BLOB。垃圾回收后,system-updater 将重试提取。如果重试失败,system-updater 将仅将保留的软件包集替换为它尝试提取的更新软件包(如果更新软件包网址包含哈希,否则它将清除保留的软件包集),然后再次触发垃圾回收并重试更新软件包提取。

图:提取更新软件包

图 4. system-updater 会指示 pkg-resolver 解析版本 2 更新软件包。假设 system-updater 因空间不足而未能提取更新软件包,触发了垃圾回收以驱逐槽位 B 引用的版本 0 blob,然后重试以成功提取版本 2 更新软件包。

(可选)更新软件包可以包含 update-mode 文件。此文件决定系统更新是在 Normal 模式还是 ForceRecovery 模式下进行。如果不存在 update-mode 文件,system-updater 将默认为“正常”模式。

当模式为 ForceRecovery 时,system-updater 会将映像写入恢复分区,将槽位 A 和 B 标记为不可启动,然后启动到恢复分区。如需了解详情,请参阅 ForceRecovery 的实现

验证电路板匹配情况

当前运行的系统有一个位于 /config/build-info/board 中的开发板文件。system-updater 会验证系统上的开发板文件是否与更新软件包中的开发板文件匹配。

图:验证棋盘是否匹配

图 5. system-updater 会验证更新软件包中的开发板是否与插槽 A 上的开发板匹配。

验证是否支持公元纪年

更新软件包包含一个纪元文件 (epoch.json)。如果更新软件包的纪元(目标纪元)小于 system-updater 的纪元(来源纪元),则 OTA 会失败。如需了解更多背景信息,请参阅 RFC-0071

图:验证是否支持公元纪年

图 6. system-updater 会将更新软件包中的纪元与当前操作系统的纪元进行比较,以验证更新软件包中的纪元是否受支持。

替换保留的软件包集

将保留的软件包集替换为当前更新软件包以及稍后在 OTA 流程中提取的所有软件包。

保留的软件包集是一组免受垃圾回收保护的软件包(除了静态和动态索引中的软件包之外)。它用于防止垃圾回收删除当前更新进程所需的 BLOB。例如,假设某部设备提取了更新所需的部分软件包,然后出于无关的原因重启。当设备再次开始 OTA 时,它仍然需要在重新启动之前提取的软件包,但这些软件包未受动态索引保护(该索引与保留的软件包集一样,会在重新启动时清除)。通过将这些软件包添加到保留的软件包集,system-updater 便可触发垃圾回收(例如移除之前系统版本使用的 blob),而无需撤消之前的工作。

触发垃圾回收

系统会触发垃圾回收,以删除旧系统专有的所有 BLOB。此步骤会为任何新软件包腾出额外的空间。

图:垃圾回收

图 7. system-updater 会指示 pkg-cache 对旧系统专有的所有 BLOB 进行垃圾回收。在此示例中,这意味着 pkg-cache 将驱逐版本 1 更新软件包专门引用的 BLOB。

提取剩余软件包

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 解析所有软件包网址。解析软件包时,软件包管理系统只会提取更新所需的 BLOB,即仅提取尚不存在的 BLOB。软件包管理系统会提取整个 BLOB,而不是系统上当前可能存在的任何内容的差异。

提取所有软件包后,系统会触发 BlobFS 同步,以将 BLOB 刷新到永久存储空间。此过程可确保 BlobFS 中包含系统更新所需的所有 BLOB。

图:提取其余软件包

图 8. system-updater 会指示 pkg-resolver 解析 packages.json 中引用的版本 2 软件包。

将映像写入块设备

system-updater 用于确定需要将哪些映像写入块设备。映像分为两种:资源和固件。

然后,system-updater 会指示铺路工具写入引导加载程序和固件。这些图片的最终位置不取决于设备是否支持 ABR 为防止闪存磨损,只有当映像与块设备上现有的映像不同时,才会将映像写入分区。

然后,system-updater 会指示 paver 写入 Fuchsia ZBI 及其 vbmeta。这些图片的最终位置取决于设备是否支持 ABR ,则 paver 会将 Fuchsia ZBI 及其 vbmeta 写入当前未启动的槽(备用槽)。ABR否则,铺路工具会将它们同时写入 A 和 B 分区(如果存在 B 分区)。

最后,system-updater 会指示铺路工具写入恢复 ZBI 及其 vbmeta。与引导加载程序和固件一样,最终位置不取决于设备是否支持 ABR

图:将映像写入块设备

图 9. system-updater 通过 paver 将版本 2 映像写入槽 B。

将备用分区设为活动分区

如果设备支持 ABR,system-updater 会使用 paver 将备用分区设为活动分区。这样,设备会在下次启动时启动到备用分区。

您可以通过多种方式引用槽状态。例如,内部铺路工具使用 Successful,而 FIDL 服务使用 Healthy,而其他情况可能使用“Active”“Inactive”“Bootable”“Unbootable”“Current”“Alternate”等。

重要的元数据是指为每个内核槽存储的 3 条信息。此信息有助于确定每个内核槽的状态。例如,在槽位 B 被标记为活动之前,元数据可能如下所示:

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

将槽位 B 标记为活动后,元数据将如下所示:

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

如果设备不支持 ABR,则会跳过此检查,因为没有备用分区。而是有一个活动分区,系统会在每次更新时写入该分区。

图:将备用分区设为活动分区

图 10. system-updater 会将槽位 B 设置为“Active”,以便设备在下次启动时启动到槽位 B。

重新启动

设备是否会重新启动取决于更新配置。设备重新启动后,会启动到新槽。

图:重新启动

图 11. 设备重新启动到槽位 B,并开始运行版本 2。

验证更新

系统在验证更新后会提交更新。

系统会通过以下方式验证更新:

重新启动以进入更新后的版本

在下次启动时,引导加载程序需要确定要启动到哪个槽位。在此示例中,引导加载程序确定要启动到槽 B,因为槽 B 的优先级更高,并且剩余的尝试次数大于 0(请参阅将备用分区设置为 active)。然后,引导加载程序会验证 B 的 ZBI 是否与 B 的 vbmeta 匹配,最后启动到槽 B。

在早期启动后,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
...
// Note the system-image package is not referenced in static_packages
// because it's impossible for it to refer to its own hash.

然后,pkgfs 会将所有这些软件包加载为基本软件包。这些软件包会显示在 /pkgfs/{packages, versions} 中,表示这些软件包已安装或已激活。然后,系统会启动 pkg-resolverpkg-cachenetstack

提交更新

system-update-committer 组件会运行各种检查,以验证新更新是否成功。例如,它会指示 BlobFs 任意读取 1 MiB 的数据。如果系统在启动时已提交,则会跳过这些检查。如果检查失败,并且具体取决于系统的配置方式,system-update-committer 可能会触发重新启动。

更新经过验证后,当前分区(槽位 B)会被标记为 Healthy。使用将备用分区设为活动分区中所述的示例,引导元数据现在可能如下所示:

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

然后,备用分区(槽位 A)会被标记为不可启动。现在,启动元数据可能如下所示:

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

此后,系统会将更新视为已提交。这意味着:

  • 在下次系统更新之前,系统始终会启动到槽位 B。
  • 系统会放弃启动到槽位 A,直到下次系统更新覆盖槽位 A。
  • 现在,槽 A 引用的 BLOB 可以进行垃圾回收。
  • 现在允许进行后续系统更新。当更新检查工具发现新更新时,整个更新流程将重新开始。