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四者相互影响,又独立工做。它们之间斩不断理还乱的关系,能够如下图归纳之: