blob: 5e8fc80780157616bb49fd0d1bc4ebd3870d11e3 [file] [log] [blame]
<html devsite><head>
<title>实现 A/B 更新</title>
<meta name="project_path" value="/_project.yaml"/>
<meta name="book_path" value="/_book.yaml"/>
</head>
<body>
<!--
Copyright 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<p>想要实现 A/B 系统更新的原始设备制造商 (OEM) 和 SoC 供应商必须确保其引导加载程序实现 boot_control HAL,并将<a href="#kernel">正确的参数</a>传递到内核。</p>
<h2 id="bootcontrol">实现启动控件 HAL</h2>
<p>支持 A/B 更新的引导加载程序必须在 <code>boot_control</code> 中实现 <code><a href="https://android.googlesource.com/platform/hardware/libhardware/+/master/include/hardware/boot_control.h" class="external">hardware/libhardware/include/hardware/boot_control.h</a></code> HAL。您可以使用 <code><a href="https://android.googlesource.com/platform/system/extras/+/master/bootctl/" class="external">system/extras/bootctl</a></code> 实用工具和 <code><a href="https://android.googlesource.com/platform/system/extras/+/refs/heads/master/tests/bootloader/" class="external">system/extras/tests/bootloader/</a></code> 来测试实现。
</p>
<p>您还必须实现状态机,如下所示:</p>
<img src="/devices/tech/ota/images/ab-updates-state-machine.png"/>
<figcaption><strong>图 1.</strong> 引导加载程序状态机</figcaption>
<h2 id="kernel">设置内核</h2>
<p>要实现 A/B 系统更新,请执行以下操作:</p>
<ol>
<li>择优挑选下列内核补丁程序系列(如果需要):
<ul>
<li>如果在没有使用 ramdisk 的情况下启动并使用“以恢复方式启动”,请访问 <a href="https://android-review.googlesource.com/#/c/158491/" class="external">android-review.googlesource.com/#/c/158491/</a> 根据需要进行选择。</li>
<li>要在没有使用 ramdisk 的情况下设置 dm-verity,请访问 <a href="https://android-review.googlesource.com/#/q/status:merged+project:kernel/common+branch:android-3.18+topic:A_B_Changes_3.18" class="external">android-review.googlesource.com/#/q/status:merged+project:kernel/common+branch:android-3.18+topic:A_B_Changes_3.18</a> 根据需要进行选择。</li>
</ul>
</li>
<li>确保内核命令行参数包含中以下额外参数:
<pre class="devsite-click-to-copy">
<code class="devsite-terminal">skip_initramfs rootwait ro init=/init root="/dev/dm-0 dm=system none ro,0 1 android-verity &lt;public-key-id&gt; &lt;path-to-system-partition&gt;"</code></pre>
其中 <code>&lt;public-key-id&gt;</code> 值是用于验证 verity 表签名的公钥 ID(有关详情,请参阅 <a href="/security/verifiedboot/dm-verity.html">dm-verity</a>)。</li>
<li>将包含公钥的 .X509 证书添加到系统密钥环:
<ol>
<li>将设置为 <code>.der</code> 格式的 .X509 证书复制到 <code>kernel</code> 的根目录。如果 .X509 证书的格式为 <code>.pem</code> 文件,请使用以下 <code>openssl</code> 命令将证书格式从 <code>.pem</code> 转换为 <code>.der</code>
<pre class="devsite-terminal devsite-click-to-copy">
openssl x509 -in &lt;x509-pem-certificate&gt; -outform der -out &lt;x509-der-certificate&gt;</pre>
</li>
<li>构建 <code>zImage</code> 以将该证书添加为系统密钥环的一部分。要进行验证,请检查 <code>procfs</code> 条目(需要启用 <code>KEYS_CONFIG_DEBUG_PROC_KEYS</code>):
<pre class="devsite-click-to-copy">
angler:/# cat /proc/keys
1c8a217e I------ 1 perm 1f010000 0 0 asymmetri
Android: 7e4333f9bba00adfe0ede979e28ed1920492b40f: X509.RSA 0492b40f []
2d454e3e I------ 1 perm 1f030000 0 0 keyring
.system_keyring: 1/4</pre>
如果 .X509 证书添加成功,则表示系统密钥环中存在相应公钥(突出显示的部分为公钥 ID)。</li>
<li>将空格替换为 <code>#</code>,并将其作为 <code>&lt;public-key-id&gt;</code> 在内核命令行中传递。例如,传递 <code>Android:#7e4333f9bba00adfe0ede979e28ed1920492b40f</code> 而非 <code>&lt;public-key-id&gt;</code></li>
</ol>
</li>
</ol>
<h2 id="build-variables">设置编译变量</h2>
<p>支持 A/B 更新的引导加载程序必须满足以下编译变量条件:</p>
<table>
<tbody><tr>
<th>必须针对 A/B 更新目标定义的变量</th>
<td>
<ul>
<li><code>AB_OTA_UPDATER := true</code></li>
<li><code>AB_OTA_PARTITIONS := \</code><br />
<code>  boot \</code><br />
<code>  system \</code><br />
<code>  vendor</code><br />
以及通过 <code>update_engine</code> 更新的其他分区(无线装置、引导加载程序等)。</li>
<li><code>BOARD_BUILD_SYSTEM_ROOT_IMAGE := true</code></li>
<li><code>TARGET_NO_RECOVERY := true</code></li>
<li><code>BOARD_USES_RECOVERY_AS_BOOT := true</code></li>
<li><code>PRODUCT_PACKAGES += \</code><br />
<code>  update_engine \</code><br />
<code>  update_verifier</code></li>
</ul>
要查看示例,请参阅 <code><a href="https://android.googlesource.com/device/google/marlin/+/android-7.1.0_r1/device-common.mk" class="external">/device/google/marlin/+/android-7.1.0_r1/device-common.mk</a></code>
您可以选择执行<a href="#compilation">编译</a>中所述的安装后(但在重新启动前)dex2oat 步骤。
</td>
</tr>
<tr><th>无法针对 A/B 目标定义的变量</th>
<td>
<ul>
<li><code>BOARD_RECOVERYIMAGE_PARTITION_SIZE</code></li>
<li><code>BOARD_CACHEIMAGE_PARTITION_SIZE</code></li>
<li><code>BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE</code></li>
</ul>
</td>
</tr>
<tr>
<th>(可选)针对调试版本定义的变量</th>
<td><code>PRODUCT_PACKAGES_DEBUG += update_engine_client</code></td>
</tr>
<tr>
</tr></tbody></table>
<h2 id="partitions">设置分区(插槽)</h2>
<p>A/B 设备不需要恢复分区或缓存分区,因为 Android 已不再使用这些分区。数据分区现在用于存储下载的 OTA 软件包,而恢复映像代码位于启动分区。
A/B 化的所有分区都应按以下方法命名(插槽始终被命名为 <code>a</code><code>b</code> 等):<code>boot_a</code><code>boot_b</code><code>system_a</code><code>system_b</code><code>vendor_a</code><code>vendor_b</code></p>
<h3 id="cache">缓存</h3>
<p>对于非 A/B 更新,缓存分区用于存储下载的 OTA 软件包,并在应用更新时暂时隐藏块。调整缓存分区大小从来没有好办法:所需的大小取决于您想要应用的更新。最糟糕的情况是缓存分区与系统映像一样大。如果使用 A/B 更新,则无需隐藏块(因为您始终在向当前未使用的分区写入数据);如果流式传输 A/B 更新,则无需在应用之前下载整个 OTA 软件包。</p>
<h3 id="recovery">恢复</h3>
<p>恢复 RAM 磁盘现已包含在 <code>boot.img</code> 文件中。进入恢复模式时,引导加载程序<strong>无法</strong>在内核命令行中添加 <code>skip_initramfs</code> 选项。</p>
<p>对于非 A/B 更新,恢复分区包含用于应用更新的代码。A/B 更新由在正常启动的系统映像中运行的 <code>update_engine</code> 应用。同时,仍有一种用于实现恢复出厂设置和旁加载更新软件包的恢复模式(“恢复”就由此而来)。恢复模式的代码和数据存储在 ramdisk 的常规启动分区中;为启动进入系统映像,引导加载程序会指示内核跳过 ramdisk(否则,设备会启动进入恢复模式)。恢复模式很小(其中大部分已在启动分区上),所以启动分区的大小不会增加。</p>
<h3 id="fstab">Fstab</h3>
<p><code>slotselect</code> 参数<strong>必须</strong>位于进行 A/B 更新的分区对应的行中。例如:</p>
<pre class="devsite-click-to-copy">
&lt;path-to-block-device&gt;/vendor /vendor ext4 ro
wait,verify=&lt;path-to-block-device&gt;/metadata,slotselect
</pre>
<p>不得将任何分区命名为 <code>vendor</code>,而应选择 <code>vendor_a</code><code>vendor_b</code> 分区并将其装载到 <code>/vendor</code> 装载点上。</p>
<h3 id="kernel-slot-arguments">内核插槽参数</h3>
<p>应通过特定的设备树 (DT) 节点 (<code>/firmware/android/slot_suffix</code>) 或 <code>androidboot.slot_suffix</code> 命令行参数传递当前插槽后缀。</p>
<p>默认情况下,fastboot 只会闪存 A/B 设备上的插槽 <code>a</code>,并将当前插槽设置为 <code>a</code>。如果更新软件包还包含插槽 <code>b</code> 的映像,则 fastboot 也会闪存这些映像。可用选项包括:</p>
<ul>
<li><code>--slot</code>。提示 fastboot 使用插槽 <code>b</code>,而非插槽 <code>a</code></li>
<li><code>--set-active</code>。将插槽设置为活动插槽。</li>
<li><code>fastboot --help</code>。获取有关命令的详细信息。</li>
</ul>
<p>如果引导加载程序实现 fastboot,则应该支持命令 <code>set_active &lt;slot&gt;</code>,该命令将当前活动插槽设置为指定插槽(此外,还必须清除该插槽的不可启动标记,并将重试计数重置为默认值)。引导加载程序还应支持以下变量:</p>
<ul>
<li><code>has-slot:&lt;partition-base-name-without-suffix&gt;</code>。如果指定分区支持插槽,则返回“yes”,否则返回“no”。</li>
<li><code>current-slot</code>。返回接下来将从中启动的插槽后缀。</li>
<li><code>slot-count</code>。返回一个表示可用插槽数量的整数。目前支持两个插槽,因此该值为 <code>2</code></li>
<li><code>slot-successful:&lt;slot-suffix&gt;</code>。如果指定插槽已标记为成功启动,则返回“yes”,否则返回“no”。</li>
<li><code>slot-unbootable:&lt;slot-suffix&gt;</code>。如果指定插槽已标记为不可引导,则返回“yes”,否则返回“no”。</li>
<li><code>slot-retry-count<slot suffix></slot></code>。启动指定插槽的剩余重试次数。</li>
</ul>
<p>要查看所有变量,请运行 <code class="devsite-terminal devsite-click-to-copy">fastboot getvar all</code>
</p>
<h2 id="ota-package-generation">生成 OTA 软件包</h2>
<p><a href="/devices/tech/ota/tools.html">OTA 软件包工具</a>遵循的命令与不采取 A/B 更新的设备相同。<code>target_files.zip</code> 文件必须通过为 A/B 更新目标定义编译变量生成。OTA 软件包工具会自动识别并生成格式适用于 A/B 更新程序的软件包。</p>
<p>例如:</p>
<ul>
<li>生成完整 OTA:
<pre class="devsite-terminal devsite-click-to-copy">
./build/tools/releasetools/ota_from_target_files \
dist_output/tardis-target_files.zip ota_update.zip
</pre>
</li>
<li>生成增量 OTA:<pre class="devsite-terminal devsite-click-to-copy">
./build/tools/releasetools/ota_from_target_files \
-i PREVIOUS-tardis-target_files.zip \
dist_output/tardis-target_files.zip incremental_ota_update.zip
</pre>
</li>
</ul>
<h2 id="configuration">配置分区</h2>
<p><code>update_engine</code> 可以更新同一磁盘中定义的任何一对 A/B 分区。一对分区共用一个前缀(例如 <code>system</code><code>boot</code>),每个插槽设置一个后缀(例如 <code>_a</code>)。有效负载生成器为其定义更新的分区列表由 <code>AB_OTA_PARTITIONS</code> make 变量配置。</p>
<p>例如,如果磁盘中有一对分区 <code>bootloader_a</code><code>booloader_b</code><code>_a</code><code>_b</code> 为插槽后缀),则您可以通过在产品或单板配置中指定以下变量来更新这些分区:</p>
<pre class="devsite-click-to-copy">
AB_OTA_PARTITIONS := \
boot \
system \
bootloader
</pre>
<p><code>update_engine</code> 更新的所有分区不得由系统的其余部分修改。在增量更新期间,来自当前插槽的二进制数据将用于在新插槽中生成数据。<em></em>任何修改都可能导致新插槽数据在更新过程中无法通过验证,从而导致更新失败。</p>
<h2 id="post-install">配置安装后步骤</h2>
<p>对于每个已更新的分区,您可以使用一组键值对配置不同的安装后步骤。要在新映像中运行位于 <code>/system/usr/bin/postinst</code> 的程序,请指定相对于系统分区中文件系统的根目录的路径。</p>
<p>例如,<code>usr/bin/postinst</code> 的对应路径为 <code>system/usr/bin/postinst</code>(如果未使用 RAM 磁盘)。此外,请指定要传递到 <code>mount(2)</code> 系统调用的文件系统类型。
请将以下代码添加到产品或设备的 <code>.mk</code> 文件中(如果适用):</p>
<pre class="devsite-click-to-copy">
AB_OTA_POSTINSTALL_CONFIG += \
RUN_POSTINSTALL_system=true \
POSTINSTALL_PATH_system=usr/bin/postinst \
FILESYSTEM_TYPE_system=ext4
</pre>
<h2 id="compilation">编译</h2>
<p>出于安全考虑,<code>system_server</code> 无法使用<a href="/devices/tech/dalvik/jit-compiler">即时 (JIT)</a> 编译。
这意味着,您必须至少为 <code>system_server</code> 及其依赖项提前编译 odex 文件;其他内容则可以先不编译。</p>
<p>要在后台编译应用,您必须将以下内容添加到产品的设备配置(位于产品的 device.mk 中):</p>
<ol>
<li>向版本中添加原生组件,以确保编译脚本和二进制文件能够编译并添加到系统映像中。
<pre class="devsite-click-to-copy">
# A/B OTA dexopt package
PRODUCT_PACKAGES += otapreopt_script
</pre></li>
<li>将编译脚本与 <code>update_engine</code> 相关联,以便后者可以作为安装后步骤运行。
<pre class="devsite-click-to-copy">
# A/B OTA dexopt update_engine hookup
AB_OTA_POSTINSTALL_CONFIG += \
RUN_POSTINSTALL_system=true \
POSTINSTALL_PATH_system=system/bin/otapreopt_script \
FILESYSTEM_TYPE_system=ext4 \
POSTINSTALL_OPTIONAL_system=true
</pre>
</li>
</ol>
<p>要获取有关将预先优化的文件安装到未使用的第二个系统分区的帮助,请参阅 <a href="/devices/tech/dalvik/configure.html#other_odex">DEX_PREOPT 文件的首次启动安装</a></p>
</body></html>