仮想 A/B の概要

Android には、A/B(シームレス)アップデートと非 A/B アップデートの 2 つのアップデート メカニズムがあります。コードの複雑性を削減してアップデート プロセスを強化するため、Android 11 では、仮想 A/B によって 2 つのメカニズムを統合し、ストレージのコストを最小限に抑えつつ、すべてのデバイスにシームレスなアップデートを提供します。Android 12 には、仮想 A/B 圧縮を使用して、スナップショットされたパーティションを圧縮するオプションがあります。Android 11 と Android 12 は、どちらも以下の特徴を備えています。

  • 仮想 A/B アップデートは A/B アップデートと同様にシームレスです。仮想 A/B アップデートにより、デバイスがオフライン状態で使用できない時間を最小限に抑えられます。
  • 仮想 A/B アップデートはロールバックできます。新しい OS の起動に失敗した場合、デバイスは自動的に以前のバージョンにロールバックされます。
  • 仮想 A/B アップデートは、ブートローダーで使用されるパーティションのみを複製することにより、最小限の追加スペースを使用します。その他の更新可能なパーティションはスナップショットされます。

背景と用語

このセクションでは、仮想 A/B に関連する用語を定義し、仮想 A/B を支えるテクノロジーについて説明します。

デバイス マッパー

デバイス マッパーは、Android で頻繁に使用される Linux 仮想ブロックレイヤです。動的パーティションの場合、/system などのパーティションはレイヤ化されたデバイスのスタックです。

  • スタックの下部には、物理 super パーティション(/dev/block/by-name/super など)があります。
  • 中央の dm-linear デバイスでは、super パーティション内のブロックのうち、目的のパーティションを形成するものを指定します。これは A/B デバイスでは /dev/block/mapper/system_[a|b]、非 A/B デバイスでは /dev/block/mapper/system として表示されます。
  • 上部には、確認済みパーティション用に作成された dm-verity デバイスがあります。このデバイスは、dm-linear デバイスのブロックが正しく署名されていることを検証します。これは /dev/block/mapper/system-verity として表示され、/system マウント ポイントのソースとなります。

図 1 は、/system マウント ポイントの下のスタックを示しています。

system の下にスタックされているパーティション

図 1. /system マウント ポイントの下のスタック

dm-snapshot

仮想 A/B は dm-snapshot に依存します。これは、ストレージ デバイスの状態をスナップショットするためのデバイス マッパー モジュールです。dm-snapshot を使用する場合、次の 4 つのデバイスが機能します。

  • ベースデバイスは、スナップショットされるデバイスです。このページでは、ベースデバイスは常にシステムやベンダーなどの動的パーティションです。
  • 書き込み時コピー(COW)デバイスは、ベースデバイスへの変更をログに記録するためのデバイスです。任意のサイズに設定できますが、ベースデバイスのすべての変更に対応できる大きさである必要があります。
  • スナップショット デバイスは、snapshot ターゲットを使用して作成されます。スナップショット デバイスへの書き込みは、COW デバイスにも書き込まれます。スナップショット デバイスからの読み取りでは、アクセス対象のデータがスナップショットによって変更されているかどうかに応じて、ベースデバイスまたは COW デバイスから読み取りが実行されます。
  • オリジン デバイスは snapshot-origin ターゲットを使用して作成されます。オリジン デバイスからの読み取りでは、ベース デバイスから直接読み取りを行います。オリジン デバイスへの書き込みではベースデバイスに直接書き込みが実行されますが、元のデータは COW デバイスへの書き込みによりバックアップされます。

dm-snapshot のデバイス マッピング

図 2. dm-snapshot のデバイス マッピング

圧縮スナップショット

Android 12 以降では、/data パーティションに必要なスペースが大きくなる可能性があるため、ビルドで圧縮スナップショットを有効にして、/data パーティションのスペース要件の高さに対処できます。

仮想 A/B の圧縮スナップショットは、Android 12 以降で利用できる以下のコンポーネントの上に構築されます。

  • dm-user: FUSE に似たカーネル モジュール。ユーザースペースにブロック デバイスを実装できます。
  • snapuserd: 新しいスナップショット形式を実装するユーザースペース デーモン。

これらのコンポーネントにより、圧縮が可能になります。圧縮スナップショット機能を実装するために必要なその他の変更については、この後のセクション(圧縮スナップショットの COW 形式dm-usersnapuserd)をご覧ください。

圧縮スナップショットの COW 形式

