A/B System 概述
Android從7.0開(kāi)始,引入了新的OTA升級(jí)方式 A/B System Updates
A/B系統(tǒng)是指設(shè)備上有A和B兩套可以工作的系統(tǒng)(用戶(hù)數(shù)據(jù)只有一份,為兩套系統(tǒng)共用),我們可以理解為一套系統(tǒng)分區(qū),另外一套為備份分區(qū).其系統(tǒng)版本可能一樣,也可能不一樣;通過(guò)升級(jí),可以將舊版本也更新為新版本.當(dāng)然,設(shè)備出廠時(shí)這兩套系統(tǒng)肯定是一樣的.
Android 7.0上傳統(tǒng)OTA方式和新的A/B系統(tǒng)方式都存在,編譯時(shí)只能選擇其中的一種OTA方式.由于A/B系統(tǒng)在分區(qū)上與傳統(tǒng)OTA的分區(qū)設(shè)計(jì)不一樣,二者無(wú)法兼容,所以7.0以前的系統(tǒng)無(wú)法通過(guò)OTA方式升級(jí)為A/B系統(tǒng).
7.0以前傳統(tǒng)的OTA方式:
設(shè)備上有一個(gè)Android主系統(tǒng)和一個(gè)Recovery系統(tǒng),Android主系統(tǒng)運(yùn)行時(shí)檢測(cè)是否需要升級(jí),如果需要升級(jí),
則將升級(jí)的數(shù)據(jù)包下載并存放到cache分區(qū),重啟系統(tǒng)后進(jìn)入Recovery系統(tǒng),并用cache分區(qū)下載好的數(shù)據(jù)更新Android主系統(tǒng),
更新完成后重新啟動(dòng)進(jìn)入Android主系統(tǒng)。如果更新失敗,設(shè)備重啟后就不能正常使用了,唯一的辦法就是重新升級(jí),直到成功為止。
而A/B系統(tǒng)主要由運(yùn)行在Android后臺(tái)的update_engine和兩套分區(qū)‘slot A’和‘slot B’組成。
Android系統(tǒng)從其中一套分區(qū)啟動(dòng),在后臺(tái)運(yùn)行update_engine監(jiān)測(cè)升級(jí)信息并下載升級(jí)數(shù)據(jù),
然后將數(shù)據(jù)更新到另外一套分區(qū),寫(xiě)入數(shù)據(jù)完成后從更新的分區(qū)啟動(dòng)
與傳統(tǒng)OTA方式相比,A/B系統(tǒng)的變化主要有:
1、系統(tǒng)的分區(qū)設(shè)置
傳統(tǒng)方式只有一套分區(qū)
A/B系統(tǒng)有兩套分區(qū),稱(chēng)為slot A和slot B
2、跟bootloader溝通的方式
傳統(tǒng)方式bootloader通過(guò)讀取misc分區(qū)信息來(lái)決定是進(jìn)入Android主系統(tǒng)還是Recovery系統(tǒng)
A/B系統(tǒng)的bootloader通過(guò)特定的分區(qū)信息來(lái)決定從slot A還是slot B啟動(dòng)
3、系統(tǒng)的編譯過(guò)程
傳統(tǒng)方式在編譯時(shí)會(huì)生成boot.img和recovery.img分別用于Android主系統(tǒng)和Recovery系統(tǒng)的ramdisk
A/B系統(tǒng)只有boot.img,而不再生成單獨(dú)的recovery.img
4、OTA更新包的生成方式
A/B系統(tǒng)生成OTA包的工具和命令跟傳統(tǒng)方式一樣,但是生成內(nèi)容的格式不一樣了
A/B 系統(tǒng)更新可帶來(lái)以下好處:
- OTA 更新可以在系統(tǒng)運(yùn)行期間進(jìn)行,而不會(huì)打斷用戶(hù)。用戶(hù)可以在 OTA 期間繼續(xù)使用其設(shè)備。在更新期間,唯一的一次宕機(jī)發(fā)生在設(shè)備重新啟動(dòng)到更新后的磁盤(pán)分區(qū)時(shí)。
- 更新后,重新啟動(dòng)所用的時(shí)間不會(huì)超過(guò)常規(guī)重新啟動(dòng)所用的時(shí)間。
- 如果 OTA 無(wú)法應(yīng)用(例如,因?yàn)樗C(jī)失?。脩?hù)將不會(huì)受到影響。用戶(hù)將繼續(xù)運(yùn)行舊的操作系統(tǒng),并且客戶(hù)端可以重新嘗試進(jìn)行更新。
- 如果 OTA 更新已應(yīng)用但無(wú)法啟動(dòng),設(shè)備將重新啟動(dòng)回舊分區(qū),并且仍然可以使用??蛻?hù)端可以重新嘗試進(jìn)行更新。
- 任何錯(cuò)誤(例如 I/O 錯(cuò)誤)都只會(huì)影響未使用的分區(qū)組,并且用戶(hù)可以進(jìn)行重試。由于 I/O 負(fù)載被特意控制在較低水平,以免影響用戶(hù)體驗(yàn),因此發(fā)生此類(lèi)錯(cuò)誤的可能性也會(huì)降低。
更新包可以流式傳輸?shù)?A/B 設(shè)備,因此在安裝之前不需要先下載更新包。流式更新意味著用戶(hù)沒(méi)有必要在 /data 或 /cache 上留出足夠的可用空間來(lái)存儲(chǔ)更新包。 - 緩存分區(qū)不再用于存儲(chǔ) OTA 更新包,因此無(wú)需確保緩存分區(qū)的大小要足以應(yīng)對(duì)日后的更新。
- dm-verity 可保證設(shè)備將使用未損壞的啟動(dòng)映像。如果設(shè)備因 OTA 錯(cuò)誤或 dm-verity問(wèn)題而無(wú)法啟動(dòng),則可以重新啟動(dòng)到舊映像。(Android 驗(yàn)證啟動(dòng)不需要 A/B 更新。)
A/B 更新對(duì) 2016 Pixel 分區(qū)大小有什么影響?
[圖片上傳失敗...(image-89986-1566889673273)]
OTA 系統(tǒng)分區(qū)
在非 A/B 系統(tǒng)Android設(shè)備上,閃存空間通常包含以下分區(qū):
boot
boot分區(qū)中包含了Linux內(nèi)核和最小的根文件系統(tǒng)(會(huì)被加載到RAM中去).它裝載了系統(tǒng)和其它分區(qū),并且boot分區(qū)還被用來(lái)啟動(dòng)system分區(qū)中的運(yùn)行環(huán)境.
system
system分區(qū)中包含在 Android 開(kāi)源項(xiàng)目 (AOSP)上提供源代碼的系統(tǒng)應(yīng)用和庫(kù).在正常操作期間,此分區(qū)被裝載為只讀分區(qū);其內(nèi)容僅在 OTA 更新期間更改.
vendor
vendor分區(qū)中包含在 Android 開(kāi)源項(xiàng)目 (AOSP) 上未提供源代碼的系統(tǒng)應(yīng)用和庫(kù).在正常操作期間,此分區(qū)被裝載為只讀分區(qū);其內(nèi)容僅在 OTA 更新期間更改.
userdata
存儲(chǔ)由用戶(hù)安裝的應(yīng)用所保存的數(shù)據(jù)等.OTA 更新過(guò)程通常不會(huì)觸及該分區(qū).
cache
幾個(gè)應(yīng)用使用的臨時(shí)保留區(qū)域(訪問(wèn)此分區(qū)需要使用特殊的應(yīng)用權(quán)限),用于存儲(chǔ)下載的 OTA 更新包。其他程序也可使用該空間,但是此類(lèi)文件可能會(huì)隨時(shí)消失。安裝某些 OTA 更新包可能會(huì)導(dǎo)致此分區(qū)被完全擦除。緩存還包含 OTA 更新的更新日志。
recovery
包含第二個(gè)完整的 Linux 系統(tǒng),其中包括一個(gè)內(nèi)核和特殊的恢復(fù)二進(jìn)制文件(該文件可讀取一個(gè)軟件包并使用其內(nèi)容來(lái)更新其他分區(qū))。
misc
執(zhí)行恢復(fù)操作時(shí)使用的微小分區(qū),可在應(yīng)用 OTA 更新包并重新啟動(dòng)設(shè)備時(shí),隱藏某些進(jìn)程的信息。
A/B系統(tǒng)的分區(qū)
bootloader
存放用于引導(dǎo)linux的bootloader
boot_a和boot_b
分別用于存放兩套系統(tǒng)各自的linux kernel文件和用于掛載system和其他分區(qū)的ramdisk
system_a和system_b
Android主系統(tǒng)分區(qū),分別用于存放兩套系統(tǒng)各自的系統(tǒng)應(yīng)用程序和庫(kù)文件
vendor_a和vendor_b
Android主系統(tǒng)分區(qū), 分別用于存放兩套系統(tǒng)各自的開(kāi)發(fā)廠商定制的一些應(yīng)用和庫(kù)文件,很多時(shí)候開(kāi)發(fā)廠商也直接將這個(gè)分區(qū)的內(nèi)容直接放入system分區(qū)
userdata
用戶(hù)數(shù)據(jù)分區(qū),存放用戶(hù)數(shù)據(jù),包括用戶(hù)安裝的應(yīng)用程序和使用時(shí)生成的數(shù)據(jù)
misc或其他名字分區(qū)
存放Android主系統(tǒng)和Recovery系統(tǒng)跟bootloader通信的數(shù)據(jù),由于存放方式和分區(qū)名字沒(méi)有強(qiáng)制要求,所以部分實(shí)現(xiàn)上保留了misc分區(qū)(代碼中可見(jiàn)Brillo和Intel的平臺(tái)),另外部分實(shí)現(xiàn)采用其他分區(qū)存放數(shù)據(jù)(Broadcom機(jī)頂盒平臺(tái)采用名為eio的分區(qū))。
兩者區(qū)別為: A/B系統(tǒng)boot,system和vendor分區(qū)從傳統(tǒng)的一套變?yōu)閮商?叫做slot A和slot B;并且不再需要cache和recovery分區(qū),同時(shí)misc分區(qū)也不是必要的
Tips:
什么是Bootloader?
在嵌入式操作系統(tǒng)中,Bootloader在操作系統(tǒng)內(nèi)核運(yùn)行之前運(yùn)行,可以初始化硬件設(shè)備、建立內(nèi)存空間映射圖,為調(diào)用操作系統(tǒng)內(nèi)核
準(zhǔn)備好正確的環(huán)境。Bootloader和硬件是相關(guān)的,且廠商一般都會(huì)對(duì)bootloader加鎖,這樣就不能隨便刷機(jī)了。
當(dāng)然bootloader也是可以解鎖的,這里不得不提一下root和bootloader解鎖分別是怎么一回事:root是通過(guò)內(nèi)核漏洞獲取最高的權(quán)
限,也就是所謂的超級(jí)用戶(hù)(su,superuser),屬于系統(tǒng)層面,root之后就可以修改system分區(qū)的數(shù)據(jù);bootloader解鎖則屬于硬
件層面的解鎖boot和recovery分區(qū),解鎖bootloader不會(huì)root手機(jī)
Fastboot和recovery的區(qū)別?
Bootloader過(guò)程中,先做一些初始化,然后根據(jù)組合鍵做不同的事情,這個(gè)過(guò)程內(nèi)核沒(méi)有加載,機(jī)器只是在按順序執(zhí)行指令。
Fastboot:在這種模式下,可以修改手機(jī)的硬件,并且允許我們發(fā)送一些命令給Bootloader。如使用電腦刷機(jī),則需要進(jìn)入fastboot
模式,通過(guò)電腦執(zhí)行命令將系統(tǒng)鏡像刷到通過(guò)USB刷到手機(jī)中。
Recovery:Recovery是一個(gè)小型的操作系統(tǒng),并且會(huì)加載部分文件系統(tǒng),這樣才能從sdcard中讀取升級(jí)包。
OTA 升級(jí)流程
非A/B系統(tǒng)OTA 更新包含以下步驟:
- 設(shè)備會(huì)與 OTA 服務(wù)器進(jìn)行定期確認(rèn),并被告知是否有更新可用,包括更新軟件包的 URL 和向用戶(hù)顯示的描述字符串。
- 將更新下載到緩存或數(shù)據(jù)分區(qū),并根據(jù) /system/etc/security/otacerts.zip 中的證書(shū)驗(yàn)證加密簽名。系統(tǒng)提示用戶(hù)安裝更新。
- 設(shè)備重新啟動(dòng)進(jìn)入恢復(fù)模式,引導(dǎo)恢復(fù)分區(qū)中的內(nèi)核和系統(tǒng)啟動(dòng),而非引導(dǎo)分區(qū)中的內(nèi)核。
- 恢復(fù)分區(qū)的二進(jìn)制文件由 init 啟動(dòng)。它會(huì)在 /cache/recovery/command 中尋找將其指向下載軟件包的命令行參數(shù)。
- 恢復(fù)操作會(huì)根據(jù) /res/keys (包含在恢復(fù)分區(qū)中的 RAM 磁盤(pán)的一部分)中的公鑰來(lái)驗(yàn)證軟件包的加密簽名。
- 從軟件包中提取數(shù)據(jù),并根據(jù)需要使用該數(shù)據(jù)更新引導(dǎo)、系統(tǒng)和/或供應(yīng)商分區(qū)。系統(tǒng)分區(qū)上其中一個(gè)新文件包含新恢復(fù)分區(qū)內(nèi)容。
- 設(shè)備正常重啟。
1.加載最新更新的引導(dǎo)分區(qū),在最新更新的系統(tǒng)分區(qū)中裝載并開(kāi)始執(zhí)行二進(jìn)制文件。
2.作為正常啟動(dòng)的一部分,系統(tǒng)會(huì)根據(jù)所需內(nèi)容(預(yù)先存儲(chǔ)為 /system 中的一個(gè)文件)檢查恢復(fù)分區(qū)的內(nèi)容。二者內(nèi)容不同,所以恢復(fù)分區(qū)會(huì)被所需內(nèi)容重新刷寫(xiě)(在后續(xù)引導(dǎo)中,恢復(fù)分區(qū)已經(jīng)包含新內(nèi)容,因此無(wú)需重新刷寫(xiě))。
系統(tǒng)更新完成!更新日志可以在 /cache/recovery/last_log.# 中找到。
A/B系統(tǒng)的啟動(dòng):
[圖片上傳失敗...(image-b87734-1566889673273)]
1.手機(jī)啟動(dòng)后,BootLoader會(huì)去讀取slot metadata,來(lái)確定從哪個(gè)slot啟動(dòng)
2.檢查是否有可啟動(dòng)的分區(qū),如果沒(méi)有則,直接進(jìn)入bootloader的recovery mode(即bootloader下的刷機(jī)模式),一般是進(jìn)入fastboot命令行.
3.如果有可啟動(dòng)的分區(qū),則選擇可啟動(dòng)分區(qū)中優(yōu)先級(jí)最高的slot(例如,直接選擇當(dāng)前設(shè)置為active的分區(qū))
4.檢查所選擇分區(qū)的retry count(retry count表示當(dāng)前分區(qū)可以嘗試啟動(dòng)的次數(shù)),
如果retry count等于0啟動(dòng)成功(啟動(dòng)成功的分區(qū)會(huì)標(biāo)記為successful),大于0則繼續(xù)嘗試從當(dāng)前分區(qū)啟動(dòng),并且累計(jì)遞減,
而小于0則將所選擇分區(qū)標(biāo)記為無(wú)效分區(qū)(通常設(shè)置為unbootable),然后重復(fù)第2步,查找下一個(gè)可以啟動(dòng)的分區(qū)
5.最后從當(dāng)前slot的boot中啟動(dòng)Linux內(nèi)核,并且掛載system和其他分區(qū)
6.Linux啟動(dòng)后,通過(guò)dm-verify機(jī)制校驗(yàn)system分區(qū),完成后加載system分區(qū)內(nèi)包含的rootfs,通過(guò)/init程序解析/init.rc腳本,完成Android系統(tǒng)的啟動(dòng)
參考內(nèi)容:
相關(guān)鏈接直達(dá):
ls dev/block/bootdevice/by-name/
應(yīng)用安裝OTA 包需用用到的類(lèi):
frameworks/base/core/java/android/os/RecoverySystem.java
校驗(yàn)
應(yīng)用可調(diào)用RecoverySystem 的靜態(tài)方法verifyPackage
安裝
應(yīng)用可調(diào)用RecoverySystem 的靜態(tài)方法installPackage
手機(jī)啟動(dòng)模式檢測(cè)
機(jī)器啟動(dòng)時(shí),首先檢測(cè)是否有組合鍵按下,如檢測(cè)到(音量下+power)組合鍵,則進(jìn)入recovery;否則檢測(cè)系統(tǒng)的/misc分區(qū),根據(jù)此分區(qū)存儲(chǔ)的命令選擇不同的模式。
/misc分區(qū)下存儲(chǔ)著結(jié)構(gòu)體bootloader_message,稱(chēng)之為BCB塊,其定義如下:
struct bootloader_message{
char command[32]; //存放不同的啟動(dòng)命令,如果command為空:正常啟動(dòng)機(jī)器;如果是boot-recovery,系統(tǒng)會(huì)進(jìn)入Recovery模式;如果是update-radia或update-hboot:刷固件
char status[32]; //存放執(zhí)行結(jié)果
char recovery[1024]; //存放/cache/recovery/command中的命令
};
recovery[1024]中則存放著升級(jí)包路徑,其存儲(chǔ)結(jié)構(gòu)如下:第一行存放字符串“recovery”;第二行存放路徑信息“--update_package=/sdcard/update.zip”等。
除了BCB塊外,還可以將路徑信息--update_package=/sdcard/update.zip寫(xiě)入文件/cache/recovery/command傳遞給recovery模式。
進(jìn)入recovery模式后,系統(tǒng)通過(guò)get_args函數(shù)獲取升級(jí)包信息。此函數(shù)首先獲取BCB塊信息,如果未檢測(cè)到相關(guān)信息,則繼續(xù)檢測(cè)/cache/recovery/command文件;最后,將啟動(dòng)命令boot-recovery及升級(jí)包路徑--update_package=/sdcard/update.zip重新寫(xiě)入到BCB塊中,以便系統(tǒng)下次啟動(dòng)時(shí)再次進(jìn)入到recovery模式,直到升級(jí)成功后執(zhí)行finish_recovery函數(shù)清空BCB及/cache/recovery/command文件。
OTA差分包升級(jí)失敗
升級(jí)失敗log如下:
I update_engine: [0530/162336:INFO:delta_performer.cc(359)] Applying 21701 operations to partition "system"
E update_engine: [0530/162336:ERROR:delta_performer.cc(1060)] The hash of the source data on disk for this operation doesn't match the expected value. This could mean that the delta update payload was targeted for another version, or that the source partition was modified after it was installed, for example, by mounting a filesystem.
E update_engine: [0530/162336:ERROR:delta_performer.cc(1065)] Expected: sha256|hex = 839ACF5296B9AB820DC822B6C09EBA896905531EB2C581093A357411F1A444A0
E update_engine: [0530/162336:ERROR:delta_performer.cc(1068)] Calculated: sha256|hex = 18AF8D6842A71554893F1DE65B87F2A9639FB390357C71D5383C6ED7A6051AFA
E update_engine: [0530/162336:ERROR:delta_performer.cc(1077)] Operation source (offset:size) in blocks: 0:2,193:1,218:456,23471:8,32769:1,32961:1,37333:4,37351:3,37554:3,37570:2,37951:1,37959:1,38111:1,38125:1,38129:1,38139:1,38147:1,38149:1,38151:2,38155:1,38157:1,38360:5,38372:1,38377:5,38384:1,38437:1,38442:1,38447:1,38452:1,38457:1,38462:1,38467:1
E update_engine: [0530/162336:ERROR:delta_performer.cc(1260)] ValidateSourceHash(source_hasher.raw_hash(), operation, error) failed.
E update_engine: [0530/162336:ERROR:delta_performer.cc(283)] Failed to perform SOURCE_BSDIFF operation 8, which is the operation 0 in partition "system"
E update_engine: [0530/162336:ERROR:download_action.cc(273)] Error 20 in DeltaPerformer's Write method when processing the received payload -- Terminating processing
I update_engine: [0530/162336:INFO:delta_performer.cc(299)] Discarding 113721 unused downloaded bytes
I update_engine: [0530/162336:INFO:multi_range_http_fetcher.cc(171)] Received transfer terminated.
I update_engine: [0530/162336:INFO:multi_range_http_fetcher.cc(123)] TransferEnded w/ code 200
I update_engine: [0530/162336:INFO:multi_range_http_fetcher.cc(125)] Terminating.
I update_engine: [0530/162336:INFO:action_processor.cc(116)] ActionProcessor: finished DownloadAction with code ErrorCode::kDownloadStateInitializationError
I update_engine: [0530/162336:INFO:action_processor.cc(121)] ActionProcessor: Aborting processing due to failure.
I update_engine: [0530/162336:INFO:update_attempter_android.cc(286)] Processing Done.
I update_engine: [0530/162336:INFO:update_attempter_android.cc(306)] Resetting update progress.
The hash of the source data on disk for this operation doesn't match the expected value. This could mean that the delta update payload was targeted for another version, or that the source partition was modified after it was installed, for example, by mounting a filesystem.
這個(gè)操作的磁盤(pán)上的源數(shù)據(jù)的散列與預(yù)期值不匹配。這可能意味著增量更新有效負(fù)載是針對(duì)另一個(gè)版本的,或者是在安裝之后修改了源分區(qū),例如,通過(guò)安裝文件系統(tǒng)。
原因: make otapackage 會(huì)對(duì)system.img重新打包 導(dǎo)致重新打包的system.img和out目錄下的system.img Hash值不一致.
也就是線刷版本的system.img和OTA包的system.img不一致;整包升級(jí)會(huì)替換system.img,而差分包升級(jí)則需要保證系統(tǒng)內(nèi)部的system.img和整包中system.img一致才能升級(jí)成功(差分包中保存了通過(guò)sha256 Hash算法計(jì)算出整包system.img的值,通過(guò)這個(gè)值來(lái)確定兩個(gè)system.img一致).
驗(yàn)證是否如上原因?qū)е?
將整包中system.img通過(guò)fastboot燒錄到當(dāng)前系統(tǒng),再驗(yàn)證差分包升級(jí)
adb reboot bootloader
fastboot devices
//切換到system.img所在的目錄
fastboot flash system system.img
fastboot reboot
解決:保證差分包或整包的system.img和線刷(也就是out目錄下)的system.img保持一致.將make otapackage源包中的system.img替換out目錄下system.img
out/target/product/xxx_qn6005_64/obj/PACKAGING/systemimage_intermediates/system.img
out/target/product/xxx_qn6005_64/system.img
腳本實(shí)現(xiàn):
1.在/build/core/Makefile中添加:
$(hide) ./build/tools/releasetools/replace_img_from_target_files.py $@ $(PRODUCT_OUT)
2.在/build/tools/releasetools/中定義replace_img_from_target_files.py腳本
#!/usr/bin/env python
#
# Copyright (C) 2014 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Given a target-files zipfile that does contain images (ie, does
have an IMAGES/ top-level subdirectory), replace the images to
the output dir.
Usage: replace_img_from_target_files target_files output
"""
import sys
if sys.hexversion < 0x02070000:
print >> sys.stderr, "Python 2.7 or newer is required."
sys.exit(1)
import errno
import os
import re
import shutil
import subprocess
import tempfile
import zipfile
image_replace_list = ["boot.img","system.img"]
# missing in Python 2.4 and before
if not hasattr(os, "SEEK_SET"):
os.SEEK_SET = 0
def main(argv):
if len(argv) != 2:
sys.exit(1)
if not os.path.exists(argv[0]):
print "Target file:%s is invalid" % argv[0]
sys.exit(1)
if not os.path.exists(argv[1]):
print "Output dir:%s is invalid" % argv[1]
sys.exit(1)
zf = zipfile.ZipFile(argv[0], 'r')
for img in zf.namelist():
if img.find("IMAGES/") != -1:
if img.find(".img") != -1:
data = zf.read(img)
name = img.replace("IMAGES/", '')
if name in image_replace_list:
print "Replace %s" % name
name = '/'.join((argv[1], name))
file = open(name, "w")
file.write(data)
file.close()
if __name__ == '__main__':
main(sys.argv[1:])