Android--Recovery模块之升级过程

本节继续分析一下升级过程。Android中sideload的安装方式比较少见,并且是比较复杂的,我们通过分析这种升级方式来了解Recovery的升级过程。

1、sideload安装方式  

在上一节prompt_and_wait函数中处理菜单命令时,如果选择的命令是APPLY_ADB_SIDELOAD,将启动ADBD,让用户通过adb连接来执行sideload命令上传,更新文件到/tmp/update.zip,然后再执行更新操作。在prompt_and_wait函数中的处理如下:

case Device::APPLY_ADB_SIDELOAD:
                status = apply_from_adb(ui, &wipe_cache, TEMPORARY_INSTALL_FILE);//入口函数,/tmp/last_install 
                if (status >= 0) {
                    if (status != INSTALL_SUCCESS) {
                        ui->SetBackground(RecoveryUI::ERROR);
                        ui->Print("Installation aborted.\n");
                        copy_logs();
                    } else if (!ui->IsTextVisible()) {
                        return Device::NO_ACTION;  // reboot if logs aren't visible
                    } else {
                        ui->Print("\nInstall from ADB complete.\n");
                    }
                }
                break;

sideload安装方式的入口是apply_from_adb()函数,位于bootable/recovery/Adb_install.cpp中,代码如下:

int
apply_from_adb(RecoveryUI* ui_, int* wipe_cache, const char* install_file) {
    ui = ui_;

    stop_adbd();//先停止ADBD
    set_usb_driver(true);//启动USB连接
    //打印使用sideload命令的提示
    ui->Print("\n\nNow send the package you want to apply\n"
              "to the device with \"adb sideload \"...\n");

    pid_t child;
    if ((child = fork()) == 0) {//fork一个子进程,然后在子进程中执行recovery
        execl("/sbin/recovery", "recovery", "--adbd", NULL);
        _exit(-1);
    }

    // FUSE_SIDELOAD_HOST_PATHNAME will start to exist once the host
    // connects and starts serving a package.  Poll for its
    // appearance.  (Note that inotify doesn't work with FUSE.)
    int result;
    int status;
    bool waited = false;
    struct stat st;
    for (int i = 0; i < ADB_INSTALL_TIMEOUT; ++i) {
        if (waitpid(child, &status, WNOHANG) != 0) {//等待子进程ADBD结束
            result = INSTALL_ERROR;
            waited = true;
            break;
        }

        if (stat(FUSE_SIDELOAD_HOST_PATHNAME, &st) != 0) {//获取/sideload/package.zip文件信息,并保存在stat结构体中
            if (errno == ENOENT && i < ADB_INSTALL_TIMEOUT-1) {//文件不存在
                sleep(1);
                continue;
            } else {//超时
                ui->Print("\nTimed out waiting for package.\n\n", strerror(errno));
                result = INSTALL_ERROR;
                kill(child, SIGKILL);
                break;
            }
        }
        result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, install_file, false);//文件存在,调用install_package函数开始安装
        break;
    }

    if (!waited) {
        // Calling stat() on this magic filename signals the minadbd
        // subprocess to shut down.
        stat(FUSE_SIDELOAD_HOST_EXIT_PATHNAME, &st);

        // TODO(dougz): there should be a way to cancel waiting for a
        // package (by pushing some button combo on the device).  For now
        // you just have to 'adb sideload' a file that's not a valid
        // package, like "/dev/null".
        waitpid(child, &status, 0);
    }

    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
        if (WEXITSTATUS(status) == 3) {
            ui->Print("\nYou need adb 1.0.32 or newer to sideload\nto this device.\n\n");
        } else if (!WIFSIGNALED(status)) {
            ui->Print("\n(adbd status %d)\n", WEXITSTATUS(status));
        }
    }

    set_usb_driver(false);//关闭USB连接
    maybe_restart_adbd();

    return result;
}
apply_from_adb()函数会创建一个子进程,这个子进程执行recovery文件,但是启动的参数是--adbd,在前面Recovery的main函数中介绍过,如果启动参数带有adbd,将会调用adb_main函数,这时候recovery实际上变成了一个简单的ADBD的守护进程。这个简单的ADBD的主要作用就是接收从用户上传的更新包文件,然后放到/tmp/目录下,并改名为update.zip,得到文件后ADBD就会结束。recovery模块启动子进程后,会调用waitpid等待它结束,然后才继续执行,如果recovery在temp目录下发现了update.zip文件,则调用install_package函数来执行安装。

2、升级的入口函数

系统升级的入口为install_package()函数,代码位于/bootable/recovery/Install.cpp中,如下:

int
install_package(const char* path, int* wipe_cache, const char* install_file,
                bool needs_mount)
{
    FILE* install_log = fopen_path(install_file, "w");
    if (install_log) {//检查文件/tmp/last_install是否存在
        fputs(path, install_log);//将新的文件路径/sideload/package.zip记录到/tmp/last_install文件中 
        fputc('\n', install_log);
    } else {
        LOGE("failed to open last_install: %s\n", strerror(errno));
    }
    int result;
    if (setup_install_mounts() != 0) {//确保/tmp和/cache分区已经mount
        LOGE("failed to set up expected mounts for install; aborting\n");
        result = INSTALL_ERROR;
    } else {
        result = really_install_package(path, wipe_cache, needs_mount);//继续执行安装
    }
    if (install_log) {
        fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log);//记录更新是否成功
        fputc('\n', install_log);
        fclose(install_log);
    }
    return result;
}
install_package函数首先调用setup_install_mounts函数来mount安装过程中需要使用的目录,因为这些目录可能还没有装载,或者中间又被卸载了。setup_install_mounts函数位于/bootable/recovery/Roots.cpp中,如下:

int setup_install_mounts() {
    if (fstab == NULL) {
        LOGE("can't set up install mounts: no fstab loaded\n");
        return -1;
    }
    for (int i = 0; i < fstab->num_entries; ++i) {
        Volume* v = fstab->recs + i;

        if (strcmp(v->mount_point, "/tmp") == 0 ||
            strcmp(v->mount_point, "/cache") == 0) {
            if (ensure_path_mounted(v->mount_point) != 0) {//只mount /tmp和/cache分区
                LOGE("failed to mount %s\n", v->mount_point);
                return -1;
            }

        } else {
            if (ensure_path_unmounted(v->mount_point) != 0) {//unmount其他分区
                LOGE("failed to unmount %s\n", v->mount_point);
                return -1;
            }
        }
    }
    return 0;
}
setup_install_mounts函数主要目标是确保/tmp和/cache分区加载成功,同时将其他分区卸载。之后install_package调用really_install_package()继续安装。

static int
really_install_package(const char *path, int* wipe_cache, bool needs_mount)
{
    ui->SetBackground(RecoveryUI::INSTALLING_UPDATE);//更新屏幕的提示为正在更新
    ui->Print("Finding update package...\n");
    // Give verification half the progress bar...
    ui->SetProgressType(RecoveryUI::DETERMINATE);
    ui->ShowProgress(VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME);
    LOGI("Update location: %s\n", path);

    // Map the update package into memory.
    ui->Print("Opening update package...\n");

    if (path && needs_mount) {//确保更新包所在的路径已经mount
        if (path[0] == '@') {
            ensure_path_mounted(path+1);
        } else {
            ensure_path_mounted(path);
        }
    }

    MemMapping map;
    if (sysMapFile(path, &map) != 0) {
        LOGE("failed to map file\n");
        return INSTALL_CORRUPT;
    }

    int numKeys;//从/res/keys文件中装载设备的签名文件
    Certificate* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);
    if (loadedKeys == NULL) {//找不到系统文件
        LOGE("Failed to load keys\n");
        return INSTALL_CORRUPT;
    }
    LOGI("%d key(s) loaded from %s\n", numKeys, PUBLIC_KEYS_FILE);

    ui->Print("Verifying update package...\n");

    int err;//校验更新包的签名
    err = verify_file(map.addr, map.length, loadedKeys, numKeys);
    free(loadedKeys);
    LOGI("verify_file returned %d\n", err);
    if (err != VERIFY_SUCCESS) {//校验失败,退出
        LOGE("signature verification failed\n");
        sysReleaseMap(&map);
        return INSTALL_CORRUPT;
    }

    /* Try to open the package.
     */
    ZipArchive zip;
    err = mzOpenZipArchive(map.addr, map.length, &zip);//打开文件
    if (err != 0) {
        LOGE("Can't open %s\n(%s)\n", path, err != -1 ? strerror(err) : "bad");
        sysReleaseMap(&map);
        return INSTALL_CORRUPT;
    }

    /* Verify and install the contents of the package.
     */
    ui->Print("Installing update...\n");
    ui->SetEnableReboot(false);
    int result = try_update_binary(path, &zip, wipe_cache);//开始安装
    ui->SetEnableReboot(true);
    ui->Print("\n");

    sysReleaseMap(&map);

    return result;
}
really_install_package函数最主要的工作就是校验更新包的签名。之后调用try_update_binary继续安装过程,代码如下