Android 12 以降、圧縮スナップショットは COW 形式を使用します。非圧縮スナップショットに使用されるカーネルの組み込み形式と同様に、圧縮スナップショットの COW 形式にはメタデータのセクションとデータのセクションが交互に含まれています。元の形式のメタデータは、置換オペレーションのみでした。ベース画像のブロック X をブロック Y の内容で置き換えます。圧縮スナップショットの COW 形式は表現力がさらに高くなっており、次のオペレーションをサポートしています。

  • コピー: ベースデバイス内のブロック X が、ベースデバイス内のブロック Y に置き換えられます。
  • 置換: ベースデバイス内のブロック X が、スナップショット内のブロック Y の内容に置き換えられます。それぞれのブロックは gz 形式で圧縮されます。
  • ゼロ: ベースデバイス内のブロック X がすべてゼロに置き換えられます。
  • XOR: COW デバイスは、ブロック X とブロック Y の間で XOR 圧縮されたバイトを格納します。(Android 13 以降で利用できます)。

フル OTA アップデートは、「置換」オペレーションと「ゼロ」オペレーションのみで構成されます。増分 OTA アップデートには、追加で「コピー」オペレーションを含めることができます。

Android 12 の dm-user

dm-user カーネル モジュールを使用すると、userspace にデバイス マッパー ブロック デバイスを実装できます。dm-user テーブル エントリは、/dev/dm-user/<control-name> の下にその他デバイスを作成します。userspace プロセスは、カーネルからの読み取りリクエストと書き込みリクエストを受信するためにデバイスをポーリングできます。各リクエストには、ユーザースペースがデータ移入(読み取りの場合)または伝播(書き込みの場合)を行うためのバッファが関連付けられています。

dm-user カーネル モジュールは、アップストリームの kernel.org コードベースの一部ではない、ユーザーから見える新しいユーザー インターフェースをカーネルに提供します。現時点では、Google は Android の dm-user インターフェースを変更する権限を有しています。

snapuserd

dm-user に対する snapuserd ユーザースペース コンポーネントは、仮想 A/B 圧縮を実装します。

仮想 A/B の非圧縮バージョン(Android 11 以前、または圧縮スナップショット オプションなしの Android 12)では、COW デバイスは RAW ファイルです。圧縮が有効になっている場合、COW は代わりに dm-user デバイスとして機能し、snapuserd デーモンのインスタンスに接続されます。

カーネルは新しい COW 形式を使用しません。したがって、snapuserd コンポーネントは、Android の COW 形式とカーネルの組み込み形式の間でリクエストを変換します。

Android の COW 形式とカーネルの組み込み形式の間でリクエストを変換する snapuserd コンポーネント

図 3. Android の COW 形式とカーネルの形式を変換する snapuserd のフロー図

この変換と解凍はディスク上では行われません。snapuserd コンポーネントは、カーネル内で発生する COW の読み取りと書き込みをインターセプトし、Android COW 形式を使用してそれらを実装します。

XOR 圧縮

Android 13 以降を搭載したデバイスの場合、デフォルトで有効になっている XOR 圧縮機能により、ユーザー空間のスナップショットを使用して、古いブロックと新しいブロックの間で XOR 圧縮されたバイトを格納できます。仮想 A/B アップデートでブロック内の数バイトのみが変更される場合、スナップショットが 4K バイトすべてを保存するわけではないため、XOR 圧縮ストレージ スキームが使用する容量はデフォルトのストレージ スキームよりも少なくなります。XOR データはゼロを多く含み、未加工のブロックデータよりも圧縮しやすいため、スナップショット サイズを削減できます。Pixel デバイスでは、XOR 圧縮によりスナップショット サイズが 25~40% 削減されます。

Android 13 以降にアップグレードするデバイスでは、XOR 圧縮を有効にする必要があります。詳細については、XOR 圧縮をご覧ください。

仮想 A/B 圧縮プロセス

このセクションでは、Android 13 と Android 12 で使用される仮想 A/B 圧縮プロセスについて説明します。

メタデータの読み取り(Android 12)

メタデータは snapuserd デーモンによって構築されます。メタデータは主として 2 つの ID のマッピングです。各 ID は 8 バイトで、統合されるセクターを表します。dm-snapshot では disk_exception といいます。

struct disk_exception {
    uint64_t old_chunk;
    uint64_t new_chunk;
};

ディスク例外は、古いデータチャンクが新しいデータチャンクに置き換えられるときに使用されます。

snapuserd デーモンは、COW ライブラリを介して内部 COW ファイルを読み取り、COW ファイル内に存在する各 COW オペレーションのメタデータを構築します。

メタデータの読み取りは、dm- snapshot デバイスが作成されたときに、カーネル内の dm-snapshot から開始されます。

