Android 官方AB Update說明
A/B 系統更新,也稱為無縫更新,用於確保可運行的啟動系統在無線 (OTA) 更新期間能夠保留在磁盤上。這樣可以降低更新之后設備無法啟動的可能性,也就是說,用戶需要將設備送到維修/保修中心進行更換和刷機的情況將有所減少。
用戶在 OTA 期間可以繼續使用設備。在更新過程中,僅當設備重新啟動到更新后的磁盤分區時,會發生一次宕機情況。即使 OTA 失敗,設備也仍然可以使用,因為它會啟動到 OTA 之前的磁盤分區。您可以再次嘗試下載 OTA。建議僅針對新設備通過 OTA 實現 A/B 系統更新。
A/B 系統更新將影響:
- 與引導加載程序的交互
- 分區選項
- 構建流程
- OTA 更新軟件包的生成
現有的 dm-verity 功能可確保設備會啟動未損壞的映像。如果設備因糟糕的 OTA 或 dm-verity 問題而無法啟動,則可以重新啟動到原來的映像。
A/B 系統之所以非常強大,是因為任何錯誤(如 I/O 錯誤)都只能影響未使用的分區集,並且可以進行重試。由於 I/O 負載被特意控制在較低水平,以免影響用戶體驗,因此此類錯誤不太可能會發生。
OTA 更新可以在系統運行時進行,而不會打斷用戶,更新的內容包括重新啟動后進行的應用優化。此外,緩存分區不再用於存儲 OTA 更新軟件包;無需調整緩存分區的大小。
概覽
A/B 系統更新使用稱為 update_engine
的后台守護進程以及兩組分區。這兩組分區稱為插槽,通常為插槽 A 和插槽 B。系統從其中一個插槽(“當前插槽”)運行,但運行的系統不會訪問“未使用的”插槽中的分區(用於正常操作)。
此功能的目標是將未使用的插槽保留為后備插槽,從而使更新具有抗故障性。如果更新期間或更新后立即出現錯誤,則系統可以回滾至原來的插槽並繼續正常運行。為實現這一目標,“當前”插槽所使用的所有分區(包括只有一個副本的分區)都不應作為 OTA 更新的一部分進行更新。
每個插槽都有“可啟動”屬性,該屬性會聲明相應插槽是否包含設備可以從中啟動的正確系統。系統運行時,當前插槽肯定可以啟動,但是另一個插槽中可能包含舊(仍然正確)版本的系統,也可能包含較新版本或無效的數據。無論當前插槽是哪一個,都有一個插槽是活動插槽或首選插槽。下次啟動時,引導加載程序將從活動插槽中啟動。最后,每個插槽都有由用戶空間設置的“成功”屬性,該屬性只有在相應插槽也可以啟動時才具有相關性。
成功的插槽應該能夠自行啟動、運行和更新。未標記為成功的可啟動插槽(嘗試從其啟動幾次之后)應由引導加載程序標記為不可啟動,包括將活動插槽更改為其他可啟動插槽(通常更改為在嘗試啟動到新的活動插槽之前運行的插槽)。界面的具體詳細信息在 boot_control.h
中進行了定義。
引導加載程序狀態示例
update_engine
(以及其他可能的守護進程)使用 boot_control
HAL 指示引導加載程序從何處啟動。
以下是常見的示例情景及其相關的狀態:
- 正常情況:系統正在從其當前插槽(插槽 A 或插槽 B)運行。目前為止尚未應用任何更新。系統的當前插槽是可啟動、成功且活動的插槽。
- 正在更新:系統正在從插槽 B 運行,因此,插槽 B 是可啟動、成功且活動的插槽。由於插槽 A 中的內容正在更新,但是尚未完成,因此插槽 A 標記為不可啟動。在此狀態下,應繼續從插槽 B 重新啟動。
- 已應用更新,正在等待重新啟動:系統正在從插槽 B 運行,插槽 B 的狀態為可啟動且成功,但是插槽 A 過去標記為活動(因此現在標記為可啟動)。插槽 A 尚未被標記為成功,引導加載程序應該嘗試從插槽 A 啟動幾次。
- 系統重新啟動到新的更新:系統首次從插槽 A 運行,插槽 B 的狀態仍為可啟動且成功,而插槽 A 僅可啟動,且仍然處於活動但不成功的狀態。在進行幾次檢查之后,用戶空間守護進程應將插槽 A 標記為成功。
更新引擎功能
守護進程 update_engine
在后台運行,並會使系統做好啟動到已更新的新版本的准備。守護進程 update_engine
本身不會參與到啟動流程中,且更新期間可以執行的操作會受到限制。守護進程 update_engine
可以執行以下操作:
- 根據 OTA 軟件包的指示,從當前插槽 A/B 分區讀取數據,然后向未使用的插槽 A/B 分區中寫入數據
- 在預定義的工作流程中調用
boot_control
界面 - 根據 OTA 軟件包的指示,在將數據寫入所有未使用的插槽分區之后,從新分區運行安裝后的程序
下文對安裝后的步驟進行了詳細介紹。注意,守護進程 update_engine
受 SELinux 策略及當前插槽中的功能限制;在系統啟動到新版本之前,這些策略和功能無法更新。要實現穩健性目標,更新流程不應執行以下操作:
- 修改分區表
- 修改當前插槽中分區的內容
- 修改恢復出廠設置時無法擦除的非 A/B 分區的內容
A/B 更新過程
當 OTA 軟件包(在代碼中稱為有效負荷)可供下載時,更新流程便開始了。設備中的策略可能會基於電池電量、用戶活動、是否連接到充電器或其他策略延遲有效負荷的下載和應用。不過,由於更新在后台運行,因此用戶可能不知道更新正在進行,而且更新流程可能隨時會由於策略或意外重新啟動而被中斷。
有效負荷可用后更新流程中的步驟將如下所示:
第 1 步:通過 markBootSuccessful()
將當前插槽(或“源插槽”)標記為成功(如果尚未標記)。
第 2 步:通過調用函數 setSlotAsUnbootable()
將未使用的插槽(或“目標插槽”)標記為不可啟動。
在更新開始時,當前插槽會始終標記為成功,以防引導加載程序回退至未使用的插槽(很快將有無效數據)。如果系統已可以開始應用更新,即使其他主要組件已受損(如崩潰循環中的界面),當前插槽也會標記為成功,因為可以通過推送新軟件來修復這些主要問題。
更新有效負荷是不透明的 Blob,其中包含更新到新版本的相應指令。更新有效負荷主要由兩部分組成:元數據以及與指令相關的額外數據。元數據相對較小,其中包含在目標插槽上生成和驗證新版本的操作的列表。例如,某個操作可能會解壓縮特定 Blob 並將其寫入目標分區中的特定塊,或者從源分區讀取數據、向其應用二進制補丁程序,然后寫入目標分區中的特定塊。與操作相關聯的額外數據並未包含在元數據中,此類數據在更新有效負荷中所占比重較大,其中將包含這些示例中的已壓縮 Blob 或二進制補丁程序。
第 3 步:下載有效負荷元數據。
第 4 步:對於元數據中定義的每項操作,將按順序發生以下行為:關聯的數據(如果有)下載到內存中、操作得到應用、關聯的內存被舍棄。
這兩個步驟占用了大部分更新時間,因為它們涉及寫入和下載大量數據,並且可能會因策略或重新啟動等原因而中斷。
第 5 步:針對預期哈希重新讀取並驗證整個分區。
第 6 步:運行安裝后步驟(如果有)。
如果在執行任一步驟的過程中出現錯誤,則更新失敗,系統可能會通過其他有效負荷重新嘗試更新。如果上述所有步驟均成功完成,則更新成功,系統會執行最后一個步驟。
第 7 步:通過調用 setActiveBootSlot()
將未使用的插槽標記為活動。
將未使用的插槽標記為活動並不意味着它會完成啟動。如果它未讀取到成功的狀態,引導加載程序或系統本身可以將插槽的活動狀態切換回來。
安裝后的步驟
安裝后的步驟包括從“新更新”版本中運行仍在原來版本中運行的程序。如果此步驟已在 OTA 軟件包中定義,則為強制性步驟,且程序必須返回退出代碼 0
,否則更新失敗。
對於其中已定義安裝后步驟的每個分區,update_engine
會將新分區裝載到特定位置,並執行與裝載的分區對應的 OTA 中指定的程序。例如,如果安裝后程序在相應系統分區中定義為 usr/bin/postinstall
,則系統會將此來自未使用插槽的分區裝載到一個固定位置(如 /postinstall_mount
中),然后執行 /postinstall_mount/usr/bin/postinstall
命令。注意,要使此步驟生效,需要滿足以下條件:
- 舊內核需要能夠裝載新的文件系統格式。文件系統類型不能更改,除非舊內核中有為其提供的支持(包括使用 SquashFS 等壓縮文件系統時所用的壓縮算法等詳細信息)。
- 舊內核需要了解新分區的安裝后程序格式。如果使用的是 ELF 二進制文件,則該文件應該與舊內核兼容(例如,如果棄用 32 位版本架構,並改為使用 64 位版本架構,則 64 位的新程序應該可以在舊版 32 位內核上運行)。此外,庫將會從舊系統映像而非新系統映像加載,除非加載程序 (
ld
) 收到使用其他路徑或構建靜態二進制文件的指令。 - 新的安裝后程序將受到舊系統中定義的 SELinux 策略的限制。
一種示例情況是,將 Shell 腳本用作安裝后程序(由舊系統中頂部帶有 #!
標記的 Shell 二進制文件解析), 然后從新環境設置庫路徑,用於執行更復雜的二進制安裝后程序。
另一種示例情況是,從專用的較小分區執行安裝后步驟,以便主系統分區中的文件系統格式可以得到更新,同時不會產生向后兼容問題或引發 stepping-stone 更新,這樣一來,用戶便可以從出廠映像直接更新到最新版本。
根據 SELinux 策略,安裝后步驟適用於在指定設備上執行設計所需的任務或其他需要盡可能完成的任務:更新支持 A/B 的固件或引導加載程序、為新版本准備部分數據庫的副本等等。該步驟不適用於重新啟動之前的一次性錯誤修復(此類修復需要無法預見的權限)。
所選的安裝后程序在 postinstall
SELinux 環境中運行。新裝載的分區中的所有文件都將使用 postinstall_file
進行標記,無論它們在重新啟動到新系統后的屬性如何,都是如此。對新系統中 SELinux 屬性實施的更改不會影響安裝后步驟。如果安裝后的程序需要額外的權限,則必須將這些權限添加到安裝后的環境中。
實現
希望實現該功能的 OEM 和 SoC 供應商必須向其引導加載程序中添加以下支持:
- 將正確參數傳遞給內核
- 實現
boot_control
HAL (https://android.googlesource.com/platform/hardware/libhardware/+/master/include/hardware/boot_control.h) - 實現狀態機,如圖 1 所示:
圖 1. 引導加載程序狀態機
可使用 bootctl
實用工具測試啟動控件 HAL。
已針對 Brillo 實施了一些測試:
- https://android.googlesource.com/platform/system/extras/+/refs/heads/master/tests/bootloader/
- https://chromium.googlesource.com/chromiumos/third_party/autotest/+/master/server/site_tests/brillo_BootLoader/brillo_BootLoader.py
內核補丁程序
- https://android-review.googlesource.com/#/c/158491/
- https://android-review.googlesource.com/#/q/status:merged+project:kernel/common+branch:android-3.18+topic:A_B_Changes_3.18
內核命令行參數
內核命令行參數必須包含以下額外參數:
skip_initramfs rootwait ro init=/init root="/dev/dm-0 dm=system none ro,0 1 \
android-verity <public-key-id> <path-to-system-partition>"
值 <public-key-id>
是用於驗證 verity 表簽名的公鑰 ID(請參閱 dm-verity)。
要將包含相應公鑰的 .X509 證書添加到系統密鑰環,請執行以下操作:
- 將設置為
.der
格式的 .X509 證書復制到kernel
的根目錄。您可以使用openssl
命令將證書格式從.pem
轉換為.der
(如果 .X509 證書采用.pem
格式):
openssl x509 -in <x509-pem-certificate> -outform der -out <x509-der-certificate>
- 復制到內核版本根目錄后,構建
zImage
以將該證書添加為系統密鑰環的一部分。您可以通過以下procfs
條目(需要啟用KEYS_CONFIG_DEBUG_PROC_KEYS
)驗證該步驟:
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
如果 .X509 證書添加成功,則表示系統密鑰環中存在相應公鑰。突出顯示部分表示公鑰 ID。
下一步是將空格替換為“#”,並將其作為 <public-key-id>
在內核命令行中傳遞。例如,在上述示例中,以下證書在<public-key-id>
的位置傳遞:Android:#7e4333f9bba00adfe0ede979e28ed1920492b40f
恢復
恢復 RAM 磁盤現已包含在 boot.img
文件中。進入恢復模式時,引導加載程序無法在內核命令行中添加skip_initramfs
選項。
構建變量
必須針對 A/B 目標定義以下變量:
`AB_OTA_UPDATER := true`
`AB_OTA_PARTITIONS := \`
` boot \`
` system \`
` vendor`
以及通過 update_engine
更新的其他分區(無線裝置、引導加載程序等)。
BOARD_BUILD_SYSTEM_ROOT_IMAGE := true
TARGET_NO_RECOVERY := true`
BOARD_USES_RECOVERY_AS_BOOT := true
PRODUCT_PACKAGES += \`
update_engine \`
update_verifier`
(可選)針對調試版本定義以下變量:
PRODUCT_PACKAGES_DEBUG += update_engine_client
無法針對 A/B 目標定義以下變量:
BOARD_RECOVERYIMAGE_PARTITION_SIZE
BOARD_CACHEIMAGE_PARTITION_SIZE
BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE
分區
- A/B 設備不需要恢復分區或緩存分區,因為 Android 已不再使用這些分區。數據分區現在用於存儲下載的 OTA 軟件包,而恢復映像代碼位於啟動分區。
- A/B 化的所有分區應命名如下(插槽始終被命名為
a
、b
等):boot_a
、boot_b
、system_a
、system_b
、vendor_a
、vendor_b
。
Fstab
參數 slotselect
必須位於 A/B 化分區的行中。例如:
<path-to-block-device>/vendor /vendor ext4 ro
wait,verify=<path-to-block-device>/metadata,slotselect
請注意,不應選擇名稱為 vendor
的分區,而應選擇分區 vendor_a
或 vendor_b
並將其裝載到 /vendor
裝載點上。
內核插槽參數
應通過特定的 DT 節點 (/firmware/android/slot_suffix
) 或 androidboot.slot_suffix
命令行參數傳遞當前插槽后綴。
或者,如果引導加載程序實現 fastboot,則應支持以下命令和變量:
命令
set_active <slot>
- 將當前活動插槽設置為指定插槽。此外,還必須清除該插槽的不可啟動標記,並將重試計數重置為默認值。
變量
has-slot:<partition-base-name-without-suffix>
- 如果指定分區支持插槽,則返回“yes”,否則,返回“no”。current-slot
- 返回接下來將從中啟動的插槽后綴。slot-count
- 返回一個表示可用插槽數量的整數。目前支持兩個插槽,因此,該值為2
。slot-successful:<slot-suffix>
- 如果指定插槽已標記為成功啟動,則返回“yes”,否則,返回“no”。slot-unbootable:<slot-suffix>
- 如果指定插槽標記為不可啟動,則返回“yes”,否則,返回“no”。slot-retry-count:
- 可以嘗試啟動指定插槽的剩余重試次數。- 這些變量都應顯示在
fastboot getvar all
下
生成 OTA 軟件包
OTA 軟件包工具遵循與非 A/B 設備一樣的命令。target_files.zip
文件必須通過為 A/B 目標定義版本變量生成。OTA 軟件包工具會自動識別並生成格式適用於 A/B 更新程序的軟件包。
例如,使用以下命令生成完整 OTA:
./build/tools/releasetools/ota_from_target_files \
dist_output/tardis-target_files.zip ota_update.zip
或者生成增量 OTA:
./build/tools/releasetools/ota_from_target_files \
-i PREVIOUS-tardis-target_files.zip \
dist_output/tardis-target_files.zip incremental_ota_update.zip
配置
分區
更新引擎可以更新同一磁盤中定義的任何一對 A/B 分區。
一對分區有一個公共前綴(例如 system
或 boot
)及按插槽划分的后綴(例如 _a
)。有效負荷生成器為其定義更新的分區列表由 AB_OTA_PARTITIONS
make 變量配置。例如,如果磁盤中有一對分區 bootloader_a
和 booloader_b
(_a
和 _b
為插槽后綴),則可以通過在產品或單板配置中指定以下變量來更新這些分區:
AB_OTA_PARTITIONS := \
boot \
system \
bootloader
由更新引擎更新的所有分區不得由系統的其余部分修改。在增量更新期間,來自當前插槽的二進制數據將用於在新插槽中生成數據。任何修改都可能導致新插槽數據在更新過程中無法通過驗證,從而導致更新失敗。
安裝后
對於每個已更新的分區,都可以使用一組鍵值對配置不同的安裝后步驟。
要在新映像中運行位於 /system/usr/bin/postinst
的程序,請指定與系統分區中相應文件系統的根目錄對應的路徑。例如,usr/bin/postinst
的對應路徑為 system/usr/bin/postinst
(如果未使用 RAM 磁盤)。此外,請指定要傳遞到 mount(2)
系統調用的文件系統類型。將以下內容添加到產品或設備的 .mk
文件(如果適用):
AB_OTA_POSTINSTALL_CONFIG += \
RUN_POSTINSTALL_system=true \
POSTINSTALL_PATH_system=usr/bin/postinst \
FILESYSTEM_TYPE_system=ext4
后台中的應用編譯
要在 A/B 更新的后台編譯應用,需要對產品的設備配置(位於產品的 device.mk 中)進行以下兩項補充:
- 向版本中添加原生組件。這樣可以確保編譯腳本和二進制文件能夠編譯並添加到系統映像中。
# A/B OTA dexopt package
PRODUCT_PACKAGES += otapreopt_script
- 將編譯腳本與
update_engine
相關聯,以便它可以作為安裝后步驟運行。
# 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
請參閱 DEX_PREOPT 文件的首次啟動安裝,以將預選文件安裝到未使用的第二個系統分區中。