OHOS 3.1的Init进程two_stages相关分析-2-实现部分 原创 精华

liangkz_梁开祝
发布于 2022-5-7 12:26
浏览
2收藏

::: hljs-center

OHOS 3.1的Init进程two_stages相关分析-2-实现部分

:::

::: hljs-center

梁开祝 2022.05.04

:::

:本文可做为《沉浸式剖析OpenHarmony源代码》一书的第5章的5.4小节部分内容的大纲或草稿。】

OHOS LTS3.0版本的标准系统还不支持two_stages,3.1版本开始支持。这里的two_stages是指OHOS 3.1之后的标准系统,从内核态切换到用户态运行init进程时,分成两个stages来完成系统的启动工作:

  • stage0运行在ramdisk中,主要是生成设备节点、挂载根文件系统,并切换到stage1去运行;
  • stage1完成OHOS框架各模块、各进程的启动工作。

不过,OHOS 3.1标准系统烧录到HI3516DV300开发板,跑起来相当吃力,因此本文将基于DAYU200开发板,分别从编译和实现两大部分来对two_stages展开分析,最后再通过log确认一遍相关流程。

【本文很长,分两篇文章来发布:编译部分实现部分

2.实现部分

2.1 initA(stage0)和initB(stage1)的差异

为什么我说initA(stage0)和initB(stage1)是两个完全不同的可执行程序呢?看一下init的实现代码就可以知道了,见//base/startup/init_lite/services/init/main.c

static const pid_t INIT_PROCESS_PID = 1;
int main(int argc, char * const argv[])
{
    int isSecondStage = 0;
    // Number of command line parameters is 2
    if (argc == 2 && (strcmp(argv[1], "--second-stage") == 0)) {
        isSecondStage = 1;
    }
    if (getpid() != INIT_PROCESS_PID) {
        return 0;
    }
    //OHOS 的log部分还没有初始化,在这里用INIT_LOGI是无法打印log出来的。
    printf("###############################################################\n");
    printf("##################[Init ] [Stage%d] [%s]###################\n",
        isSecondStage, isSecondStage?"/System":"RamDisk");
    
    if (isSecondStage == 0) {   //Stage0
        printf("               [-][Init ] [main.c] init_main[5-1][Stage0]: SystemPrepare()\n");
        SystemPrepare();
    } else {                    //Stage1
        printf("               [-][Init ] [main.c] init_main[5-1][Stage1]: LogInit()\n");
        LogInit();
    }

    INIT_LOGI("init_main[5-2][Stage%d]: SystemInit()\n", isSecondStage);
    SystemInit();
    INIT_LOGI("init_main[5-3][Stage%d]: SystemExecuteRcs()\n", isSecondStage);
    SystemExecuteRcs();
    INIT_LOGI("init_main[5-4][Stage%d]: SystemConfig() --> DoJob\n", isSecondStage);
    SystemConfig();
    INIT_LOGI("init_main[5-5][Stage%d]: SystemRun() --> Looping...\n", isSecondStage);
    SystemRun();
    INIT_LOGI("init_main[5-5][Stage%d]: End.\n", isSecondStage);  //never run to this step

    return 0;
}

initA即stage0,它在SystemPrepare()里面的StartInitSecondStage()的最后一步,通过执行execv(“/bin/init”, args),切换到initB即stage1去运行了,并不会跑剩余的[5-2/3/4/5]几个步骤,而是让stage1的initB来跑。initB也不跑SystemPrepare()部分,而是去跑LogInit()以及接下来的[5-2/3/4/5]几个步骤。

如流程图3所示。
OHOS 3.1的Init进程two_stages相关分析-2-实现部分-鸿蒙开发者社区
由上图可知:

  • initA就仅仅是跑SystemPrepare()而已;
  • initA’又仅仅是跑SystemPrepare()的一部分,就转去执行"/bin/updater"了;
  • initB则是跑init中除了SystemPrepare()之外的其余部分。
    从这个角度来看,就可以认为initA、initB、initA’是“三个完全不同的”可执行程序了。
    接下来我们看一下initA和initB的具体实现。

2.2 initA(stage0)的实现和流程

SystemPrepare()的前几步,initA和initA’基本相同,没啥说的,看一下代码就明白了。不同的地方是initA’不跑StartInitSecondStage(),而是转去执行"/bin/updater"跑升级流程去了,这里不展开分析。

我们关注一下StartInitSecondStage()里面的五大步骤,用【5-1/2/3/4/5】标记。
5-1】Fstab* fstab = LoadRequiredFstab()
这一步会去读取并解析“/etc/fstab.required”文件,这个文件就是编译时拷贝到//out/rk3568/目录下的那个,在制作ramdisk镜像和system镜像时,会再次拷贝到镜像的/etc/目录下被使用。
char **devices = GetRequiredDevices(*fstab, &requiredNum)会读取其中带有“required”flag的设备。
注意其中的userdata块设备,没有带“required”flag;而misc块设备,类型是none。
OHOS 3.1的Init进程two_stages相关分析-2-实现部分-鸿蒙开发者社区
5-2】StartUeventd(devices, requiredNum)
通过uevent机制去为块设备创建DeviceNode,中间过程稍微有点复杂,这里不展开分析,请感兴趣的小伙伴们自行阅读代码了解一下。
5-3】MountRequriedPartitions(fstab)
这一步会去按fstab的描述,会把system、vendor两个块设备分别挂载到ramdisk根目录下的/usr、/vendor路径下,而/userdata块设备会因为没有“required”flag而推迟到stage1才去挂载,/misc块设备会因为文件类型为“none”而挂载失败,可以先不用管。
这样,ramdisk目录结构就变成了如下的样子:
OHOS 3.1的Init进程two_stages相关分析-2-实现部分-鸿蒙开发者社区
5-4】SwitchRoot(“/usr”)
在真正 SwitchRoot 之前,我把当前路径(ramdisk)下的一级目录打印了出来,如log中下面这一小段所示:

SwitchRoot: [-]Before SwitchRoot:
.
[d]vendor/
[d]lib/
[d]etc/
[d]sys/
[d]storage/
[d]usr/
[d]mnt/
[l]init //link to ‘bin/init’,即initA
[d]system/
[d]bin/
[d]proc/
[d]root/ //空目录,暂不知道哪里生成的
[d]dev/

这基本上契合了【5-3】步骤后的ramdisk的目录结构,只是我还没找到root这个空目录是在哪里生成的。在build_image.py的_prepare_ramdisk()中,并没有在ramdisk中生成root目录(或挂载点),在SystemPrepare()的前几步中也没看到要生成root目录(或挂载点)的地方。

SwitchRoot(“/usr”)这一步,非常关键,里面做了以下一组事情,如log所示:

SwitchRoot: [0]Switch root from ramdisk’s ‘/’ to ‘/usr’ Begin:
SwitchRoot: MountToNewTarget(‘/usr’)
MountToNewTarget: [0] continue [/]: [-][is ‘/’][-]
MountToNewTarget: [1]Move mount [/vendor] to [/usr/vendor]
MountToNewTarget: [2] continue [/usr]: [-][-][mountPoint is same]
MountToNewTarget: [3] continue [/sys/fs/selinux]: already UnderBasicMountPoint
MountToNewTarget: [4]Move mount [/sys] to [/usr/sys]
MountToNewTarget: [5]Move mount [/proc] to [/usr/proc]
MountToNewTarget: [6] continue [/dev/pts]: already UnderBasicMountPoint
MountToNewTarget: [7]Move mount [/storage] to [/usr/storage]
MountToNewTarget: [8]Move mount [/mnt] to [/usr/mnt]
MountToNewTarget: [9]Move mount [/dev] to [/usr/dev]
SwitchRoot: chdir(‘/usr’)
SwitchRoot: mount(‘/usr’ to ‘/’)
SwitchRoot: chroot(‘.’)
FreeOldRoot: Failed to unlink[init], err = 20
SwitchRoot: [0]Switch root from ramdisk’s ‘/’ to ‘/usr’ End. OK

简单来说就是把stage0的“/proc/mounts”上描述的、挂载到ramdisk根目录下的各个设备节点,全部统一重新挂载到/usr/路径下对应节点上。这个/usr/就是【5-3】步骤挂载上去的system.img所描述的块设备。
需要注意的是,这里的“/proc/mounts”是stage0阶段的设备挂载信息,与系统跑完stage1之后,我们在shell上“cat /proc/mounts”所看到的信息,可能还有点不一样,这个请小伙伴们自行确认一下。
Move mount步骤之后,再通过chdir(‘/usr’)、mount(‘/usr’ to ‘/’)、chroot(‘.’)操作,把原先的ramdisk根目录替换成以/usr为根的新的目录结构。

在执行完 SwitchRoot 之后,我再次把当前路径(已经切换到usr/)下的一级目录打印出来,如下log所示:

SwitchRoot: [-]After SwitchRoot:
.
[d]storage/
[d]chip_prod/
[d]chipset/
[d]mnt/
[d]tmp/
[d]sys_prod/
[d]data/
[l]etc
[d]vendor/
[d]sys/
[d]proc/
[d]dev/
[l]bin
[l]init
[l]lib
[d]lost+found/
[d]updater/
[d]config/
[d]system/

chroot之后,系统的根目录结构,就变成了:
OHOS 3.1的Init进程two_stages相关分析-2-实现部分-鸿蒙开发者社区

5-5】execv(“/bin/init”, args)
这一步就很明朗了,args定义为:

    char * const args[] = {
        "/bin/init",
        "--second-stage",
        NULL,
    };

    printf("StartInitSecondStage[5-5]: execv('/bin/init')-->>[Stage1]\n");
    if (execv("/bin/init", args) != 0) {
        INIT_LOGE("Failed to exec \"/bin/init\", err = %d", errno);
        exit(-1);
    }

带参数去运行/bin/init,这个init就是新的root下的/bin/init,也就是前面说的initB。
stage0的initA进程到此就结束了,它的上下文环境仍然保持不变,但是从这里开始切换去运行initB,即流程图3的右边绿色部分,进入stage1。

2.3 initB(stage1)的实现和流程

这一阶段就是OHOS框架的启动入口了,请小伙伴自己阅读代码去理解一下。

3.Log确认流程

我对init进程的two_stages流程做了一下整理,把相关log打印出来,完整的log如附件所示。

4.思考与讨论

为什么要引入这么复杂的启动流程?有什么好处?

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
Init two_stages log.rar 3.08K 51次下载
已于2022-5-7 12:26:27修改
4
收藏 2
回复
举报
2条回复
按时间正序
/
按时间倒序
小强鼓掌
小强鼓掌

非常棒的分享

回复
2022-5-7 17:34:44
wx648c0452dde3a
wx648c0452dde3a

不知道为啥,按理说没必要搞ramdisk了,就算大型系统也可以直接挂真正的根目录,而没必要保持这种结构。只能说还是有Android的影响在里面。也许纯粹就是为了保持兼容吧。

回复
2023-6-22 14:18:08
回复
    相关推荐