OHOS 3.1的Init进程two_stages相关分析-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所示。
由上图可知:
- 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。
【5-2】StartUeventd(devices, requiredNum)
通过uevent机制去为块设备创建DeviceNode,中间过程稍微有点复杂,这里不展开分析,请感兴趣的小伙伴们自行阅读代码了解一下。
【5-3】MountRequriedPartitions(fstab)
这一步会去按fstab的描述,会把system、vendor两个块设备分别挂载到ramdisk根目录下的/usr、/vendor路径下,而/userdata块设备会因为没有“required”flag而推迟到stage1才去挂载,/misc块设备会因为文件类型为“none”而挂载失败,可以先不用管。
这样,ramdisk目录结构就变成了如下的样子:
【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之后,系统的根目录结构,就变成了:
【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.思考与讨论
为什么要引入这么复杂的启动流程?有什么好处?
非常棒的分享
不知道为啥,按理说没必要搞ramdisk了,就算大型系统也可以直接挂真正的根目录,而没必要保持这种结构。只能说还是有Android的影响在里面。也许纯粹就是为了保持兼容吧。