Recovery模式指的是一種能夠對安卓機內部的數據或系統進行修改的模式(相似於windows PE或DOS)。也能夠稱之爲安卓的恢復模式,在這個所謂的恢復模式下,咱們能夠刷入新的安卓系統,或者對已有的系統進行備份或升級,也能夠在此恢復出廠設置(格式化數據和緩存)。linux
通常來講,安卓手機和平板通常包括如下標準內部分區:android
Boot:包含Linux內核和一個最小的root文件系統(裝載到ramdisk中),用於掛載系統和其餘的分區,並開始Runtime。正如名字所表明的意思(注:boot的意思是啓動),這個分區使Android設備能夠啓動。若是沒有這個分區,Android設備一般沒法啓動到Android系統。windows
System:這個分區幾乎包含了除kerner和ramdisk以外的整個android操做系統,包括了用戶界面、和全部預裝的系統應用程序和庫文件(AOSP中能夠獲取到源代碼)。在運行的過程當中,這個分區是read-only的。固然,一些Android設備,也容許在remount的狀況下,對system分區進行讀寫。 擦除這個分區,至關於刪除整個安卓系統,會致使不能進入Main System, 但不會影響到Recovery。所以,能夠經過進入Recovery程序或者bootloader程序中,升級安裝一個新ROM。緩存
Userdata:用戶數據區,用戶安裝的應用程序會把數據保存在這裏,包含了用戶的數據:聯繫人、短信、設置、用戶安裝的程序。擦除這個分區,本質上等同於手機恢復出廠設置,也就是手機系統第一次啓動時的狀態,或者是最後一次安裝官方或第三方ROM後的狀態。在Recovery程序中進行的「data/factory reset 」操做就是在擦除這個分區。正常狀況下OTA是不會清除這裏的數據的,指定要刪除數據的除外。網絡
Cache:系統緩存區,臨時的保存應用數據(要把數據保存在這裏,須要特意的app permission), OTA的升級包也能夠保存在這裏。OTA升級過程可能會清楚這個分區的數據。通常來說,Android差分包升級也須要依賴此分區存放一些中間文件。app
Recovery:包括了一個完整Linux內核和一些特殊的recovery binary,能夠讀取升級文件用這些文件來更新其餘的分區。tcp
Misc:一個很是小的分區,4 MB左右。recovery用這個分區來保存一些關於升級的信息,應對升級過程當中的設備掉電重啓的情況,Bootloader啓動的時候,會讀取這個分區裏面的信息,以決定系統是否進Recovery System 或 Main System。函數
以上幾個分區是Google官方的標準,對於第三方Android設備廠商來說,分區的狀況可能稍微不同,好比Rockchip平臺,還增長了user分區、kernel分區和backup分區。其中:工具
kernel:顧名思義,是存放kernel.img鏡像的。在boot分區裏面的kernel內核鏡像損壞的狀況下(好比flash損壞),bootloader會嘗試加載kerner分區裏面的內核鏡像。ui
backup:存放整個系統鏡像(update.img), 可用於恢復設備到出廠ROM。
user: 用戶分區,也就是平時咱們所說的內置sdcard。另外還有外置的sdcard分區,用於存放用戶相片、視頻、文檔、ROM安裝包等。
通常來說,Android有三種啓動模式:Fastboot模式,Recovery System 以及Main System。
首先說一下,正常啓動和進入Recovery的區別,一圖以概之:
通常來說,進入recovery有兩種方式,一種是經過組合鍵進入recovery,按鍵指引的方式,各個Android平臺都不同,好比三星的手機是在關機狀態下同時按住【音量上】、【HOME鍵】、【電源鍵】,等待屏幕亮起後便可放開,進入Recovery模式。而Rockchip的機頂盒,則是使用按【Reset鍵】加【電源鍵】開機的方式,形式不一。
另外一種,則是使用系統命令啓動到Recovery模式的,這對絕大部分Android設備是通用的:
reboot recovery
在Android應用層部分,OTA系統升級流程。大概的流程圖以下所示:
以上部分,只介紹了應用層層面的 ota升級包的下載、校驗以及最後的發起安裝過程。在這裏,重要講解進入Recovery模式後,OTA包的升級過程。
首先,在應用層下載升級包後,會調用RecoverySystem.installPackage(Context context, File packageFile)函數來發起安裝過程,這個過程主要的原理,實際上只是往 /cache/recovery/command 寫入ota升級包存放路徑,而後重啓到recovery模式,僅此而已。
public static void installPackage(Context context, File packageFile) throws IOException { String filename = packageFile.getCanonicalPath(); Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); final String filenameArg = "--update_package=" + filename; final String localeArg = "--locale=" + Locale.getDefault().toString(); bootCommand(context, filenameArg, localeArg); } private static void bootCommand(Context context, String... args) throws IOException { RECOVERY_DIR.mkdirs(); // In case we need it COMMAND_FILE.delete(); // In case it's not writable LOG_FILE.delete(); FileWriter command = new FileWriter(COMMAND_FILE); try { for (String arg : args) { if (!TextUtils.isEmpty(arg)) { command.write(arg); command.write("\n"); } } } finally { command.close(); } // Having written the command file, go ahead and reboot PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); pm.reboot(PowerManager.REBOOT_RECOVERY); throw new IOException("Reboot failed (no permissions?)"); }
所以,實質上等同於如下命令:
echo -e "--update_package=/mnt/sdcard/ota/update.zip" > /cache/recovery/command reboot recovery
OTA升級包的目錄結構大體以下所示:
|----boot.img |----system/ |----recovery/ |----recovery-from-boot.p |----etc/ `|----install-recovery.sh |---META-INF/ |CERT.RSA |CERT.SF |MANIFEST.MF |----com/ |----google/ |----android/ |----update-binary |----updater-script |----android/ |----metadata
其中:
out/host/linux-x86/framework/signapk.jar build/target/product/security/testkey.x509.pem build/target/product/security/testkey.pk8
MANIFEST.MF:這個manifest文件定義了與包的組成結構相關的數據。相似Android應用的mainfest.xml文件。
CERT.RSA:與簽名文件相關聯的簽名程序塊文件,它存儲了用於簽名JAR文件的公共簽名。
CERT.SF:這是JAR文件的簽名文件,其中前綴CERT表明簽名者。
進入Recovery模式以後,便開始對下載的升級包進行升級,總體的流程圖以下所示:
BCB(Bootloader與Recovery經過BCB(Bootloader Control Block)通訊)
這裏,詳解介紹一下升級流程中的各個模塊。
get_args的原理流程圖以下所示:
get_args()函數的主要做用是獲取系統的啓動參數,並回寫到bootloader control block(BCB)塊中。若是系統在啓動recovery時已經傳遞了啓動參數,那麼這個函數只是把啓動參數的內容複製到函數的參數boot對象中,不然函數會首先經過get_bootloader_message()函數從/misc分區的BCB中獲取命令字符串來構建啓動參數。若是/misc分區下沒有內容,則會嘗試解析/cache/recovery/command文件並讀取文件的內容來創建啓動參數。
接着,會把啓動參數的信息經過set_bootloader_message()函數又保存到了BCB塊中。這樣作的目的是防止一旦升級或擦除數據的過程當中發生崩潰或不正常斷電,下次重啓,Bootloader會依據BCB的指示,引導進入Recovery模式,從/misc分區中讀取更新的命令,繼續進行更新操做。所以,能夠說是一種掉電保護機制。
get_args具體的流程以下圖所示:
get_args函數核心代碼以下:
static void get_args(int *argc, char ***argv) { struct bootloader_message boot; memset(&boot, 0, sizeof(boot)); //解析BCB模塊 get_bootloader_message(&boot); // this may fail, leaving a zeroed structure ...... // --- if that doesn't work, try the command file if (*argc <= 1) { FILE *fp = fopen_path(COMMAND_FILE, "r");//COMMAND_FILE指/cache/recovery/command if (fp != NULL) { char *argv0 = (*argv)[0]; *argv = (char **) malloc(sizeof(char *) * MAX_ARGS); (*argv)[0] = argv0; // use the same program name char buf[MAX_ARG_LENGTH]; for (*argc = 1; *argc < MAX_ARGS; ++*argc) { if (!fgets(buf, sizeof(buf), fp)) break; (*argv)[*argc] = strdup(strtok(buf, "\r\n")); // Strip newline. } check_and_fclose(fp, COMMAND_FILE); LOGI("Got arguments from %s\n", COMMAND_FILE); } } ...... set_bootloader_message(&boot); //回寫BCB
這裏須要說一下「BCB」,即bootloader control block, 中文能夠呼之爲「啓動控制模信息塊」**,位於/misc分區,從代碼上看,就是一個struct 結構體 :
struct bootloader_message { char command[32]; char status[32]; char recovery[1024]; };
bootloader_message 結構體包含三個字段,具體含義以下:
command 字段中存儲的是命令,它有如下幾個可能值:
status 字段存儲的是更新的結果。更新結束後,由Recovery或者Bootloader將更新結果寫入到這個字段中。
recovery 字段存放的是recovry模塊的啓動參數,通常包括升級包路徑。其存儲結構以下:第一行存放字符串「recovery」,第二行存放路徑信息「–update_package=/mnt/sdcard/update.zip」等。 所以,參數之間是以「\n」分割的。
ota升級包的存放路徑,從BCB或者/cache/recovery/command裏面解析獲得的,升級包通常下載後存放在cache或sdcard分區,固然,也有一些是存放到U盤之類的外接存儲設備中的。通常賦值格式以下:
--update_package=/mnt/sdcard/update.zip 或 --update_package=CACHE:update.zip
int install_package(const char* path, int* wipe_cache, const char* install_file) { //install_file 爲 /cache/recovery/last_install FILE* install_log = fopen_path(install_file, "w"); if (install_log) { fputs(path, install_log); fputc('\n', install_log); } else { LOGE("failed to open last_install: %s\n", strerror(errno)); } int result = really_install_package(path, wipe_cache); if (install_log) { fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log); fputc('\n', install_log); fclose(install_log); } return result; }
really_install_package函數在install_package函數中被調用,函數的主要做用是調用ensure_path_mounted確保升級包所在的分區已經掛載,另外,還會對升級包進行一系列的校驗,在具體升級時,對update.zip包檢查時大體會分三步:
檢驗SF文件與RSA文件是否匹配;
檢驗MANIFEST.MF與簽名文件中的digest是否一致;
檢驗包中的文件與MANIFEST中所描述的是否一致
經過校驗後,調用try_update_binary函數去實現真正的升級。
try_update_binary是真正實現對升級包進行升級的函數:
static int try_update_binary(const char *path, ZipArchive *zip, int* wipe_cache) { const ZipEntry* binary_entry = mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME); ...... const char* binary = "/tmp/update_binary"; unlink(binary); int fd = creat(binary, 0755); ..... //將升級包裏面的update_binary解壓到/tmp/update_binary bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd); close(fd); mzCloseZipArchive(zip); ...... int pipefd[2]; pipe(pipefd); const char** args = (const char**)malloc(sizeof(char*) * 5); args[0] = binary; //update_binary存放路徑 args[1] = EXPAND(RECOVERY_API_VERSION); // Recovery版本號 char* temp = (char*)malloc(10); sprintf(temp, "%d", pipefd[1]); args[2] = temp; args[3] = (char*)path; //升級包存放路徑 args[4] = NULL; pid_t pid = fork();//fork一個子進程 if (pid == 0) { close(pipefd[0]); //子進程調用update-binary執行升級操做 execv(binary, (char* const*)args); fprintf(stdout, "E:Can't run %s (%s)\n", binary, strerror(errno)); _exit(-1); } ...... int status; waitpid(pid, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { //安裝失敗,返回INSTALL_ERROR return INSTALL_ERROR; } //安裝成功,返回INSTALL_SUCCESS return INSTALL_SUCCESS; }
總的來講,try_update_binary主要作了如下幾個操做:
(1)mzOpenZipArchive():打開升級包,並將相關的信息拷貝到一個臨時的ZipArchinve變量中。注意這一步並未對咱們的update.zip包解壓。
(2)mzExtractZipEntryToFile(): 解壓升級包特定文件,將升級包裏面的META-INF/com/google/android/update-binary 解壓到內存文件系統的/tmp/update_binary中。
(3)fork建立一個子進程 , 使用系統調用函數execv( ) 去執行/tmp/update-binary程序,
(4)update-binary: 這個是Recovery OTA升級的核心程序,是一個二進制文件,實現代碼位於系統源碼bootable/recovery/updater。其實質是至關於一個腳本解釋器,可以識別updater-script中描述的操做並執行。
(5)updater-script:updater-script是咱們升級時所具體使用到的腳本文件,具體描述了更新過程,它主要用以控制升級流程的主要邏輯。具體位置位於升級包中/META-INF/com/google/android/update-script,在咱們製做升級包的時候產生。在升級的時候,由update_binary程序從升級包裏面解壓到內存文件系統的/tmp/update_script中,並按照update_script裏面的命令,對系統進行升級。好比,一個完整包升級的update_script的內容大體以下所示:
assert(getprop("ro.product.device") == "rk31sdk" || getprop("ro.build.product") == "rk301dk"); show_progress(0.500000, 0); format("ext4", "EMMC", "/dev/block/mtd/by-name/system", "0", "/system"); mount("ext4", "EMMC", "/dev/block/mtd/by-name/system", "/system"); package_extract_dir("recovery", "/system"); package_extract_dir("system", "/system"); symlink("Roboto-Bold.ttf", "/system/fonts/DroidSans-Bold.ttf"); symlink("mksh", "/system/bin/sh"); ...... set_perm_recursive(0, 0, 0755, 0644, "/system"); set_perm_recursive(0, 2000, 0755, 0755, "/system/bin"); ...... set_perm(0, 0, 06755, "/system/xbin/su"); set_perm(0, 0, 06755, "/system/xbin/tcpdump"); show_progress(0.200000, 0); show_progress(0.200000, 10); write_raw_image(package_extract_file("boot.img"), "boot"); show_progress(0.100000, 0); clear_misc_command(); unmount("/system");
update_script經常使用的命令以下:
所以,根據上面的升級腳本,能夠知道,升級包的大體升級流程以下:
main函數,在執行完install_package後,會根據傳入的wipe_data/wipe_cache,決定是否執行/data和/cache分區的清空操做。
這個函數的做用就是一直在等待用戶輸入,是一個不斷的循環,能夠選擇Recovery模式下的一些選項進行操做,包括恢復出廠設置和重啓等。若是升級失敗, prompt_and_wait會顯示錯誤,並等待用戶響應。
OTA升級成功,清空misc分區(BCB置零),並將保存到內存系統的升級日誌/tmp/recovery.log保存到/cache/recovery/last_log。重啓設備進入Main System,升級完成。
從上面的流程中,能夠知道,Recovery模式下的OTA升級成功,只是更新了/system和/boot兩個最核心的分區,而自己用來升級的Recovery自身並無在那個時候獲得更新。Recovery分區的更新,是在重啓進入主系統的時候,由install-recovery.sh來更新的。這樣能夠保證,即便升級失敗,Recovery模式也不會受到影響,仍然能夠手動進入Recovery模式執行升級或擦除數據操做。
在Recovery升級的時候,有一句:
package_extract_dir("recovery", "/system");
這條命令就是將升級包裏面的recovery目錄的內容,解壓到/system分區
recovery目錄下的文件,主要有install-recovery.sh和 recovery-from-boot.p,目錄結構以下所示:
├── bin │ └── install-recovery.sh └── recovery-from-boot.p
其中:
至此,一個完整的OTA包升級就正式完成了!
首先,經過前面的介紹,能夠知道, Recovery System與Main System的交互,主要是經過/cache分區下的文件進行信息交互的。具體以下:
其中,command的值通常有如下一個或多個:
其次,Bootloader與Recovery和Main System之間也是存在交互的: Bootloader會經過解析BCB模塊,決定啓動系統到Recovery或Main System。而Recovery或Main System也可以操做BCB,進而影響到Bootloader的行爲。
當Main System系統關鍵進程崩潰太屢次的時候,系統還會自發啓動進入到Recovery模式。
另外,部分平臺的Android設備,在Recovery模式下,也可以對Bootloader進行升級。
Bootloader、BCB、Recovery與Main System四者相互影響,又獨立工做。它們之間斬不斷理還亂的關係,能夠如下圖歸納之: