A/B (無縫) 系統更新

舊版 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 更新會在背景執行,因此不再由使用者啟動。為避免干擾使用者,建議您在裝置處於閒置維護模式 (例如夜間) 且連上 Wi-Fi 時,安排更新作業。不過,您的用戶端可以使用任何您想要的啟發法。
  • 請洽詢 OTA 套件伺服器,確認是否有可用的更新。這段程式碼應與現有的用戶端程式碼大致相同,但您需要傳送裝置支援 A/B 的訊號。(Google 的用戶端也提供「立即檢查」按鈕,方便使用者查看最新更新內容)。
  • 使用更新套件的 HTTPS 網址呼叫 update_engine (如果有)。update_engine 會在串流更新套件時,更新目前未使用的分區上的原始區塊。
  • 根據 update_engine 結果碼,向伺服器回報安裝成功或失敗的情況。如果更新成功,update_engine 會在下次重新啟動時,指示啟動載入程式啟動至新作業系統。如果新作業系統無法啟動,引導程式會改用舊作業系統,因此用戶端不必採取任何行動。如果更新失敗,用戶端需要根據詳細的錯誤代碼決定何時 (以及是否) 重試。舉例來說,良好的用戶端可以辨識部分 (「diff」) OTA 套件失敗,並改為嘗試完整 OTA 套件。

用戶端可視需要執行下列操作:

  • 顯示通知,要求使用者重新啟動。如果您想實施一項政策,鼓勵使用者定期更新,則可將這項通知加入您的用戶端。如果用戶端未向使用者發出提示,使用者還是會在下次重新啟動時收到更新。(Google 的用戶端有可設定的每次更新延遲時間)。
  • 顯示通知,告知使用者是否已啟動至新的 OS 版本,或是預期要啟動至新版本,但實際上卻回復至舊版 OS。(Google 的用戶端通常不會執行這兩項操作)。

在系統層級,A/B 系統更新會影響下列項目:

  • 分割區選取 (插槽)、update_engine 守護程式和引導程式互動 (如下所述)
  • 建構程序和 OTA 更新套件產生程序 (請參閱「實作 A/B 更新」一文)

分區選項 (時段)

A/B 系統更新會使用兩組分區,稱為「位置」 (通常是位置 A 和位置 B)。系統會從「current」分區執行,而「unused」分區則在正常運作期間不會由執行中的系統存取。這種做法可將未使用的插槽保留為備用,讓更新作業具備錯誤耐受性:如果在更新期間或更新後立即發生錯誤,系統可以回溯至舊插槽,並繼續運作。為達成這個目標,OTA 更新中不應更新 目前插槽使用的任何分割區 (包括只有一個副本的分割區)。

每個插槽都有可啟動屬性,指出插槽是否包含可讓裝置啟動的正確系統。系統執行時,目前的插槽可啟動,但其他插槽可能會包含舊版 (仍正確) 的系統、較新的版本或無效的資料。無論「目前」的插槽為何,都會有一個「有效」插槽 (即下次啟動時會從中啟動的引導程式) 或「偏好」插槽。

每個插槽也都有由使用者空間設定的成功屬性,只有在插槽可啟動時才相關。成功的運算單元應可自行啟動、執行及更新。可啟動的插槽如果在多次嘗試啟動後仍未標示為成功,應由 Bootloader 標示為無法啟動,包括將有效的插槽變更為其他可啟動插槽 (通常是指在嘗試啟動新的有效插槽之前立即執行的插槽)。介面具體詳細資料已在 boot_control.h 中定義。

更新引擎 Daemon

A/B 系統更新會使用名為 update_engine 的背景 Daemon,讓系統準備啟動至新版更新版本。這個守護程式可以執行下列動作:

  • 根據 OTA 套件的指示,從目前的插槽 A/B 分區讀取資料,並將任何資料寫入未使用的插槽 A/B 分區。
  • 在預先定義的工作流程中呼叫 boot_control 介面。
  • 按照 OTA 套件的指示,在寫入所有未使用的插槽分割區後,從分割區執行安裝後程式。(詳情請參閱「安裝後」)。

由於 update_engine 守護程序不會參與啟動程序,因此在更新期間,其功能會受到 current 槽中的 SELinux 政策和功能的限制 (系統必須先啟動至新版本,才能更新這些政策和功能)。為了維持系統穩定性,更新程序不應修改分區表、目前插槽中分區的內容,或非 A/B 分區的內容,因為這些內容無法透過恢復原廠設定來清除。

更新引擎來源

update_engine 來源位於 system/update_engine 中。A/B OTA dexopt 檔案會在 installd 和套件管理工具之間分割:

如需實際範例,請參閱 /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/ 會儲存最近五筆記錄。擁有 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 空間不一定足以下載更新套件。由於原始設備製造商和使用者都不想浪費 /cache 分區的空間,因此有些使用者無法更新,因為裝置沒有可用來儲存更新套件的空間。為解決這個問題,Android 8.0 新增了對串流 A/B 更新的支援,可在下載時直接將區塊寫入 B 分區,而無須將區塊儲存在 /data 上。串流式 A/B 更新幾乎不需要暫存空間,只需要足以儲存約 100 KiB 中繼資料的空間即可。

如要在 Android 7.1 中啟用串流更新功能,請精選下列修補程式:

無論使用 Google 行動服務 (GMS) 或任何其他更新用戶端,這些修補程式都是在 Android 7.1 以上版本中支援串流 A/B 更新的必要條件。

A/B 更新的生命週期

