虛擬 A/B 概述

Android 有兩種更新機制:A/B(無縫)更新和非 A/B 更新。為了降低程式碼複雜性並增強更新過程,在 Android 11 中,這兩種機制透過虛擬 A/B 進行統一,以最小化的儲存成本為所有裝置帶來無縫更新。 Android 12 提供虛擬 A/B 壓縮選項來壓縮快照分割區。在 Android 11 和 Android 12 中,以下內容均適用:

  • 虛擬 A/B 更新與 A/B 更新一樣無縫。虛擬 A/B 更新可最大限度地減少設備離線和不可用的時間。
  • 虛擬 A/B 更新可以回滾。如果新作業系統無法啟動,裝置會自動回滾到先前的版本。
  • 虛擬 A/B 更新僅複製引導程式使用的分割區,從而最大限度地減少額外空間。其他可更新分割區會被快照

背景和術語

本節定義術語並描述支援虛擬 A/B 的技術。

裝置映射器

Device-mapper 是 Android 中常用的 Linux 虛擬區塊層。對於動態分區,像/system這樣的分區是一堆分層設備:

  • 堆疊底部是實體超級分區(例如/dev/block/by-name/super )。
  • 中間是一個dm-linear設備,指定超級分區中的哪些區塊形成給定分區。這在 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掛載點下的堆疊的樣子。

Partition stacking underneath system

圖 1. /system 掛載點下的堆疊

dm 快照

Virtual A/B 依賴dm-snapshot ,它是一個裝置映射器模組,用於對儲存裝置的狀態進行快照。使用dm-snapshot時,有四個裝置在使用:

  • 基本設備是進行快照的設備。在此頁面上,基本設備始終是動態分區,例如係統或供應商。
  • 寫入時複製(COW) 設備,用於記錄對基本設備的變更。它可以是任何大小,但必須足夠大以容納對基本設備的所有更改。
  • 快照設備是使用snapshot目標建立的。對快照設備的寫入將寫入 COW 設備。從快照設備讀取 從基本設備​​或 COW 設備讀取,取決於正在存取的資料是否已被快照更改。
  • 來源設備是使用snapshot-origin目標建立的。讀取到來源設備,直接從基礎設備讀取。寫入原始設備直接寫入基礎設備,但原始資料透過寫入 COW 設備進行備份。

Device mapping for 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 用戶

dm-user 核心模組使userspace能夠實現設備映射器塊設備。 dm-user 表條目在/dev/dm-user/<control-name>下建立一個雜項設備。 userspace進程可以輪詢設備以接收來自核心的讀寫請求。每個請求都有一個關聯的緩衝區,供用戶空間填充(用於讀取)或傳播(用於寫入)。

dm-user核心模組為核心提供了一個新的用戶可見接口,該接口不屬於上游 kernel.org 程式碼庫。在此之前,Google 保留修改 Android 中dm-user介面的權利。

快照程式

dm-usersnapuserd用戶空間元件實作虛擬 A/B 壓縮。

在 Virtual A/B 的未壓縮版本中(無論是在 Android 11 及更低版本中,還是在沒有壓縮快照選項的 Android 12 中),COW 裝置是原始檔案。啟用壓縮後,COW 將充當dm-user設備,該設備連接到snapuserd守護程序的實例。

核心不使用新的 COW 格式。因此snapuserd元件在 Android COW 格式和核心內建格式之間轉換請求:

Snapuserd component translating requests between Android COW format and kernel built-in format

圖 3. snapuserd 作為 Android 和核心 COW 格式之間的轉換器的流程圖

這種轉換和解壓縮永遠不會在磁碟上發生。 snapuserd元件攔截核心中發生的 COW 讀寫,並使用 Android COW 格式實作它們。

異或壓縮

對於搭載 Android 13 及更高版本的設備,預設啟用的 XOR 壓縮功能使用戶空間快照能夠在舊區塊和新區塊之間儲存 XOR 壓縮位元組。當虛擬 A/B 更新中僅更改區塊中的幾個位元組時,XOR 壓縮儲存方案使用的空間比預設儲存方案更少,因為快照不儲存完整的 4K 位元組。快照大小的減少是可能的,因為 XOR 資料包含許多零並且比原始區塊資料更容易壓縮。在 Pixel 設備上,XOR 壓縮可將快照大小減少 25% 到 40%。

對於升級到 Android 13 及更高版本的設備,必須啟用 XOR 壓縮。有關詳細信息,請參閱異或壓縮

虛擬 A/B 壓縮過程

本部分提供有關 Android 13 和 Android 12 中使用的虛擬 A/B 壓縮過程的詳細資訊。

讀取元資料(Android 12)

元資料由snapuserd守護程式建構。元資料主要是兩個 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路徑的時序圖。

Sequence diagram, IO path for metadata construction

圖 4.元資料建構中 IO 路徑的序列流

合併(Android 12)

啟動程序完成後,更新引擎會將插槽標記為啟動成功,並透過將dm-snapshot目標切換到dm-snapshot-merge目標來啟動合併。

dm-snapshot遍曆元資料並為每個磁碟異常啟動合併 IO。合併 IO 路徑的高階概述如下所示。

Merge IO path

圖 5.合併 IO 路徑概述

如果在合併過程中裝置重新啟動,則合併將在下次重新啟動時恢復,並完成合併。

設備映射器分層

對於搭載 Android 13 及更高版本的設備,虛擬 A/B 壓縮中的快照和快照合併過程由snapuserd用戶空間元件執行。對於升級到 Android 13 及更高版本的設備,必須啟用此功能。有關詳細信息,請參閱用戶空間合併

以下介紹Virtual A/B壓縮過程:

  1. 該框架將/system分區掛載在dm-verity設備上,該設備堆疊在dm-user設備之上。這意味著來自根檔案系統的每個 I/O 都會路由到dm-user
  2. dm-user將 I/O 路由到用戶空間snapuserd守護進程,該守護程序處理 I/O 請求。
  3. 合併操作完成後,框架會將dm-verity折疊到dm-linear ( system_base ) 之上並刪除dm-user

虛擬A/B壓縮過程

圖 6.虛擬 A/B 壓縮過程

快照合併過程可以中斷。如果在合併過程中裝置重新啟動,合併過程將在重新啟動後恢復。

初始化轉換

使用壓縮快照啟動時,第一階段 init 必須啟動snapuserd來掛載分割區。這帶來了一個問題:當載入並強制執行sepolicy時, snapuserd會被置於錯誤的上下文中,其讀取請求會因 selinux 拒絕而失敗。

為了解決這個問題, snapuserdinit同步轉換,如下所示:

  1. 第一階段init從 ramdisk 啟動snapuserd ,並將開啟的檔案描述符儲存到環境變數。
  2. 第一階段init將根檔案系統切換到系統分割區,然後執行init的系統副本。
  3. init的系統副本將組合的 sepolicy 讀取到字串中。
  4. Init在所有 ext4 支援的頁面上呼叫mlock() 。然後,它會停用快照裝置的所有裝置映射器表,並停止snapuserd 。此後禁止從分區讀取,因為這樣做會導致死鎖。
  5. 使用snapuserd的 ramdisk 副本的開啟描述符, init使用正確的 selinux 上下文重新啟動守護程序。快照設備的設備映射器表被重新啟動。
  6. Init 呼叫munlockall() - 再次執行 IO 是安全的。

空間使用

下表提供了使用 Pixel 作業系統和 OTA 大小的不同 OTA 機制的空間使用情況比較。

尺寸影響非A/B A/B虛擬A/B虛擬 A/B(壓縮)
原廠圖片4.5GB超大(3.8G鏡像+700M預留) 1 9GB超級(3.8G + 700M預留,兩個插槽) 4.5GB超大(3.8G鏡像+700M預留) 4.5GB超大(3.8G鏡像+700M預留)
其他靜態分區/快取沒有任何沒有任何沒有任何
OTA 期間的額外儲存空間(應用 OTA 後返回的空間) /數據 1.4GB 0 /數據 3.8GB 2 /數據 2.1GB 2
應用 OTA 所需的總儲存空間5.9GB 3 (超級和數據) 9GB(超級) 8.3GB 3 (超級和數據) 6.6GB 3 (超級和數據)

1表示基於像素映射的假設佈局。

2假設新系統映像與原始系統映像大小相同。

3在重新啟動之前,空間需求是暫時的。

若要實作虛擬 A/B,或使用壓縮快照功能,請參閱實作虛擬 A/B

,

Android 有兩種更新機制:A/B(無縫)更新和非 A/B 更新。為了降低程式碼複雜性並增強更新過程,在 Android 11 中,這兩種機制透過虛擬 A/B 進行統一,以最小化的儲存成本為所有裝置帶來無縫更新。 Android 12 提供虛擬 A/B 壓縮選項來壓縮快照分割區。在 Android 11 和 Android 12 中,以下內容均適用:

  • 虛擬 A/B 更新與 A/B 更新一樣無縫。虛擬 A/B 更新可最大限度地減少設備離線和不可用的時間。
  • 虛擬 A/B 更新可以回滾。如果新作業系統無法啟動,裝置會自動回滾到先前的版本。
  • 虛擬 A/B 更新僅複製引導程式使用的分割區,從而最大限度地減少額外空間。其他可更新分割區會被快照

背景和術語

本節定義術語並描述支援虛擬 A/B 的技術。

裝置映射器

Device-mapper 是 Android 中常用的 Linux 虛擬區塊層。對於動態分區,像/system這樣的分區是一堆分層設備:

  • 堆疊底部是實體超級分區(例如/dev/block/by-name/super )。
  • 中間是一個dm-linear設備,指定超級分區中的哪些區塊形成給定分區。這在 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掛載點下的堆疊的樣子。

Partition stacking underneath system

圖 1. /system 掛載點下的堆疊

dm 快照

Virtual A/B 依賴dm-snapshot ,它是一個裝置映射器模組,用於對儲存裝置的狀態進行快照。使用dm-snapshot時,有四個裝置在使用:

  • 基本設備是進行快照的設備。在此頁面上,基本設備始終是動態分區,例如係統或供應商。
  • 寫入時複製(COW) 設備,用於記錄對基本設備的變更。它可以是任何大小,但必須足夠大以容納對基本設備的所有更改。
  • 快照設備是使用snapshot目標建立的。對快照設備的寫入將寫入 COW 設備。從快照設備讀取 從基本設備​​或 COW 設備讀取,取決於正在存取的資料是否已被快照更改。
  • 來源設備是使用snapshot-origin目標建立的。讀取到來源設備,直接從基礎設備讀取。寫入原始設備直接寫入基礎設備,但原始資料透過寫入 COW 設備進行備份。

Device mapping for 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 用戶

dm-user 核心模組使userspace能夠實現設備映射器塊設備。 dm-user 表條目在/dev/dm-user/<control-name>下建立一個雜項設備。 userspace進程可以輪詢設備以接收來自核心的讀寫請求。每個請求都有一個關聯的緩衝區,供用戶空間填充(用於讀取)或傳播(用於寫入)。

dm-user核心模組為核心提供了一個新的用戶可見接口,該接口不屬於上游 kernel.org 程式碼庫。在此之前,Google 保留修改 Android 中dm-user介面的權利。

快照程式

dm-usersnapuserd用戶空間元件實作虛擬 A/B 壓縮。

在 Virtual A/B 的未壓縮版本中(無論是在 Android 11 及更低版本中,還是在沒有壓縮快照選項的 Android 12 中),COW 裝置是原始檔案。啟用壓縮後,COW 將充當dm-user設備,該設備連接到snapuserd守護程序的實例。

核心不使用新的 COW 格式。因此snapuserd元件在 Android COW 格式和核心內建格式之間轉換請求:

Snapuserd component translating requests between Android COW format and kernel built-in format

圖 3. snapuserd 作為 Android 和核心 COW 格式之間的轉換器的流程圖

這種轉換和解壓縮永遠不會在磁碟上發生。 snapuserd元件攔截核心中發生的 COW 讀寫,並使用 Android COW 格式實作它們。

異或壓縮

對於搭載 Android 13 及更高版本的設備,預設啟用的 XOR 壓縮功能使用戶空間快照能夠在舊區塊和新區塊之間儲存 XOR 壓縮位元組。當虛擬 A/B 更新中僅更改區塊中的幾個位元組時,XOR 壓縮儲存方案使用的空間比預設儲存方案更少,因為快照不儲存完整的 4K 位元組。快照大小的減少是可能的,因為 XOR 資料包含許多零並且比原始區塊資料更容易壓縮。在 Pixel 設備上,XOR 壓縮可將快照大小減少 25% 到 40%。

對於升級到 Android 13 及更高版本的設備,必須啟用 XOR 壓縮。有關詳細信息,請參閱異或壓縮

虛擬 A/B 壓縮過程

本部分提供有關 Android 13 和 Android 12 中使用的虛擬 A/B 壓縮過程的詳細資訊。

讀取元資料(Android 12)

元資料由snapuserd守護程式建構。元資料主要是兩個 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路徑的時序圖。

Sequence diagram, IO path for metadata construction

圖 4.元資料建構中 IO 路徑的序列流

合併(Android 12)

啟動程序完成後,更新引擎會將插槽標記為啟動成功,並透過將dm-snapshot目標切換到dm-snapshot-merge目標來啟動合併。

dm-snapshot遍曆元資料並為每個磁碟異常啟動合併 IO。合併 IO 路徑的高階概述如下所示。

Merge IO path

圖 5.合併 IO 路徑概述

如果在合併過程中裝置重新啟動,則合併將在下次重新啟動時恢復,並完成合併。

設備映射器分層

對於搭載 Android 13 及更高版本的設備,虛擬 A/B 壓縮中的快照和快照合併過程由snapuserd用戶空間元件執行。對於升級到 Android 13 及更高版本的設備,必須啟用此功能。有關詳細信息,請參閱用戶空間合併

以下介紹Virtual A/B壓縮過程:

  1. 該框架將/system分區掛載在dm-verity設備上,該設備堆疊在dm-user設備之上。這意味著來自根檔案系統的每個 I/O 都會路由到dm-user
  2. dm-user將 I/O 路由到用戶空間snapuserd守護進程,該守護程序處理 I/O 請求。
  3. 合併操作完成後,框架會將dm-verity折疊到dm-linear ( system_base ) 之上並刪除dm-user

虛擬A/B壓縮過程

圖 6.虛擬 A/B 壓縮過程

快照合併過程可以中斷。如果在合併過程中裝置重新啟動,合併過程將在重新啟動後恢復。

初始化轉換

使用壓縮快照啟動時,第一階段 init 必須啟動snapuserd來掛載分割區。這帶來了一個問題:當載入並強制執行sepolicy時, snapuserd會被置於錯誤的上下文中,其讀取請求會因 selinux 拒絕而失敗。

為了解決這個問題, snapuserdinit同步轉換,如下所示:

  1. 第一階段init從 ramdisk 啟動snapuserd ,並將開啟的檔案描述符儲存到環境變數。
  2. 第一階段init將根檔案系統切換到系統分割區,然後執行init的系統副本。
  3. init的系統副本將組合的 sepolicy 讀取到字串中。
  4. Init在所有 ext4 支援的頁面上呼叫mlock() 。然後,它會停用快照裝置的所有裝置映射器表,並停止snapuserd 。此後禁止從分區讀取,因為這樣做會導致死鎖。
  5. 使用snapuserd的 ramdisk 副本的開啟描述符, init使用正確的 selinux 上下文重新啟動守護程序。快照設備的設備映射器表被重新啟動。
  6. Init 呼叫munlockall() - 再次執行 IO 是安全的。

空間使用

下表提供了使用 Pixel 作業系統和 OTA 大小的不同 OTA 機制的空間使用情況比較。

尺寸影響非A/B A/B虛擬A/B虛擬 A/B(壓縮)
原廠圖片4.5GB超大(3.8G鏡像+700M預留) 1 9GB超級(3.8G + 700M預留,兩個插槽) 4.5GB超大(3.8G鏡像+700M預留) 4.5GB超大(3.8G鏡像+700M預留)
其他靜態分區/快取沒有任何沒有任何沒有任何
OTA 期間的額外儲存空間(應用 OTA 後返回的空間) /數據 1.4GB 0 /數據 3.8GB 2 /數據 2.1GB 2
應用 OTA 所需的總儲存空間5.9GB 3 (超級和數據) 9GB(超級) 8.3GB 3 (超級和數據) 6.6GB 3 (超級和數據)

1表示基於像素映射的假設佈局。

2假設新系統映像與原始系統映像大小相同。

3在重新啟動之前,空間需求是暫時的。

若要實作虛擬 A/B,或使用壓縮快照功能,請參閱實作虛擬 A/B