相关文章推荐
急躁的伏特加  ·  java.lang.NullPointerE ...·  3 天前    · 
销魂的棒棒糖  ·  laravel 的EXCEL读写 ...·  1 年前    · 
八块腹肌的松鼠  ·  Disabling ...·  1 年前    · 
逆袭的鸭蛋  ·  git 强制push - OSCHINA ...·  1 年前    · 


最近做了一个高通平台安卓的需求,功能使得data分区在第一次启动时,自动适配emmc/ufs的实际大小,在此过程中对init的执行以及.rc文件的解析流程有了一些理解,但是对于一些细节的东西还不清楚,在这里提出几个自己疑惑的关键问题,趁热打铁!!梳理并寻找答案!!!

这里以 高通 平台为例,基于最新的 安卓11 ,init这块的代码mtk与高通基本是一模一样的(差异很小),都是中间层的东西;

1,init进程在第二初始阶段如何加载init.rc,在整个工程中有几个地方存放rc文件,存放有什么规律??

2,根据目前的理解,rc文件中的cmd最终映射到了对应的函数执行,如下图,他们是什么时候被调用的?? 例如:rc文件中cmd  mount_all  最终会映射执行do_mount_all()函数;

Android ecc加解密 安卓11rec解密_linux

3,*.rc文件加载的时候就伴随执行吗,还是是分开的??

4,目前看分区并不是一次性挂载的,分区分了几次挂载,这几次区分有什么规律??

解析linux配置脚本init.rc文件,根据init.rc文件的内容,init进程会装载Android的文件系统,创建系统目录,初始化属性系统,启动android系统重要的守护进程, 这些进程包括USB守护进程,adb守护进程,vold守护进程,rild守护进程等;

最后init进程也会作为守护进程来执行修改属性请求,重启崩溃的进程操作;

init进程初始化过程

init进程的源码位于目录system/core/init 下,程序的入口函数main()位于文件main.cpp中;

main函数的流程;

int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
    __asan_set_error_report_callback(AsanReportCallback);