// If the package contains an update binary, extract it and run it.
static int
try_update_binary(const char *path, ZipArchive *zip, int* wipe_cache) {
    const ZipEntry* binary_entry =
            mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME);//在zip格式的更新包文件中得到文件META-INF/com/google/android/update-binary的entry
    if (binary_entry == NULL) {
        mzCloseZipArchive(zip);
        return INSTALL_CORRUPT;//安装包中找不到update_binary文件
    }

    const char* binary = "/tmp/update_binary";
    unlink(binary);
    int fd = creat(binary, 0755);//创建空文件/tmp/update_binary
    if (fd < 0) {
        mzCloseZipArchive(zip);
        LOGE("Can't make %s\n", binary);
        return INSTALL_ERROR;//创建文件失败,返回
    }
    bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd);//把zip更新包中的update_binary文件保存到/tmp/update_binary文件中
    close(fd);
    mzCloseZipArchive(zip);

    if (!ok) {
        LOGE("Can't copy %s\n", ASSUMED_UPDATE_BINARY_NAME);
        return INSTALL_ERROR;//解压缩失败,返回
    }

    int pipefd[2];
    pipe(pipefd);//创建管道

    const char** args = (const char**)malloc(sizeof(char*) * 5);//准备参数
    args[0] = binary;
    args[1] = EXPAND(RECOVERY_API_VERSION);   // defined in Android.mk
    char* temp = (char*)malloc(10);
    sprintf(temp, "%d", pipefd[1]);
    args[2] = temp;
    args[3] = (char*)path;
    args[4] = NULL;

    pid_t pid = fork();
    if (pid == 0) {
        umask(022);
        close(pipefd[0]);
        execv(binary, (char* const*)args);//在子进程中执行/tmp/update_binary文件
        fprintf(stdout, "E:Can't run %s (%s)\n", binary, strerror(errno));
        _exit(-1);
    }
    close(pipefd[1]);

    *wipe_cache = 0;
    //父进程通过管道接收子进程的命令,主要的命令就是用来更新UI,显示更新进度和提示信息
    char buffer[1024];
    FILE* from_child = fdopen(pipefd[0], "r");
    while (fgets(buffer, sizeof(buffer), from_child) != NULL) {
        char* command = strtok(buffer, " \n");
        if (command == NULL) {
            continue;
        } else if (strcmp(command, "progress") == 0) {//更新进度提示
            char* fraction_s = strtok(NULL, " \n");
            char* seconds_s = strtok(NULL, " \n");

            float fraction = strtof(fraction_s, NULL);
            int seconds = strtol(seconds_s, NULL, 10);

            ui->ShowProgress(fraction * (1-VERIFICATION_PROGRESS_FRACTION), seconds);
        } else if (strcmp(command, "set_progress") == 0) {//设置进程
            char* fraction_s = strtok(NULL, " \n");
            float fraction = strtof(fraction_s, NULL);
            ui->SetProgress(fraction);
        } else if (strcmp(command, "ui_print") == 0) {//更新UI提示
            char* str = strtok(NULL, "\n");
            if (str) {
                ui->Print("%s", str);
            } else {
                ui->Print("\n");
            }
            fflush(stdout);
        } else if (strcmp(command, "wipe_cache") == 0) {
            *wipe_cache = 1;
        } else if (strcmp(command, "clear_display") == 0) {
            ui->SetBackground(RecoveryUI::NONE);//清除背景
        } else if (strcmp(command, "enable_reboot") == 0) {
            // packages can explicitly request that they want the user
            // to be able to reboot during installation (useful for
            // debugging packages that don't exit).
            ui->SetEnableReboot(true);
        } else {
            LOGE("unknown command [%s]\n", command);
        }
    }
    fclose(from_child);

    int status;
    waitpid(pid, &status, 0);//等待子进程执行完毕
    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
        LOGE("Error in %s\n(Status %d)\n", path, WEXITSTATUS(status));
        return INSTALL_ERROR;
    }

    return INSTALL_SUCCESS;
}

从try_update_binary函数可以看到,最后的文件更新过程其实还不是在recovery进程中完成的。try_update_binary()函数把更新包中的META_INF/com/google/android/update-binary文件解压缩出来放到目录/tmp中,然后创建子进程来运行update-binary可执行文件,update-binary将根据更新包中的update-script脚本来更新系统文件。在子进程执行更新过程中,recovery进程通过管道和子进程保持联系,子进程把更新的进度通过管道传递给recovery,由recovery显示出来。

3、update-binary模块

update-binary才是最后执行系统升级的模块。Android中update-binary模块的源文件位于bootable/recovery/updater中,这个目录下的Android.mk中指定的模块名是updater,update-binary是在制作更新包时指定的一个新的文件名。

