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//dev/block/mapper/system
。 - 頂部是一個
dm-verity
設備,為已驗證的分區創建。該設備驗證dm-linear
設備上的塊是否已正確簽名。它顯示為/dev/block/mapper/system-verity
並且是/system
安裝點的來源。
圖 1 顯示了/system
掛載點下的堆棧的樣子。
圖 1. /system 掛載點下的堆棧
dm-快照
虛擬 A/B 依賴於dm-snapshot
,這是一個設備映射器模塊,用於對存儲設備的狀態進行快照。使用dm-snapshot
時,有四個設備在運行:
- 基本設備是快照的設備。在此頁面上,基本設備始終是動態分區,例如係統或供應商。
- 寫時復制(COW) 設備,用於記錄對基本設備的更改。它可以是任意大小,但必須足夠大以適應對基本設備的所有更改。
- 快照設備是使用
snapshot
目標創建的。對快照設備的寫入將寫入 COW 設備。從快照設備讀取從基本設備或 COW 設備讀取,具體取決於正在訪問的數據是否已被快照更改。 - 源設備是使用
snapshot-origin
目標創建的。直接從基礎設備讀取到源設備。寫入源設備直接寫入基礎設備,但通過寫入COW設備備份原始數據。
圖 2. dm-snapshot 的設備映射
壓縮快照
在 Android 12 中,由於/data
分區的空間要求可能很高,您可以在構建中啟用壓縮快照來解決/data
分區的更高空間要求。
虛擬 A/B 壓縮快照基於 Android 12 中可用的兩個新組件構建:
這些組件啟用壓縮。為實現壓縮快照功能所做的其他必要更改將在下一節中給出:壓縮快照的 COW 格式、 dm-user和Snapuserd 。
壓縮快照的 COW 格式
在 Android 12 中,壓縮快照使用新的 COW 格式。與用於未壓縮快照的內核內置格式類似,壓縮快照的 COW 格式具有元數據和數據的交替部分。原始格式的元數據只允許“替換”操作:將基礎映像中的塊X替換為快照中塊Y的內容。壓縮快照 COW 格式更具表現力,支持三種操作:
- 複製- 基礎設備中的塊X應替換為基礎設備中的塊Y。
- 替換- 基礎設備中的塊X應替換為快照中塊Y的內容。這些塊中的每一個都經過 gz 壓縮。
- 零- 基本設備中的塊X應替換為全零。
完整的 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-user
的snapuserd
用戶空間組件實現了虛擬 A/B 壓縮。
在 Virtual A/B 的未壓縮版本中(在 Android 11 及更低版本中,或在沒有壓縮快照選項的 Android 12 中),COW 設備是原始文件。啟用壓縮後,COW 將作為dm-user
設備運行,該設備連接到snapuserd
守護程序的實例。
內核不使用新的 COW 格式。所以snapuserd
組件在 Android COW 格式和內核內置格式之間轉換請求:
圖 3. snapuserd 作為 Android 和 Kernel COW 格式之間的轉換器的流程圖
這種轉換和解壓縮永遠不會發生在磁盤上。 snapuserd
組件攔截發生在內核中的 COW 讀取和寫入,並使用 Android COW 格式實現它們。
虛擬 A/B 壓縮過程
這些部分提供了有關虛擬 A/B 壓縮中使用的過程的詳細信息:讀取元數據、合併和執行初始化轉換。
讀取元數據
元數據由snapuserd
守護進程構建。元數據主要是 2 個 ID 的映射,每個 8 字節,表示要合併的扇區。在dm-snapshot
中,它被稱為disk_exception
。
struct disk_exception {
uint64_t old_chunk;
uint64_t new_chunk;
};
當舊數據塊被新數據替換時,會使用磁盤異常。
Snapuserd
守護程序通過 COW 庫讀取內部 COW 文件,並為 COW 文件中存在的每個 COW 操作構造元數據。
創建 dm- dm- snapshot
設備時,從內核中的dm-snapshot
啟動元數據讀取。
下圖提供了元數據構建的 IO 路徑的時序圖。
圖 4.元數據構建中 IO 路徑的序列流
合併
引導過程完成後,更新引擎會將插槽標記為引導成功,並通過將dm-snapshot
目標切換到dm-snapshot-merge
目標來啟動合併。
dm-snapshot
遍曆元數據並為每個磁盤異常啟動一個合併 IO。下面顯示了合併 IO 路徑的高級概述。
圖 5.合併 IO 路徑概覽
如果設備在合併過程中重新啟動,則合併在下次重新啟動時恢復,合併完成。
初始化轉換
使用壓縮快照啟動時,第一階段 init 必須啟動snapuserd
以掛載分區。這帶來了一個問題:當sepolicy
被加載和執行時, snapuserd
被置於錯誤的上下文中,並且它的讀取請求失敗,並被 selinux 拒絕。
為了解決這個問題, snapuserd
與init
同步轉換,如下所示:
- 第一階段
init
從 ramdisk 啟動snapuserd
,並將打開的文件描述符保存在環境變量中。 - 第一階段
init
將根文件系統切換到系統分區,然後執行init
的系統副本。 -
init
的系統副本將組合的 sepolicy 讀入一個字符串。 -
Init
在所有 ext4 支持的頁面上調用mlock()
。然後,它會停用快照設備的所有設備映射表,並停止snapuserd
。在此之後,禁止從分區讀取,因為這樣做會導致死鎖。 - 使用
snapuserd
的 ramdisk 副本的打開描述符,init
使用正確的 selinux 上下文重新啟動守護程序。重新激活快照設備的設備映射表。 - Init 調用
munlockall()
- 再次執行 IO 是安全的。
空間使用
下表提供了使用 Pixel 的操作系統和 OTA 大小的不同 OTA 機制的空間使用比較。
尺寸影響 | 非 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後返回的空間) | /data 1.4GB | 0 | /data 3.8GB 2 | /data 2.1GB 2 |
應用 OTA 所需的總存儲空間 | 5.9GB 3 (超級和數據) | 9GB(超級) | 8.3GB 3 (超級和數據) | 6.6GB 3 (超級和數據) |
1表示基於像素映射的假定佈局。
2假設新系統映像與原始系統映像大小相同。
3空間需求是暫時的,直到重新啟動。
要實施虛擬 A/B,或使用壓縮快照功能,請參閱實施虛擬 A/B