#endif
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);//uevent的初始化
    if (argc > 1) {
        if (!strcmp(argv[1], "subcontext")) {
            android::base::Initialling(argv, &android::base::KernelLogger);
            const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();//命令映射表
            return SubcontextMain(argc, argv, &function_map);
        if (!strcmp(argv[1], "selinux_setup")) {//selinux的初始化设置
            return SetupSelinux(argv);
        if (!strcmp(argv[1], "second_stage")) {
            return SecondStageMain(argc, argv); //init的第二阶段初始化
    return FirstStageMain(argc, argv); //init的第一阶段初始化
system/core/init/main.cpp

main函数一开始,首先初始化了守护进程uevent,然后紧接着的就是init的初始化过程;

init引导序列分为三个主要阶段:1 first_stage_init;

2 selinux_setup;(可选)

3 second_stage_init;

其中 first_stage_init负责设置最低限度的基本需求用以加载系统其余部分,具体来说,包括“/dev”,“/proc”的挂载,挂载“early mount”分区(这包括所有包含系统代码的分区,例如system和vendor)对于有ramdisk的设备,将system.img挂载到“/”;

一旦first_stage_init完成接着执行 execs /system/bin/init 并以“selinux_setup”作为参数,在这个阶段,SELinux可选地编译并加载到系统中,这个阶段主要是加载初始化selinux相关的东西,关于此更多的细节在Selinux.cpp中;

最后,一旦该阶段结束,它将再次使用"second_stage"参数执行/system/bin/init。此时,init的主要阶段将运行并通过init继续引导init.rc脚本。

由此我们可以知道rc文件的解析执行均在init的第二阶段,因此我们需要重点关注init初始化的第二阶段;

second_stage_init;

...

初始化属性系统;

初始化信号;

LoadBootScripts(am, sm); //加载*.rc文件

进入while(1)循环,监听处理到达的事件;

init.rc语言

这部分将介绍init.rc文件的格式这是理解解析过程的根本;

Android Init语言由5大类语句组成:
Actions, Commands, Services, Options, 和 Imports.

以下是对各个语句的简单解释

actions

actions其实就是以序列的commands的集合,每个actions都有一个trigger,它用于决定action的执行时机,当一个符合action触发条件的事件发生了,此action会加入到执行队列的末尾,除非它已经在队列里;

每一个action都将以此从队列中取出,此action的每个command都将依次执行,在这些命令执行时init还同时处理这其他活动(设备节点的创建和销毁,设置属性,重启进程);

services :

services是一个后台程序,在init中启动,如果退出了可以由系统重启(可选);

options

options是services的修饰项,它们决定一个services何时以及如何运行;

triggers

Triggers是一个用于匹配某种事件类型的字符串,它将使对应的actions执行;

触发器分为事件触发器和属性触发器。

事件触发器
是由'trigger'命令或init可执行文件中的QueueEventTrigger()函数触发的字符串。它们采用简单字符串的形式,如'boot'或'late-init'。

属性触发器
是指指定属性将值更改为给定的新值或指定属性将值更改为任何新值时触发的字符串。它们分别采用'property:='和'property:=\*'的形式。属性触发器将在init的初始引导阶段额外计算并相应地触发。

一个操作可以有多个属性触发器,但可能只有一个事件触发器。

commands

command是actions的命令列表中的命令,或者是service的选项参数命令;

import

一般用作 “import <path>”,扩展当前配置。如果path是一个目录,该目录中的每个文件都被解析为一个配置文件。它不是递归的,嵌套的目录将不会被解析。

import关键字不是命令,而是它自己的部分,这意味着它不是作为action的一部分发生的,而是在解析文件时处理导入;

第一级安装设备的实际顺序是:
1. 解析/init.rc,然后递归地解析它的每个导入(此处递归是import的递归,不是文件夹的递归,文件夹不支持递归);
2. /system/etc/init/的内容按字母顺序排列并按顺序解析,在每个文件解析后递归地进行导入;
3. 步骤2重复/vendor/etc/init,然后是/odm/etc/init;

-----------------

/init.rc是主要的.rc文件,由init可执行文件在开始执行时加载。它负责系统的初始设置。

在加载主目录/init.rc后,init立即加载包含在/{system,vendor,odm}/etc/init/目录中的所有文件。

rc文件的存放目录以及目的:
1 /system/etc/init/  用于核心系统项,例如 SurfaceFlinger, MediaService和logd。
2 /vendor/etc/init/  是针对SoC供应商的项目,如SoC核心功能所需的actions或守护进程。
3 /odm/etc/init/      用于设备制造商的项目,如actions或运动传感器或其他外围功能所需的守护进程。

以下是个人认为理解init.rc脚本重要的几点内容:

1,init.rc文件是以section为单位组织的,一个section可以包含多行,section可以分为两大类,一类是action,另一类是service;action以关键字on开始,表示一组命令的集合,service以关键字service开始,表示启动某个进程的方式和参数;

2,section以on或service开始,直到下一个on或者service结束,中间的所有行都属于这一个section(空行或者注释不具有分割作用),截取init.rc部分如下

on property:apexd.status=ready && property:ro.product.cpu.abilist32=*
    exec_start boringssl_self_test_apex32
on property:apexd.status=ready && property:ro.product.cpu.abilist64=*
    exec_start boringssl_self_test_apex64
service boringssl_self_test32 /system/bin/boringssl_self_test32
    setenv BORINGSSL_SELF_TEST_CREATE_FLAG true # Any nonempty value counts as true
    reboot_on_failure reboot,boringssl-self-check-failed
    stdio_to_kmsg

根据1,2可以得出,上图有3个section,其中两个是action,一个是service;

3,无论是action还是service,并不是按照文件的编排顺序执行的,他们只是一份定义,至于执行与否以及什么时候执行取决于init在运行时的操作.

脚本文件的解析过程:

init进程启动时最重要的工作就是解析并执行启动文件init.rc,本节介绍init进程解析脚本文件的流程;

这里我们主要以解析action为例讲解,service的解析过程同理,首先讲解基本流程,然后列举实例进行解析分析;

init.rc文件以及*.rc文件加载是从下面这个函数开始的:

static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
    Parser parser = CreateParser(action_manager, service_list);
    std::string bootscript = GetProperty("ro.boot.init_rc", "");
    if (bootscript.empty()) {
        parser.ParseConfig("/system/etc/init/hw/init.rc");
        if (!parser.ParseConfig("/system/etc/init")) {
            late_import_paths.emplace_back("/system/etc/init");
        // late_import is available only in Q and earlier release. As we don't
        // have system_ext in those versions, skip late_import for system_ext.
        parser.ParseConfig("/system_ext/etc/init");
        if (!parser.ParseConfig("/product/etc/init")) {
            late_import_paths.emplace_back("/product/etc/init");
        if (!parser.ParseConfig("/odm/etc/init")) {
            late_import_paths.emplace_back("/odm/etc/init");
        if (!parser.ParseConfig("/vendor/etc/init")) {
            late_import_paths.emplace_back("/vendor/etc/init");
    } else {
        parser.ParseConfig(bootscript);
}

以下是主要的解析流程:

Android ecc加解密 安卓11rec解密_init_02

ParseConfig 函数传入需要解析的rc文件的路径,如果是目录,则遍历该目录取出所有的 rc 文件并调用 ParseConfigFile 函数进行解析,如果是文件路径,则直接调用 ParseConfigFile 函数进行解析。

从代码中可以看出,init 解析 rc 文件的过程中,首先调用 ReadFile 函数将 rc 文件的内容全部保存为字符串,存在 data 中,然后调用 ParseData 进行解析。ParseData 函数会根据关键字解析出service和action,最终挂在到 service_list 与 action_manager  的向量(vector)上。

下面分析一下 ParseData 函数,根据关键字的不同会调用不同的 parser 去解析(多态),action 使用 ActionParser,而 service 使用 ServiceParser 解析,该部分定义在LoadBootScrip()函数的第一行,parser.AddSectionParser()方法为parser的map成员section_parsers_创建了三个SectionParser,分别用来解析service,on,import的section;

Android ecc加解密 安卓11rec解密_android_03

下面重点分析ParseData函数:

next_token(&parse_state)处理数据类型使用parse_state的构体返回:

struct parse_state
    char *ptr;      // 要解析的字符串
    char *text;     // 解析到的字符串,可以理解为返回一行的数据
    int line;       // 解析到第行数
    int nexttoken;  // 解析状态,有 T_EOF T_NEWLINE T_TEXT
};//其中 T_EOF 表示字符串解析结束,T_NEWLINE 表示解析完一行的数据,T_TEXT 表示解析到一个单词

其中 T_EOF 表示字符串解析结束,T_NEWLINE 表示解析完一行的数据,T_TEXT 表示解析到一个单词,

void Parser::ParseData(const std::string& filename, std::string* data) {
    data->push_back('\n');  // TODO: fix tokenizer
    data->push_back('\0');
    parse_state state;
    state.line = 0;
    state.ptr = data->data();
    state.nexttoken = 0;
    SectionParser* section_parser = nullptr;
    int section_start_line = -1;
    std::vector<std::string> args;
    // If we encounter a bad section start, there is no valid parser object to parse the subsequent
    // sections, so we must suppress errors until the next valid section is found.
    bool bad_section_found = false;
    auto end_section = [&] {//lambda类似于一个函数指针
        bad_section_found = false;
        if (section_parser == nullptr) return;
        //同样是调用相应的section的EndSection()结束该section的解析;
        if (auto result = section_parser->EndSection(); !result.ok()) {
            parse_error_count_++;
            LOG(ERROR) << filename << ": " << section_start_line << ": " << result.error();
        section_parser = nullptr;
        section_start_line = -1;
    for (;;) {
        switch (next_token(&state)) {
            case T_EOF:
                end_section();
                for (const auto& [section_name, section_parser] : section_parsers_) {
                    section_parser->EndFile();//解析到文件末尾,则结束
                return;
            case T_NEWLINE: { // 开始处理新的一行
                state.line++;
                if (args.empty()) break;
                // If we have a line matching a prefix we recognize, call its callback and unset any
                // current section parsers.  This is meant for /sys/ and /dev/ line entries for
                // uevent.
            /*    auto line_callback = std::find_if(
                    line_callbacks_.begin(), line_callbacks_.end(),
                    [&args](const auto& c) { return android::base::StartsWith(args[0], c.first); });
                if (line_callback != line_callbacks_.end()) {
                    end_section();
                    if (auto result = line_callback->second(std::move(args)); !result.ok()) {
                        parse_error_count_++;
                        LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
            */              //这部分是uevent的暂时跳过
            //section_parsers_ 是一个map,是在LoadBootScrip()第一行创建了三个键值对  "on"      ---> ActionParser
            //                                                                 "import"  ---> ImportParser
            //                                                                 "service" ---> ServiceParser
            //args[0]为某一行的第一个单词,该行由可能为section的开头,也有可能为command行,count()方法判断该行的第一个单词是不是为on/import/serivce,如果是则返回1
                } else if (section_parsers_.count(args[0])) { 
                    end_section();//在处理新的section前,结束之前的section;
                    section_parser = section_parsers_[args[0]].get(); //依据args[0]是on/import/service取出其对应的解析方法的地址ActionParser/ImportParser/ServiceParser
                    section_start_line = state.line;
                    if (auto result =
                                section_parser->ParseSection(std::move(args), filename, state.line); //调用相应的section解析函数(ActionParser/ImportParser/ServiceParser)解析section
                        !result.ok()) {
                        parse_error_count_++;
                        LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
                        section_parser = nullptr;
                        bad_section_found = true;
                } else if (section_parser) {//该行的第一个单词不是新的section的开头,则使用上一次的解析方法的函数ActionParser/ImportParser/ServiceParser解析其命令行
                    if (auto result = section_parser->ParseLineSection(std::move(args), state.line);//解析命令
                        !result.ok()) {
                        parse_error_count_++;
                        LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
                } else if (!bad_section_found) {
                    parse_error_count_++;
                    LOG(ERROR) << filename << ": " << state.line
                               << ": Invalid section keyword found";
                args.clear();
                break;
            case T_TEXT: //解析到一个单词,就把它存入args中
                args.emplace_back(state.text);
                break;
}

next_token()函数的作用就是寻找单词结束或者行结束标志,如果是单词结束标志就将单词push到args中,如果是行结束标志,则根据第一个单词来判断是否是一个section,section的标志只有三个"on","service","import",如果是"section",则调用相应的ParseSection()来处理一个新的section,否则把这一行继续作为前“section”所属的行来处理。

ParseSection()被用来解析一个新的section,ParseLineSection()被用来解析该section下的命令行,依据注释内容我们可以绘制出解析的基本流程图如下:

Android ecc加解密 安卓11rec解密_安卓_04

针对action,import,service分别定义了其对应的三个函数(不列举import了),这三个函数在解析中扮演了重要的角色;

ActionParser::ParseSection()                               ServiceParser::ParseSection()                  ....

ActionParser::ParseLineSection()                        ServiceParser::ParseLineSection()

ActionParser::EndSection()                                  ServiceParser::EndSection()

还是以下面action为例进行说明,

on boot
    ifup io
    start sshd

首先next_token解析到 on boot这一行,根据args[0] 值为“on” 取出ActionParser的指针,首先调用ActionParser::ParseSection处理这个新的action;

Result<void> ActionParser::ParseSection(std::vector<std::string>&& args,
                                        const std::string& filename, int line) {
    std::vector<std::string> triggers(args.begin() + 1, args.end());
    if (triggers.size() < 1) {
        return Error() << "Actions must have a trigger";
    Subcontext* action_subcontext = nullptr;
    if (subcontext_ && subcontext_->PathMatchesSubcontext(filename)) {
        action_subcontext = subcontext_;
    std::string event_trigger;
    std::map<std::string, std::string> property_triggers;
    if (auto result =
                ParseTriggers(triggers, action_subcontext, &event_trigger, &property_triggers);
        !result.ok()) {
        return Error() << "ParseTriggers() failed: " << result.error();
#ifdef G1122717
    for (const auto& [property, _] : property_triggers) {
        action_manager_->StartWatchingProperty(property);
#endif
    auto action = std::make_unique<Action>(false, action_subcontext, filename, line, event_trigger,
                                           property_triggers);
    action_ = std::move(action);
    return {};
}

BuiltinFunctionMap表找到ifup对应的处理函数,

Result<void> Action::AddCommand(std::vector<std::string>&& args, int line) {
    if (!function_map_) {
        return Error() << "no function map available";
    auto map_result = function_map_->Find(args);
    if (!map_result.ok()) {
        return Error() << map_result.error();
    commands_.emplace_back(map_result->function, map_result->run_in_subcontext, std::move(args),
                           line);
    return {};
}

Android ecc加解密 安卓11rec解密_init_05

如上图ifup对应的处理函数为do_ifup,找到该函数后,根据该函数以及其参数 “io” 构造出一个command实例,并把它push到第一步构造出的action_的成员commands_中,接着继续调用ActionParser::ParseLineSection()处理下一行命令start sshd同样最后把其push到commands_中;

Result<void> ActionParser::EndSection() {
    if (action_ && action_->NumCommands() > 0) {
        action_manager_->AddAction(std::move(action_));
    return {};
}

此时,ActionParser的成员函数action_ 就代表这个解析出来的action的整体,(这里可以看出action类就是对一个整个action的抽象)最后调用ActionParser::EndSection()把这个解析出来的整体的action_添加到ActionParser的单例成员(ActionManager类型)action_manager_中,ActionManager单例成员action_manager_中包含了所有解析出来的action,相当于之前使用的链表,这就是一个action的解析过程;

执行action

action和service解析完成之后,所有的action和service都被挂在ActionParser->action_manager_->actions_ 和ServiceParser->service_list_->services_

am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");
    am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");
    am.QueueBuiltinAction(TestPerfEventSelinuxAction, "TestPerfEventSelinux");
    am.QueueEventTrigger("early-init");
    // Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...
    am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
    // ... so that we can start queuing up actions that require stuff from /dev.
    am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
    am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
    Keychords keychords;
    am.QueueBuiltinAction(
            [&epoll, &keychords](const BuiltinArguments& args) -> Result<void> {
                for (const auto& svc : ServiceList::GetInstance()) {
                    keychords.Register(svc->keycodes());
                keychords.Start(&epoll, HandleKeychord);
                return {};
            "KeychordInit");
    // Trigger all the boot actions to get us started.
    am.QueueEventTrigger("init");

am.QueueEventTrigger("early-init");意为early-init时间已经到来,可以执行triggle只为early-init的action了,QueueEventTrigger()函数只把triggle加入到ActionManager的event_queue_中,对于 am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups") 该函数在创建action的同时,把该事件触发添加到ActionManager的事件队列中,后续会遍历ActionManager的event_queue_找到的triggle对应的action的command会被依次执行,过程如下:

while (true) {
        // By default, sleep until something happens.
        // 决定timeout的时间
        auto epoll_timeout = std::optional<std::chrono::milliseconds>{};
        auto shutdown_command = shutdown_state.CheckShutdown();
        // 判断是否执行了关机
        if (shutdown_command) {
            HandlePowerctlMessage(*shutdown_command);
        //判断是否有事件需要处理
        if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
            am.ExecuteOneCommand();
        if (!IsShuttingDown()) {
            auto next_process_action_time = HandleProcessActions();
            // If there's a process that needs restarting, wake up in time for that.
            if (next_process_action_time) {
                epoll_timeout = std::chrono::ceil<std::chrono::milliseconds>(
                        *next_process_action_time - boot_clock::now());
                if (*epoll_timeout < 0ms) epoll_timeout = 0ms;
        if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
            // If there's more work to do, wake up again immediately.
            if (am.HasMoreCommands()) epoll_timeout = 0ms;
        auto pending_functions = epoll.Wait(epoll_timeout);
        if (!pending_functions.ok()) {
            LOG(ERROR) << pending_functions.error();
        } else if (!pending_functions->empty()) {
            // We always reap children before responding to the other pending functions. This is to
            // prevent a race where other daemons see that a service has exited and ask init to
            // start it again via ctl.start before init has reaped it.
            ReapAnyOutstandingChildren();
            for (const auto& function : *pending_functions) {
                (*function)();
        if (!IsShuttingDown()) {
            HandleControlMessages();
            SetUsbController();
    return 0;
}

init最终会进入了无限循环的监听状态,可以看到这里面一个核心函数就是 am.ExecuteOneCommand();该函数具体如下:

void ActionManager::ExecuteOneCommand() {
        auto lock = std::lock_guard{event_queue_lock_};
        // 编译event_queue_ 队列直到有事件处理
        while (current_executing_actions_.empty() && !event_queue_.empty()) {
            //遍历action的向量(链表),包括所有解析出来的action,每一个action都包含了完整的信息(command,triggle等)
            for (const auto& action : actions_) {
            // 一个action是否要执行,事件trigger和属性trigger都必须要满足,这里检查event_queue_的第一个元素的属性事件是不是满足,会从属性map表里查找其值,如果满足才会执行下一步
                if (std::visit([&action](const auto& event) { return action->CheckEvent(event); },
                               event_queue_.front())) {
                    //如果满足这证明该action需要执行,把action压入current_executing_actions_当前执行队列中;
                    current_executing_actions_.emplace(action.get());
            event_queue_.pop();
    if (current_executing_actions_.empty()) {
        return;
    // 从当前需要执行的action队列中取出第一个要执行的action
    auto action = current_executing_actions_.front();
    if (current_command_ == 0) {
        std::string trigger_name = action->BuildTriggersString();
        LOG(INFO) << "processing action (" << trigger_name << ") from (" << action->filename()
                  << ":" << action->line() << ")";
    // 开始执行action
    action->ExecuteOneCommand(current_command_);
    // 如果这是当前需要执行的action的最后一个命令,则从current_executing_actions_队列中移除该action
    // 如果这个action只执行依次,则从actions_向量中移除它
    ++current_command_;
    if (current_command_ == action->NumCommands()) {
        current_executing_actions_.pop();
        current_command_ = 0;
        if (action->oneshot()) {
            auto eraser = [&action](std::unique_ptr<Action>& a) { return a.get() == action; };
            actions_.erase(std::remove_if(actions_.begin(), actions_.end(), eraser),
                           actions_.end());
}

在上一步中,QueueEventTrigger("early-init")把early-init加入到event_queue_的队列中,ExecuteOneCommand()一开始就遍历之前解析的action向量表,使用每一个action自己的eventtrigger和event_queue_队列中的第一个trigger对比,如果一样,则继续判断该action中的PropertyTriggers,遍历PropertyTriggers的map表查找当前action的PropertyTriggers值是否满足条件,如果在eventtrigger相同且PropertyTriggers满足条件的情况下,就把当前action push到current_executing_actions_队列中;

接下来,从current_executing_actions_队列中取出第一个要执行的action,调用action->ExecuteOneCommand(current_command_); 执行该命令,如下代码

Result<void> Command::InvokeFunc(Subcontext* subcontext) const {
    if (subcontext) {
        if (execute_in_subcontext_) {
            return subcontext->Execute(args_);
        auto expanded_args = subcontext->ExpandArgs(args_);
        if (!expanded_args.ok()) {
            return expanded_args.error();
        return RunBuiltinFunction(func_, *expanded_args, subcontext->context());
    return RunBuiltinFunction(func_, args_, kInitContext);
}

该action下的所有命令被执行完成,;

当一个 action 对象所有的 command 均执行完毕后,再执行下一个action。

当一个 trigger 触发对应的所有 action 对象均执行完毕后(一个action有且仅有一个eventtrigger,但是一个eventtrigger可以对应多个action,他们可以由不同的属性),再执行下一个 trigger 对应 action。

到此就是一个action被解析以及到被执行的整个过程!!因为service和action存在自有的特性,因此解析执行稍有不同,需要理解的可以自行分析;

文章一开始的四个问题,前三个都在解析分析的过程中解开了;

关于分区挂载的后续碰到再另行补充吧!