メタデータ構築における IO パスのシーケンス図を以下に示します。

メタデータ構築における IO パスのシーケンス図

図 4. メタデータ構築における IO パスのシーケンス フロー

統合(Android 12)

起動プロセスが完了すると、アップデート エンジンはスロットを起動成功としてマークし、dm-snapshot ターゲットを dm-snapshot-merge ターゲットに切り替えることで統合を開始します。

dm-snapshot はメタデータを順に確認し、ディスク例外ごとに統合 IO を開始します。統合 IO パスの概要を以下に示します。

統合 IO パス

図 5. 統合 IO パスの概要

統合プロセス中にデバイスが再起動されると、次回の再起動時に統合が再開され、統合が完了します。

デバイス マッパーのレイヤ化

Android 13 以降を搭載したデバイスの場合、仮想 A/B 圧縮におけるスナップショットとスナップショットの統合プロセスは、snapuserd ユーザー空間コンポーネントによって実施されます。Android 13 以降にアップグレードするデバイスでは、この機能を有効にする必要があります。詳しくは、ユーザー空間の統合をご覧ください。

仮想 A/B 圧縮プロセスは次のとおりです。

  1. フレームワークは、dm-user デバイスの上にスタックされた dm-verity デバイスから /system パーティションをマウントします。つまり、ルート ファイル システムからの I/O はすべて dm-user にルーティングされます。
  2. dm-user は、I/O リクエストを処理するユーザー空間 snapuserd デーモンに I/O をルーティングします。
  3. 統合オペレーションが完了すると、フレームワークは dm-linearsystem_base)の上の dm-verity を閉じ、dm-user を削除します。

仮想 A/B 圧縮プロセス

図 6. 仮想 A/B 圧縮プロセス

スナップショットの統合プロセスは中断できます。統合プロセス中にデバイスが再起動された場合、統合プロセスは再起動後に再開されます。

init 遷移

圧縮スナップショットで起動する場合、第 1 ステージである init は、snapuserd を開始してパーティションをマウントする必要があります。これにより、sepolicy が読み込まれて適用される際に、snapuserd が誤ったコンテキストに配置され、その読み取りリクエストが SELinux の拒否が原因で失敗するという問題が発生します。

これに対処するため、snapuserd は、lock-step で init の遷移を次のように行います。

  1. 第 1 ステージである init は、RAM ディスクから snapuserd を起動し、それに対するオープン状態の file-descriptor を環境変数に保存します。
  2. 第 1 ステージである init は、ルート ファイルシステムをシステム パーティションに切り替えた後、init のシステムコピーを実行します。
  3. init のシステムコピーは、結合された sepolicy を文字列に読み込みます。
  4. Init は、ext4 を利用するすべてのページで mlock() を呼び出します。次に、スナップショット デバイスのすべてのデバイス マッパー テーブルを非アクティブ化し、snapuserd を停止します。これ以降、パーティションからの読み取りは、デッドロックが発生するため禁止されます。
  5. init は、snapuserd の RAM ディスクコピーに対するオープン状態の記述子を使用して、正しい SELinux コンテキストでデーモンを再起動します。スナップショット デバイス用のデバイス マッパー テーブルが再アクティブ化されます。
  6. init は munlockall() を呼び出します。これで、再び IO を安全に実行できるようになります。

スペース使用量

次の表は、Pixel の OS と OTA サイズを使用するさまざまな OTA メカニズムのスペース使用量を比較したものです。

サイズへの影響 非 A/B A/B 仮想 A/B 仮想 A/B(圧縮)
元のファクトリー イメージ 4.5 GB super(3.8 G イメージ + 700 M 予約済み)1 9 GB super(3.8 G + 700 M 予約済み、2 スロット) 4.5 GB super(3.8 G イメージ + 700 M 予約済み) 4.5 GB super(3.8 G イメージ + 700 M 予約済み)
その他の静的パーティション /cache なし なし なし
OTA 時の追加ストレージ(OTA の適用後に返されるスペース) /data で 1.4 GB 0 /data で 3.8 GB2 /data で 2.1 GB2
OTA の適用に必要な合計ストレージ 5.9 GB3(super と data) 9 GB(super) 8.3 GB3(super と data) 6.6 GB3(super と data)

1 Pixel マッピングに基づく想定レイアウトを示します。

2 新しいシステム イメージが元のイメージと同じサイズであると想定しています。

3 スペース要件は再起動までの一時的な要件です。

仮想 A/B を実装する方法または圧縮スナップショット機能を使用する方法については、仮想 A/B の実装をご覧ください。