update-binary模块的执行流程,代码位于/bootable/recovery/updater/Updater.cpp,如下:

int main(int argc, char** argv) {
    // Various things log information to stdout or stderr more or less
    // at random (though we've tried to standardize on stdout).  The
    // log file makes more sense if buffering is turned off so things
    // appear in the right order.
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    if (argc != 4) {//检查参数
        printf("unexpected number of arguments (%d)\n", argc);
        return 1;
    }

    char* version = argv[1];
    if ((version[0] != '1' && version[0] != '2' && version[0] != '3') ||
        version[1] != '\0') {//检查版本,只支持1、2、3
        // We support version 1, 2, or 3.
        printf("wrong updater binary API; expected 1, 2, or 3; "
                        "got %s\n",
                argv[1]);
        return 2;
    }

    // Set up the pipe for sending commands back to the parent process.

    int fd = atoi(argv[2]);
    FILE* cmd_pipe = fdopen(fd, "wb");//打开从父进程中创建的管道
    setlinebuf(cmd_pipe);//设置文件流为线性缓冲区

    // Extract the script from the package.

    const char* package_filename = argv[3];//打开更新包中的更新脚本文件
    MemMapping map;
    if (sysMapFile(package_filename, &map) != 0) {
        printf("failed to map package %s\n", argv[3]);
        return 3;
    }
    ZipArchive za;
    int err;
    err = mzOpenZipArchive(map.addr, map.length, &za);//打开升级包,并将相关的信息拷贝到一个临时的ZipArchinve变量中
    if (err != 0) {
        printf("failed to open package %s: %s\n",
               argv[3], strerror(err));
        return 3;
    }

    const ZipEntry* script_entry = mzFindZipEntry(&za, SCRIPT_NAME);//META-INF/com/google/android/updater-script
    if (script_entry == NULL) {
        printf("failed to find %s in %s\n", SCRIPT_NAME, package_filename);
        return 4;
    }

    char* script = malloc(script_entry->uncompLen+1);
    if (!mzReadZipEntry(&za, script_entry, script, script_entry->uncompLen)) {
        printf("failed to read script from package\n");
        return 5;
    }
    script[script_entry->uncompLen] = '\0';

    // Configure edify's functions.//注册处理命令的函数(识别脚本中命令的函数)

    RegisterBuiltins();//注册程序中控制流程的语句,如ifelse,assert等
    RegisterInstallFunctions();//实际安装过程中安装所需的功能函数
    RegisterBlockImageFunctions();
    RegisterDeviceExtensions();//与设备相关的额外添加项。
    FinishRegistration();//结束注册

    // Parse the script.//解析脚本文件

    Expr* root;
    int error_count = 0;
    int error = parse_string(script, &root, &error_count);//解析脚本文件,解析的结果保存到root中
    if (error != 0 || error_count > 0) {
        printf("%d parse errors\n", error_count);
        return 6;
    }

    struct selinux_opt seopts[] = {
      { SELABEL_OPT_PATH, "/file_contexts" }
    };

    sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1);

    if (!sehandle) {
        fprintf(cmd_pipe, "ui_print Warning: No file_contexts\n");
    }

    // Evaluate the parsed script.//执行脚本文件

    UpdaterInfo updater_info;
    updater_info.cmd_pipe = cmd_pipe;
    updater_info.package_zip = &za;
    updater_info.version = atoi(version);
    updater_info.package_zip_addr = map.addr;
    updater_info.package_zip_len = map.length;

    State state;
    state.cookie = &updater_info;
    state.script = script;
    state.errmsg = NULL;

    char* result = Evaluate(&state, root);//核心函数是Evaluate(),它会调用其他的callback函数,而这些callback函数又会去调用Evaluate去解析不同的脚本片段,从而实现一个简单的脚本解释器。

    if (result == NULL) {
        if (state.errmsg == NULL) {
            printf("script aborted (no error message)\n");
            fprintf(cmd_pipe, "ui_print script aborted (no error message)\n");
        } else {
            printf("script aborted: %s\n", state.errmsg);
            char* line = strtok(state.errmsg, "\n");
            while (line) {
                fprintf(cmd_pipe, "ui_print %s\n", line);
                line = strtok(NULL, "\n");
            }
            fprintf(cmd_pipe, "ui_print\n");
        }
        free(state.errmsg);
        return 7;
    } else {
        fprintf(cmd_pipe, "ui_print script succeeded: result was [%s]\n", result);
        free(result);
    }
    //执行完毕,释放内存,准备退出
    if (updater_info.package_zip) {
        mzCloseZipArchive(updater_info.package_zip);
    }
    sysReleaseMap(&map);
    free(script);

    return 0;
}
main函数的流程如下:

  1. 检查启动参数和版本,目前支持的版本是1、2、3.
  2. 打开父进程中创建的管道,在执行更新的过程中会通过管道通知父进程更新UI显示。
  3. 从更新包中读取updater-script脚本到内存中。
  4. 注册用于处理脚本中各种命令的函数。
  5. 调用parse_string函数来解析脚本文件,解析的结果将存放到Expr类型的对象root中。
  6. 调用函数Evaluate()执行脚本。
  7. 结束执行,释放内存,退出。
