仮想 A/B の実装

新しいデバイスに仮想 A/B を実装する、またはリリース済みデバイスをレトロフィットするには、デバイス固有のコードを変更する必要があります。

ビルドフラグ

仮想 A/B を使用するデバイスは、A/B デバイスとして構成し、動的パーティションを使ってリリースする必要があります。

仮想 A/B でリリースするデバイスの場合は、仮想 A/B デバイスの基本構成を継承するように設定します。

$(call inherit-product, \
    $(SRC_TARGET_DIR)/product/virtual_ab_ota.mk)

仮想 A/B でリリースするデバイスでは、BOARD_SUPER_PARTITION_SIZE のボードサイズに必要な部分が半分になります。これは、B スロットが super パーティションからなくなったためです。つまり、BOARD_SUPER_PARTITION_SIZE はアップデート グループのサイズ合計 + オーバーヘッド以上のサイズである必要があり、同様に、パーティションのサイズの合計 + オーバーヘッド以上のサイズである必要があります。

Android 13 以降の場合、仮想 A/B で圧縮スナップショットを有効にするには、次の基本構成を継承します。

$(call inherit-product, $(SRC_TARGET_DIR)/product/generic_ramdisk.mk)
$(call inherit-product, \
    $(SRC_TARGET_DIR)/product/virtual_ab_ota/android_t_baseline.mk)

これにより、no-op 圧縮方式を使用しながら、仮想 A/B でユーザー空間のスナップショットが有効になります。すると、圧縮方式を、サポートされている方式のいずれか(gzbrotli)に構成できます。

PRODUCT_VIRTUAL_AB_COMPRESSION_METHOD := gz

Android 12 の場合、仮想 A/B で圧縮スナップショットを有効にするには、次の基本構成を継承します。

$(call inherit-product, $(SRC_TARGET_DIR)/product/generic_ramdisk.mk)
$(call inherit-product, \
    $(SRC_TARGET_DIR)/product/virtual_ab_ota/compression.mk)

XOR 圧縮

Android 13 以降にアップグレードするデバイスの場合、デフォルトでは、XOR 圧縮機能が有効になっていません。XOR 圧縮を有効にするには、デバイスの .mk ファイルに次の行を追加します。

PRODUCT_VENDOR_PROPERTIES += ro.virtual_ab.compression.xor.enabled=true

android_t_baseline.mk を継承するデバイスでは、XOR 圧縮がデフォルトで有効になっています。

ユーザー空間の統合

Android 13 以降にアップグレードするデバイスの場合、デフォルトでは、デバイス マッパーのレイヤ化に記載されているユーザー空間の統合プロセスが有効になっていません。ユーザー空間の統合を有効にするには、デバイスの .mk ファイルに次の行を追加します。

PRODUCT_VENDOR_PROPERTIES += ro.virtual_ab.userspace.snapshots.enabled=true

13 以降でリリースするデバイスでは、ユーザー空間の統合がデフォルトで有効になっています。

ブート コントロール HAL

ブート コントロール HAL は、OTA クライアントがブートスロットを制御するためのインターフェースを提供します。仮想 A/B では、ブート コントロール HAL のマイナー バージョン アップグレードが必要です。これは、フラッシュや出荷時設定へのリセットの際にブートローダーを確実に保護するのに追加の API が必要となるためです。HAL 定義の最新バージョンについては、IBootControl.haltypes.hal をご覧ください。

// hardware/interfaces/boot/1.1/types.hal
enum MergeStatus : uint8_t {
    NONE, UNKNOWN, SNAPSHOTTED, MERGING, CANCELLED };

// hardware/interfaces/boot/1.1/IBootControl.hal
package android.hardware.boot@1.1;
interface IBootControl extends @1.0::IBootControl {
    setSnapshotMergeStatus(MergeStatus status)
        generates (bool success);
    getSnapshotMergeStatus()
        generates (MergeStatus status);
}
// Recommended implementation

Return<bool> BootControl::setSnapshotMergeStatus(MergeStatus v) {
    // Write value to persistent storage
    // e.g. misc partition (using libbootloader_message)
    // bootloader rejects wipe when status is SNAPSHOTTED
    // or MERGING
}

fstab の変更

起動プロセスでは、メタデータ パーティションの整合性が特に重要です。特に OTA アップデートが適用された直後では重要です。したがって、first_stage_init がマウントする前にメタデータ パーティションをチェックする必要があります。確実にこれを行うには、check fs_mgr フラグを /metadata のエントリに追加します。以下に例を示します。

/dev/block/by-name/metadata /metadata ext4 noatime,nosuid,nodev,discard,sync wait,formattable,first_stage_mount,check

カーネルの要件

スナップショットを有効にするには、CONFIG_DM_SNAPSHOTtrue に設定します。

F2FS を使用するデバイスの場合、f2fs: export FS_NOCOW_FL flag to user のカーネルパッチを追加して、ファイルの固定を修正します。また、f2fs: support aligned pinned file のカーネルパッチも含めてください。

仮想 A/B は、カーネル バージョン 4.3 で追加された機能である snapshotsnapshot-merge ターゲットにおけるオーバーフロー ステータス ビットに依存します。Android 9 以降を搭載したデバイスには、カーネル バージョン 4.4 以降が搭載されているはずです。

圧縮スナップショットを有効にする場合、サポートされる最小カーネル バージョンは 4.19 です。CONFIG_DM_USER=m または CONFIG_DM_USER=y を設定します。前者(モジュール)を使用する場合は、モジュールを第 1 ステージの RAM ディスクに読み込む必要があります。そのためには、デバイスの Makefile に次の行を追加します。

BOARD_GENERIC_RAMDISK_KERNEL_MODULES_LOAD := dm-user.ko

Android 11 にアップグレードするデバイスでのレトロフィット

Android 11 にアップグレードする際、動的パーティションを使用してリリースされたデバイスでは、必要に応じて仮想 A/B をレトロフィットできます。更新プロセスは、仮想 A/B を使用してリリースするデバイスとほぼ同じですが、次の若干の違いがあります。

  • COW ファイルの場所 - リリース済みデバイスの場合、OTA クライアントは /data のスペースを使用する前に、super パーティションの利用可能な空のスペースをすべて使用します。レトロフィット デバイスの場合、super パーティションに十分なスペースがあり、COW ファイルが /data に作成されないようにします。

  • ビルド時の機能フラグ - 仮想 A/B をレトロフィットするデバイスの場合、次に示すように PRODUCT_VIRTUAL_AB_OTAPRODUCT_VIRTUAL_AB_OTA_RETROFIT の両方が true に設定されます。

    (call inherit-product, \
        (SRC_TARGET_DIR)/product/virtual_ab_ota_retrofit.mk)
    
  • super パーティション サイズ - 仮想 A/B でリリースするデバイスでは、super パーティションに B スロットが含まれないため、BOARD_SUPER_PARTITION_SIZE を半分に短縮できます。仮想 A/B をレトロフィットしたデバイスでは、古い super パーティションのサイズが維持されます。そのため、BOARD_SUPER_PARTITION_SIZE は 2 × アップデート グループのサイズ合計 + オーバーヘッド以上となり、同様に、2 × パーティションのサイズ合計 + オーバーヘッド以上の値になります。

ブートローダーの変更

アップデートでのマージ中、/data には Android OS のインスタンス全体のみが含まれます。移行が開始されると、コピーが完了するまで、ネイティブの systemvendorproduct のパーティションは不完全な状態になります。このプロセス中に、リカバリまたはシステム設定ダイアログによってデバイスが出荷時設定にリセットされると、デバイスは起動できなくなります。

/data を消去する前に、デバイスの状態に応じて、リカバリまたはロールバックでマージを完了します。

  • 新しいビルドが正常に起動した場合は、移行を完了します。
  • それ以外の場合は、古いスロットにロールバックします。
    • 動的パーティションの場合は、前の状態にロールバックします。
    • 静的パーティションの場合は、アクティブ スロットを古いスロットに設定します。

デバイスがロック解除されている場合、ブートローダーと fastbootd はどちらも /data パーティションを消去できます。fastbootd は移行の完了を強制することもできますが、ブートローダーは強制できません。ブートローダーは、マージが進行中かどうか、/data 内のどのブロックが OS パーティションを構成するのかを把握できません。デバイスは、ユーザーが知らずにデバイスを動作不能(文鎮化)にすることを防ぐために、以下を行う必要があります。

  1. ブートローダーが setSnapshotMergeStatus() メソッドで設定された値を読み取れるように、ブート コントロール HAL を実装する。
  2. マージ ステータスが MERGING の場合、またはマージ ステータスが SNAPSHOTTED で、スロットが新しく更新されたスロットに変更された場合は、ブートローダーで userdatametadata、またはマージ ステータスを格納するパーティションのワイプ リクエストを拒否する。
  3. fastboot snapshot-update cancel コマンドを実装して、ユーザーがこの保護メカニズムの回避をブートローダーに通知できるようにする。
  4. デバイス全体のフラッシュ時に fastboot snapshot-update cancel を発行するように、カスタム フラッシュ ツールまたはスクリプトを変更する。デバイス全体をフラッシュすると OTA が削除されるため、こうすると安全です。ツール利用時は、fastboot getvar snapshot-update-status を実装することで、ランタイムにこのコマンドを検出できます。このコマンドは、エラー状態を区別するのに役立ちます。

struct VirtualAbState {
    uint8_t StructVersion;
    uint8_t MergeStatus;
    uint8_t SourceSlot;
};

bool ShouldPreventUserdataWipe() {
    VirtualAbState state;
    if (!ReadVirtualAbState(&state)) ...
    return state.MergeStatus == MergeStatus::MERGING ||
           (state.MergeStatus == MergeStatus::SNAPSHOTTED &&
            state.SourceSlot != CurrentSlot()));
}

fastboot ツールの変更

Android 11 では、fastboot プロトコルに次の変更が加えられます。

  • getvar snapshot-update-status - ブート コントロール HAL がブートローダーに通知した値を返します。
    • ステータスが MERGING の場合、ブートローダーは merging を返します。
    • ステータスが SNAPSHOTTED の場合、ブートローダーは snapshotted を返します。
    • それ以外の場合は、ブートローダーは none を返します。
  • snapshot-update merge - マージ処理を完了します。必要に応じてリカバリまたは fastboot を起動します。このコマンドは、snapshot-update-statusmerging の場合にのみ有効で、fastbootd でのみサポートされています。
  • snapshot-update cancel - ブート コントロール HAL のマージ ステータスを CANCELLED に設定します。このコマンドは、デバイスがロックされている場合は無効です。
  • erase または wipe - metadatauserdata、またはブート コントロール HAL のマージ ステータスを保持するパーティションの erase または wipe ではスナップショットのマージ ステータスを確認します。ステータスが MERGING または SNAPSHOTTED の場合、デバイスはオペレーションを中止します。
  • set_active - アクティブ スロットを変更する set_active コマンドは、スナップショットのマージ ステータスを確認する必要があります。ステータスが MERGING の場合、デバイスはオペレーションを中止します。スロットは SNAPSHOTTED 状態で安全に変更できます。

これらの変更の目的は、デバイスが誤って起動不能になることを避けるためですが、自動化ツールの妨げとなる場合があります。すべてのパーティションをフラッシュするコンポーネント(fastboot flashall の実行など)としてコマンドを使用する場合は、次のフローを使用することをおすすめします。

  1. getvar snapshot-update-status をクエリします。
  2. merging または snapshotted の場合、snapshot-update cancel を発行します。
  3. フラッシュの手順に進みます。

ストレージ要件の削減

フル A/B ストレージが super パーティションに割り当てられたものではなく、必要に応じて /data を使用する必要があるデバイスでは、ブロック マッピング ツールを使用することを強くおすすめします。ブロック マッピング ツールは、ビルド間でブロック割り当ての一貫性を保ち、スナップショットへの不必要な書き込みを削減します。詳細については、OTA サイズの削減をご覧ください。

OTA の圧縮方法

OTA パッケージは、さまざまなパフォーマンス指標に合わせて調整できます。Android では現在、いくつかの圧縮方法(gzlz4none)がサポートされており、それぞれにインストール時間、cow のスペース使用量、起動時間、スナップショットのマージ時間のトレードオフがあります。圧縮ありの仮想 ab でデフォルトで有効になっているオプションは gz compression method です(注: 各圧縮方法のパフォーマンスは CPU の速度やストレージのスループットによって異なるため、デバイスによってパフォーマンスに差が生じる可能性があります。以下の手順で生成される OTA パッケージはすべて PostInstall を無効にしているため、起動がわずかに遅くなります。圧縮なしのフル OTA の動的パーティションの合計サイズ4.81 GB です)。

1. Google Pixel 6 Pro の増分 OTA

Postinstall フェーズなしのインストール時間 cow のスペース使用量 OTA 後の起動時間 スナップショットのマージ時間
gz 24 分 1.18 GB 40.2 秒 45.5 秒
lz4 13 分 1.49 GB 37.4 秒 37.1 秒
なし 13 分 2.90 GB 37.6 秒 40.7 秒

2. Google Pixel 6 Pro のフル OTA

Postinstall フェーズなしのインストール時間 cow のスペース使用量 OTA 後の起動時間 スナップショットのマージ時間
gz 23 分 2.79 GB 24.9 秒 41.7 秒
lz4 12 分 3.46 GB 20.0 秒 25.3 秒
なし 10 分 4.85 GB 20.6 秒 29.8 秒