當無線更新套件 (在程式碼中稱為「酬載」) 可供下載時,更新程序就會開始。裝置中的政策可能會根據電池電量、使用者活動、充電狀態或其他政策,延遲酬載下載和應用程式。此外,由於更新會在背景執行,使用者可能不會知道更新程序正在進行。這表示更新程序可能會在任何時間點因政策、意外重新啟動或使用者操作而中斷。

您可以選擇在 OTA 套件中加入中繼資料,指出更新內容可透過串流方式安裝;同一個套件也可以用於非串流安裝。伺服器可能會使用中繼資料告知用戶端正在串流,以便用戶端正確地將 OTA 交給 update_engine。擁有自有伺服器和用戶端的裝置製造商,可以透過確保伺服器識別更新是否為串流 (或假設所有更新都是串流),以及用戶端正確呼叫 update_engine 來啟用串流更新功能。製造商可以利用套件屬於串流變化版本的事實,向用戶端傳送標記,以便觸發將資料傳送至架構端的串流。

有效載荷可用後,更新程序如下:

步驟 活動
1 目前的時間間隔 (或「來源時間間隔」) 會標示為成功 (如果尚未標示),並使用 markBootSuccessful()
2 呼叫 setSlotAsUnbootable() 函式可將未使用的插槽 (或稱「目標插槽」) 標示為無法啟動。在更新開始時,系統一律會將目前的插槽標示為成功,以免引導程式改回未使用的插槽,因為該插槽很快就會出現無效資料。如果系統已達到可開始套用更新的階段,即使其他主要元件發生故障 (例如發生當機迴圈的 UI),目前的版位仍會標示為成功,因為系統可以推送新軟體來修正這些問題。

更新酬載是含有更新至新版本指示的不明 Blob。更新酬載包含下列項目:
  • 中繼資料。中繼資料是更新酬載中相對較小的一部分,其中包含一項作業清單,可在目標插槽中產生及驗證新版本。舉例來說,作業可以解壓縮特定 Blob,並將其寫入目標分區中的特定區塊,或從來源分區讀取資料、套用二進位元修補程式,然後寫入目標分區中的特定區塊。
  • 額外資料:在這些範例中,更新酬載的主要部分是與作業相關聯的額外資料,包括壓縮的 blob 或二進位修補程式。
3 下載酬載中繼資料。
4 針對中繼資料中定義的每個作業,系統會依序將相關聯的資料 (如有) 下載至記憶體、套用作業,並捨棄相關聯的記憶體。
5 系統會重新讀取整個分區,並根據預期的雜湊值進行驗證。
6 執行安裝後步驟 (如有)。如果執行任何步驟時發生錯誤,更新作業就會失敗,並可能會使用其他酬載重新嘗試。如果目前所有步驟都成功,更新就會成功,並執行最後一個步驟。
7 未使用的插槽會透過呼叫 setActiveBootSlot() 標示為有效。 將未使用的插槽標示為有效,並不代表系統會完成啟動程序。如果啟動載入程式 (或系統本身) 無法讀取成功狀態,則可切換回有效的插槽。
8 安裝後 (詳見下文) 是指在舊版仍在執行的情況下,從「新更新」版本執行程式。如果在 OTA 套件中定義,此步驟是必要的,且程式必須傳回退出碼 0;否則更新會失敗。
9 系統成功啟動至新運算單元,並完成重新啟動後的檢查後,系統會呼叫 markBootSuccessful(),將目前的運算單元 (先前稱為「目標運算單元」) 標示為成功。

安裝後

對於已定義安裝後步驟的分區,update_engine 會將新分區掛載至特定位置,並執行 OTA 中指定的與掛載分區相關的程式。舉例來說,如果在系統分區中將安裝後程式定義為 usr/bin/postinstall,則未使用的插槽中的此分區會掛接至固定位置 (例如 /postinstall_mount),並執行 /postinstall_mount/usr/bin/postinstall 指令。

如要順利完成安裝後程序,舊版核心必須能夠:

  • 掛接新的檔案系統格式。除非舊版核心支援,否則無法變更檔案系統類型,包括使用壓縮檔案系統 (例如 SquashFS) 時使用的壓縮演算法等詳細資料。
  • 瞭解新分區的後安裝計畫格式。如果使用可執行和可連結格式 (ELF) 二進位檔,則應與舊版核心相容 (例如,如果架構從 32 位元版本切換為 64 位元版本,則在舊版 32 位元核心上執行的 64 位元新程式)。除非載入器 (ld) 已指示使用其他路徑或建構靜態二進位檔,否則程式庫會從舊的系統映像檔載入,而非新的系統映像檔。

舉例來說,您可以使用殼層指令碼做為安裝後程式,由舊系統的殼層二進位檔 (頂端有 #! 標記) 進行解讀,然後從新環境設定程式庫路徑,以便執行更複雜的二進位安裝後程式。或者,您也可以從專屬的小型分割區執行安裝後步驟,讓主要系統分割區中的檔案系統格式能夠更新,而不會發生向後相容性問題或階梯式更新;這樣一來,使用者就能直接從原廠映像檔更新至最新版本。

新的安裝後程序受到舊系統中定義的 SELinux 政策限制。因此,安裝後步驟適合在特定裝置上執行設計所需的任務,或執行其他盡力執行的任務。安裝後步驟不適合用於需要意外權限的一次性錯誤修正,

所選的後安裝程式會在 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、設定存取權限,然後重新啟動裝置,並在系統成功啟動至新版本後刪除已擷取的檔案。