接下来,我们看看Evaluate函数的代码,位于/bootable/recovery/edify/Expr.c中,如下:
char* Evaluate(State* state, Expr* expr) {
    Value* v = expr->fn(expr->name, state, expr->argc, expr->argv);
    if (v == NULL) return NULL;
    if (v->type != VAL_STRING) {
        ErrorAbort(state, "expecting string, got value type %d", v->type);
        FreeValue(v);
        return NULL;
    }
    char* result = v->data;
    free(v);
    return result;
}
Evaluate()函数主要是调用了expr的fn()函数,参数expr的类型是Expr,定义如下:
struct Expr {
    Function fn;
    char* name;
    int argc;
    Expr** argv;
    int start, end;
};
从Expr的定义中可以看到它有一个字段argv,这个字段是Expr指针的指针类型,它实际上会指向一个Expr指针的数组对象,表示Expr对象的所有下一级对象。通过这个字段,脚本解析后得到的所有命令都串接在一起,而且命令的执行函数还会调用Ecaluate()来继续执行argv中的Expr对象,因此,虽然Evaluate()中只调用了root对象的fn()函数,但是实际上会执行脚本中的所有命令。
4、update-scropt的语法规则
update-script文件的格式还是比较简单的,基本上都是命令行组成,每种命令都有自己的参数。
  • assert(condition):如果condition参数的计算结果为False,则停止脚本执行,否则继续执行脚本。
  • format(fs_type,partition_type,location):格式化分区为指定的文件系统。fs_type,文件系统类型,取值一般为“yaffs2”或“ext4”;partition_type,分区类型,一般为“MTD”或“EMMC”;第三个参数指定分区名称,如“system”或“cache”。
  • mount(fs_type,partition_type,location,mount_point):挂载一个文件系统到指定的目录。前三个参数与format相同,mount_point表示要挂载的目录。
  • show_progress(frac,sec):frac 表示进度完成的数值,sec 表示整个过程的总秒数,但是一般指定为0,表示不显示总时间。主要用与显示 UI 的进度条。
  • package_extract_dir(src_path,destination_path):从更新包中解压缩一个目录到指定的位置。第一个参数是压缩包中的目录名称,第二个参数是目标路径。package_extract_dir(“system”,”/system”)
  • package_extract_file(srcfile_path,desfile_path):从更新版本中解压缩一个文件到指定位置。srcfile_path,要提取的文件;desfile_path,提取文件的目标位置。package_extract_file(“boot.img”,”/tmp/boot.img”)将升级包中的 boot.img 文件拷贝到内存文件系统的/tmp 下。
  • symlink(target,src1,src2,……,srcN):进行符号连接。target是符号连接的目标。SrcX 代表要创建的符号连接的目标点。symlink(“toolbox”,”/system/bin/ps”),建立指向 toolbox 符号连接/system/bin/ps,值得注意的是,在建立新的符号连接之前,要断开已经存在的符号连接。
  • set_perm(uid,gid,mode,file1,file2,……,fileN):作用是设置单个文件或则一系列文件的uid、gid、权限,最少要指定一个文件。
  • set_perm_recursive(uid,gid,mode,dir1,dir2,……,dirN):作用同上,但是这里同时改变的是一个或多个目录及其子目录和文件的uid、给id、权限。
  • write_raw_image(src-image,partition):更新一个分区。src-image压缩包中的源镜像文件,partition,目标分区的名称。作用:将镜像写入目标分区。示例:write_raw_image(“/tmp/boot.img”,”boot”)boot.img 镜像写入到系统的 boot 分区。
  • getprop(key):通过指定key 的值来获取对应的属性信息。示例:getprop(“ro.product.device”)获取ro.product.device 的属性值。

如果希望了解所有update-binary支持的命令,可以看看RegisterBuiltins()函数和RegisterInstallFunctions()函数。

你可能感兴趣的:(Android系统)