Android A/B(無縫)系統更新
原文:https://source.android.google.cn/devices/tech/ota/ab/
A/B 系統更新(也稱為無縫更新)的目標是確保在無線下載 (OTA) 更新期間在磁碟上保留一個可正常啟動和使用的系統。採用這種方式可以降低更新之後裝置無法啟動的可能性,這意味著使用者需要將裝置送到維修和保修中心進行更換和刷機的情況將會減少。其他某些商業級作業系統(例如 ChromeOS)也成功使用了 A/B 更新機制。
要詳細瞭解 A/B 系統更新,請參見分割槽選擇(槽位)一節。
A/B 系統更新可帶來以下好處:
- OTA 更新可以在系統執行期間進行,而不會打斷使用者。使用者可以在 OTA 期間繼續使用其裝置。在更新期間,唯一的一次宕機發生在裝置重新啟動到更新後的磁碟分割槽時。
- 更新後,重新啟動所用的時間不會超過常規重新啟動所用的時間。
- 如果 OTA 無法應用(例如,因為刷機失敗),使用者將不會受到影響。使用者將繼續執行舊的作業系統,並且客戶端可以重新嘗試進行更新。
- 如果 OTA 更新已應用但無法啟動,裝置將重新啟動回舊分割槽,並且仍然可以使用。客戶端可以重新嘗試進行更新。
- 任何錯誤(例如 I/O 錯誤)都只會影響未使用的分割槽組,並且使用者可以進行重試。由於 I/O 負載被特意控制在較低水平,以免影響使用者體驗,因此發生此類錯誤的可能性也會降低。
- 更新包可以流式傳輸到 A/B 裝置,因此在安裝之前不需要先下載更新包。流式更新意味著使用者沒有必要在
/data
/cache
上留出足夠的可用空間來儲存更新包。 - 快取分割槽不再用於儲存 OTA 更新包,因此無需確保快取分割槽的大小要足以應對日後的更新。
- dm-verity 可保證裝置將使用未損壞的啟動映像。如果裝置因 OTA 錯誤或 dm-verity 問題而無法啟動,則可以重新啟動到舊映像。(Android 驗證啟動不需要 A/B 更新。)
關於 A/B 系統更新
進行 A/B 更新時,客戶端和系統都需要進行更改。不過,OTA 更新包伺服器應該不需要進行更改:更新包仍通過 HTTPS 提供。對於使用 Google OTA 基礎架構的裝置,系統更改全部是在 AOSP 中進行,並且客戶端程式碼由 Google Play 服務提供。不使用 Google OTA 基礎架構的原始裝置製造商 (OEM) 將能夠重複使用 AOSP 系統程式碼,但需要自行提供客戶端。
如果 OEM 自行提供客戶端,客戶端需要:
- 確定何時進行更新。由於 A/B 更新是在後臺進行,因此不再需要由使用者啟動。為了避免干擾使用者,建議將更新安排在裝置處於閒時維護模式(如夜間)並已連線到 WLAN 網路時進行。不過,客戶端可以使用您希望使用的任何啟發法。
- 向 OTA 更新包伺服器進行核查,確定是否有可用的更新。這應與您現有的客戶端程式碼大體相同,不過您需要表明相應裝置支援 A/B 更新。(Google 的客戶端還包含立即檢查按鈕,以便使用者檢查是否有最新更新。)
- 呼叫
update_engine
(使用 HTTPS 網址),以獲取更新包(假設有可用的更新包)。update_engine
將在流式傳輸更新包的同時,在當前未使用的分割槽上更新原始資料塊。 - 根據
update_engine
結果程式碼向您的伺服器報告安裝是成功了還是失敗了。如果更新已成功應用,update_engine
將會告知引導載入程式在下次重新啟動時啟動到新的作業系統。如果新的作業系統無法啟動,引導載入程式將會回退到舊的作業系統,因此無需在客戶端執行任何操作。如果更新失敗,客戶端將需要根據詳細的錯誤程式碼確定何時(以及是否)重試。例如,優秀的客戶端能夠識別出是一部分(“diff”)OTA 更新包失敗,並改為嘗試完整的 OTA 更新包。
客戶端可能會:
- 顯示通知,以提醒使用者重新啟動系統。如果您想要實施鼓勵使用者定期更新的政策,則可以將該通知新增到客戶端。如果客戶端不提示使用者,使用者將會在下次重新啟動系統時收到更新。(Google 的客戶端會有延遲,該延遲可按每次更新進行配置。)
- 顯示通知,以告知使用者他們是啟動到了新的作業系統版本,還是應啟動到新的作業系統版本,但卻回退到了舊的作業系統版本。(Google 的客戶端通常不會顯示此類通知。)
在系統方面,A/B 系統更新會影響以下各項:
- 分割槽選擇(槽位)、
update_engine
守護程序,以及引導載入程式互動(如下所述) - 編譯過程和 OTA 更新包生成(如實現 A/B 更新中所述)
注意:只有對於新裝置,才建議通過 OTA 實現 A/B 系統更新。
分割槽選擇(槽位)
A/B 系統更新使用兩組稱為槽位(通常是槽位 A 和槽位 B)的分割槽。系統從“當前”槽位執行,但在正常操作期間,執行中的系統不會訪問未使用的槽位中的分割槽。這種方法通過將未使用的槽位保留為後備槽位,來防範更新出現問題:如果在更新期間或更新剛剛完成後出現錯誤,系統可以回滾到原來的槽位並繼續正常執行。為了實現這一目標,當前槽位使用的任何分割槽(包括只有一個副本的分割槽)都不應在 OTA 更新期間進行更新。
每個槽位都有一個“可啟動”屬性,該屬性用於表明相應槽位儲存的系統正確無誤,裝置可從相應槽位啟動。系統執行時,當前槽位處於可啟動狀態,但另一個槽位則可能包含舊版本(仍然正確)的系統、包含更新版本的系統,或包含無效的資料。無論當前槽位是哪一個,都有一個槽位是活動槽位(引導載入程式在下次啟動時將使用的槽位,也稱為首選槽位)。
此外,每個槽位還都有一個由使用者空間設定的“成功”屬性,僅當相應槽位處於可啟動狀態時,該屬性才具有相關性。被標記為成功的槽位應該能夠自行啟動、執行和更新。未被標記為成功的可啟動槽位(多次嘗試使用它啟動之後)應由引導載入程式標記為不可啟動,其中包括將活動槽位更改為另一個可啟動的槽位(通常是更改為在嘗試啟動到新的活動槽位之前正在執行的槽位)。關於相應介面的具體詳細資訊在 boot_control.h
中進行了定義。
更新引擎守護程序
A/B 系統更新過程會使用名為 update_engine
的後臺守護程序來使系統做好準備,以啟動到更新後的新版本。該守護程序可以執行以下操作:
- 按照 OTA 更新包的指示,從當前槽位 A/B 分割槽讀取資料,然後將所有資料寫入到未使用槽位 A/B 分割槽。
- 在預定義的工作流程中呼叫
boot_control
介面。 - 按照 OTA 更新包的指示,在將資料寫入到所有未使用槽位分割槽之後,從新分割槽執行安裝後程序。(有關詳細資訊,請參閱安裝後)。
由於 update_engine
守護程序本身不會參與到啟動流程中,因此該守護程序在更新期間可執行的操作受限於當前槽位中的 SELinux 政策和功能(在系統啟動到新版本之前,此類政策和功能無法更新)。為了維持一個穩定可靠的系統,更新流程不應修改分割槽表、當前槽位中各個分割槽的內容,以及無法通過恢復出廠設定擦除的非 A/B 分割槽的內容。
更新引擎原始碼
update_engine
原始碼位於 system/update_engine
中。A/B OTA dexopt 檔案分開放到了 installd
和一個程式包管理器中:
frameworks/native/cmds/installd/
ota* 包括安裝後腳本、用於 chroot 的二進位制檔案、負責呼叫 dex2oat 的已安裝克隆、OTA 後 move-artifacts 指令碼,以及 move 指令碼的 rc 檔案。frameworks/base/services/core/java/com/android/server/pm/OtaDexoptService.java
(加上OtaDexoptShellCommand
)是負責為應用準備 dex2oat 命令的程式包管理器。
如需實際示例,請參閱 /device/google/marlin/device-common.mk
。
更新引擎日誌
對於 Android 8.x 及更低版本,可在 logcat
及錯誤報告中找到 update_engine
日誌。要使 update_engine
日誌可在檔案系統中使用,請將以下更改新增到您的細分版本中:
這些更改會將最新的 update_engine
日誌的副本儲存到 /data/misc/update_engine_log/update_engine.YEAR-TIME
。除當前日誌以外,最近的五個日誌也會儲存在 /data/misc/update_engine_log/
下。擁有日誌組 ID 的使用者將能夠訪問相應的檔案系統日誌。
引導載入程式互動
boot_control
HAL 供 update_engine
(可能還有其他守護程序)用於指示引導載入程式從何處啟動。常見的示例情況及其相關狀態包括:
- 正常情況:系統正在從其當前槽位(槽位 A 或槽位 B)執行。到目前為止尚未應用任何更新。系統的當前槽位是可啟動且被標記為成功的活動槽位。
- 正在更新:系統正在從槽位 B 執行,因此,槽位 B 是可啟動且被標記為成功的活動槽位。由於槽位 A 中的內容正在更新,但是尚未完成,因此槽位 A 被標記為不可啟動。在此狀態下,應繼續從槽位 B 重新啟動。
- 已應用更新,正在等待重新啟動:系統正在從槽位 B 執行,槽位 B 可啟動且被標記為成功,但槽位 A 之前被標記為活動槽位(因此現在被標記為可啟動)。槽位 A 尚未被標記為成功,引導載入程式應嘗試從槽位 A 啟動若干次。
- 系統已重新啟動到新的更新:系統正在首次從槽位 A 執行,槽位 B 仍可啟動且被標記為成功,而槽位 A 僅可啟動,且仍是活動槽位,但未被標記為成功。在進行一些檢查之後,使用者空間守護程序
update_verifier
應將槽位 A 標記為成功。
流式更新支援
使用者裝置並非在 /data
上總是有足夠的空間來下載更新包。由於 OEM 和使用者都不想浪費 /cache
分割槽上的空間,因此有些使用者會因為裝置上沒有空間來儲存更新包而不進行更新。為了解決這個問題,Android 8.0 中添加了對流式 A/B 更新(下載資料塊後直接將資料塊寫入 B 分割槽,而無需將資料塊儲存在 /data
上)的支援。流式 A/B 更新幾乎不需要臨時儲存空間,並且只需要能夠儲存大約 100KiB 元資料的儲存空間即可。
要在 Android 7.1 中實現流式更新,請選擇以下補丁程式:
無論是使用 Google 移動服務 (GMS),還是使用任何其他更新客戶端,都需要安裝這些補丁程式,才能在 Android 7.1 中支援流式傳輸 A/B 更新包。
A/B 更新過程
當有 OTA 更新包(在程式碼中稱為有效負載)可供下載時,更新流程便開始了。裝置中的政策可以根據電池電量、使用者活動、充電狀態或其他政策來延遲下載和應用有效負載。此外,由於更新是在後臺執行,因此使用者可能並不知道正在進行更新。所有這些都意味著,更新流程可能隨時會由於政策、意外重新啟動或使用者操作而中斷。
OTA 更新包本身所含的元資料可能會指示可進行流式更新,在這種情況下,相應更新包也可採用非流式安裝方式。伺服器可以利用這些元資料告訴客戶端正在進行流式更新,以便客戶端正確地將 OTA 移交給 update_engine
。如果裝置製造商具有自己的伺服器和客戶端,便可以通過確保以下兩項來實現流式更新:確保伺服器能夠識別出更新是流式更新(或假定所有更新都是流式更新),並確保客戶端能夠正確呼叫 update_engine
來進行流式更新。製造商可以根據更新包是流式更新變體這一事實向客戶端傳送一個標記,以便在進行流式更新時觸發向框架端的移交工作。
有可用的有效負載後,更新流程將遵循如下步驟:
步驟 | 操作 |
---|---|
1 | 通過 markBootSuccessful() 將當前槽位(或“源槽位”)標記為成功(如果尚未標記)。 |
2 | 呼叫函式 setSlotAsUnbootable() ,將未使用的槽位(或“目標槽位”)標記為不可啟動。當前槽位始終會在更新開始時被標記為成功,以防止引導載入程式回退到未使用的槽位(該槽位中很快將會有無效資料)。如果系統已做好準備,可以開始應用更新,那麼即使其他主要元件出現損壞(例如介面陷入崩潰迴圈),當前槽位也會被標記為成功,因為可以通過推送新軟體來解決這些問題。 更新有效負載是不透明的 Blob,其中包含更新到新版本的指示。更新有效負載由以下部分組成:
|
3 | 下載有效負載元資料。 |
4 | 對於元資料中定義的每項操作,都將按順序發生以下行為:將相關資料(如果有)下載到記憶體中、應用操作,然後釋放關聯的記憶體。 |
5 | 對照預期的雜湊重新讀取並驗證所有分割槽。 |
6 | 執行安裝後步驟(如果有)。如果在執行任何步驟期間出現錯誤,則更新失敗,系統可能會通過其他有效負載重新嘗試更新。如果上述所有步驟均已成功完成,則更新成功,系統會執行最後一個步驟。 |
7 | 呼叫 setActiveBootSlot() ,將未使用的槽位標記為活動槽位。將未使用的槽位標記為活動槽位並不意味著它將完成啟動。如果引導載入程式(或系統本身)未讀取到“成功”狀態,則可以將活動槽位切換回來。 |
8 | 安裝後步驟(如下所述)包括從“新更新”版本中執行仍在舊版本中執行的程式。如果此步驟已在 OTA 更新包中定義,則為強制性步驟,且程式必須返回並顯示退出程式碼 0 ,否則更新會失敗。 |
9 | 在系統足夠深入地成功啟動到新槽位並完成重新啟動後檢查之後,系統會呼叫 markBootSuccessful() ,將現在的當前槽位(原“目標槽位”)標記為成功。 |
注意:第 3 步和第 4 步佔用了大部分更新時間,因為這兩個步驟涉及寫入和下載大量資料,並且可能會因政策或重新啟動等原因而中斷。
安裝後
對於定義了安裝後步驟的每個分割槽,update_engine
都會將新分割槽裝載到特定位置,並執行與裝載的分割槽相關的 OTA 中指定的程式。例如,如果安裝後程序被定義為相應系統分割槽中的 usr/bin/postinstall
,則系統會將未使用槽位中的這個分割槽裝載到一個固定位置(例如 /postinstall_mount
),然後執行 /postinstall_mount/usr/bin/postinstall
命令。
為確保成功執行安裝後步驟,舊核心必須能夠:
- 裝載新的檔案系統格式。檔案系統型別不能更改(除非舊核心中支援這麼做),包括使用的壓縮演算法(如果使用 SquashFS 等經過壓縮的檔案系統)等詳細資訊。
- 理解新分割槽的安裝後程序格式。如果使用可執行且可連結格式 (ELF) 的二進位制檔案,則該檔案應該與舊核心相容(例如,如果架構從 32 位細分版本改為使用 64 位細分版本,則 64 位的新程式應該可以在舊的 32 位核心上執行)。除非載入程式 (
ld
) 收到使用其他路徑或編譯靜態二進位制檔案的指令,否則將會從舊系統映像而非新系統映像載入各種庫。
例如,您可以使用 shell 指令碼作為安裝後程序(由舊系統中頂部包含 #!
標記的 shell 二進位制檔案解析),然後從新環境設定庫路徑,以便執行更復雜的二進位制安裝後程序。或者,您可以從專用的較小分割槽執行安裝後步驟,以便主系統分割槽中的檔案系統格式可以得到更新,同時不會產生向後相容問題或引發 stepping-stone 更新;這樣一來,使用者便可以從出廠映像直接更新到最新版本。
新的安裝後程序將受舊系統中定義的 SELinux 政策限制。因此,安裝後步驟適用於在指定裝置上執行設計所要求的任務或其他需要儘可能完成的任務(例如,更新支援 A/B 更新的韌體或引導載入程式、為新版本準備資料庫副本,等等)。安裝後步驟不適用於重新啟動之前的一次性錯誤修復(此類修復需要無法預見的許可權)。
所選的安裝後程序在 postinstall
SELinux 環境中執行。新裝載的分割槽中的所有檔案都將帶有 postinstall_file
標記,無論在重新啟動到新系統後它們的屬性如何,都是如此。在新系統中對 SELinux 屬性進行的更改不會影響安裝後步驟。如果安裝後程序需要額外的許可權,則必須將這些許可權新增到安裝後環境中。
重新啟動後
重新啟動後,update_verifier
會觸發利用 dm-verity 進行完整性檢查。系統會先啟動該檢查,然後再啟動 zygote,以避免 Java 服務進行任何無法撤消且會導致無法進行安全回滾的更改。在此過程中,如果驗證啟動功能或 dm-verity 檢測到任何損壞,引導載入程式和核心還可能會觸發重新啟動。檢查完成後,update_verifier
會將啟動標記為成功。
update_verifier
只會讀取 /data/ota_package/care_map.txt
(在使用 AOSP 程式碼時,該檔案會包含在 A/B OTA 更新包中)中列出的資料塊。Java 系統更新客戶端(例如 GmsCore)會在重新啟動裝置前提取 care_map.txt
並設定訪問許可權,在系統成功啟動到新版本後會刪除所提取的檔案。