OpenHarmony-1.启动流程
这里将init进程的代码分成了通用的和特有的两部分,共同的代码均在 /base/startup/init/services/init/文件夹下,其中有lite/和standard/分别用来构建小型系统和标准系统的init进程。这里主要分析标准进程的启动流程。由于OH标准系统是基于kernel内核开发的,所以启动init进程,那么OH的init进程的入口为/base/startup/init/ser
- OpenHarmony 4.0 启动流程
1.OpenHarmony 4.0 标准系统启动引导流程
OpenHarmony标准系统默认支持以下几个镜像:
每个开发板都需要在存储器上划分好分区来存放上述镜像,SOC启动时都由bootloader来加载这些镜像,具体过程包括以下几个大的步骤:
- bootloader 初始化ROM和RAM等硬件,加载分区表信息。
- bootloader 根据分区表加载boot.img,从中解析并加载ramdisk.img到内存中。
- bootloader 准备好分区表信息,ramdisk地址等信息,进入内核,内核加载ramdisk并执行init。
- init 准备初始文件系统,挂载required.fstab(包括system.img和vendor.img的挂载)。
- 扫描 system.img和vendor.img中etc/init目录下的启动配置脚本,执行各个启动命令。
1.1.u-boot
u-boot启动进入内核时,通过bootargs传递关键信息给内核,这一部分内容是与平台相关的,主要信息如下:
1.2.kernel 启动
流程图如下所示:
OpenHarmony(简称OH)的标准系统的底层系统是linux,所以调用如下代码:
linux-5.10/init/main.c:
noinline void __ref rest_init(void)
{
struct task_struct *tsk;
int pid;
rcu_scheduler_starting();
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
...
}
static int __ref kernel_init(void *unused)
{
...
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/admin-guide/init.rst for guidance.");
}
由于OH标准系统是基于kernel内核开发的,所以启动init进程,那么OH的init进程的入口为base/startup/init/services/init/main.c中。
1.3.init 进程
-
基础环境初始化:init 进程挂载 tmpfs 和 procfs,创建基本的 dev 设备节点,提供一个基本的根文件系统。
- tmpfs:这是一个内存上的文件系统,用于存储临时数据,比如在启动期间创建的目录、缓存等。它不会持久化到磁盘,当系统重启时会自动清除。
- procfs:这个文件系统提供内核运行时信息的接口,如进程列表、系统配置、硬件状态等。init进程通常会挂载procfs,以便在启动早期获取和管理这些信息。
-
从/proc/cmdline中读取fstab分区表。
-
热插拔事件监听:init 进程启动 ueventd 来监控内核中的设备热插拔事件,为新插入的 block 设备分区(如 system和 vendor 分区)创建相应的 dev 设备节点。当设备被插入或移除时,内核会通过 uevent(用户空间事件)机制发送消息给 ueventd。ueventd作为系统服务的一部分,负责监听这些netlink事件,并根据接收到的事件类型动态管理相应的设备节点。
-
服务启动:挂载完分区后,init 会扫描 /system/etc/init 目录下的 init.cfg(base/startup/init/services/etc/init.cfg) 文件,根据配置启动各个系统服务,包括 SA(Service Ability,能力服务)。
1.3.1.代码分析
BUILD.gn用于编译构建模块:
base/startup/init/services/init/standard/BUILD.gn:
15 init_common_sources = [
16 "../init_capability.c",
17 "../init_common_cmds.c",
18 "../init_common_service.c",
19 "../init_config.c",
20 "../init_group_manager.c",
21 "../init_service_file.c",
22 "../init_service_manager.c",
23 "../init_service_socket.c",
24 "../main.c",
25 ]
33 ohos_executable("init") {
34 sources = [
35 "../adapter/init_adapter.c",
36 "../standard/device.c",
37 "../standard/fd_holder_service.c",
38 "../standard/init.c",
39 "../standard/init_cmdexecutor.c",
40 "../standard/init_cmds.c",
41 "../standard/init_control_fd_service.c",
42 "../standard/init_jobs.c",
43 "../standard/init_mount.c",
44 "../standard/init_reboot.c",
45 "../standard/init_service.c",
46 "../standard/init_signal_handler.c",
47 "../standard/switch_root.c",
48 ]
49
50 modulemgr_sources = [
51 "//base/startup/init/interfaces/innerkits/hookmgr/hookmgr.c",
52 "//base/startup/init/interfaces/innerkits/modulemgr/modulemgr.c",
53 ]
54 sources += modulemgr_sources
从BUILD.gn看到OH标准系统的init进程的入口就是init_common_sources的main.c。
base/startup/init/services/init/main.c:
#include <signal.h>
#include "init.h"
#include "init_log.h"
static const pid_t INIT_PROCESS_PID = 1;
int main(int argc, char * const argv[])
{
int isSecondStage = 0;
(void)signal(SIGPIPE, SIG_IGN);
// Number of command line parameters is 2
//从kernel启动的init进程并未携带任何参数,这里是init的第一阶段
if (argc == 2 && (strcmp(argv[1], "--second-stage") == 0)) {
isSecondStage = 1;
}
if (getpid() != INIT_PROCESS_PID) {
INIT_LOGE("Process id error %d!", getpid());
return 0;
}
EnableInitLog(INIT_INFO);
//第一次这里走的是SystemPrepare
if (isSecondStage == 0) {
SystemPrepare();
} else {
LogInit();
}
SystemInit();
//启动rcs进程
SystemExecuteRcs();
SystemConfig();
SystemRun();
return 0;
}
这里将init进程的代码分成了通用的和特有的两部分,共同的代码均在 /base/startup/init/services/init/文件夹下,其中有lite/和standard/分别用来构建小型系统和标准系统的init进程。这里主要分析标准进程的启动流程。
- SystemPrepare 主要工作:
挂载一些基本目录,比如/dev,/mnt,/storage,/dev/pts,/proc,/sys,/sys/fs/selinux,接着创建一些设备节点比如/dev/null,/dev/random,/dev/urandom 等,检查系统是否处于升级模式,如果不处于升级模式就启动init的第二阶段。
由于从kernel进程启动的init进程并未传递任何参数,所以会先执行SystemPrepare:
base/startup/init/services/init/standard/init.c:
225 void SystemPrepare(void)
226 {
227 MountBasicFs(); //挂载一些基本目录并创建一些设备节点
228 CreateDeviceNode();
229 LogInit();
230 // Make sure init log always output to /dev/kmsg.
231 EnableDevKmsg();
232 INIT_LOGI("Start init first stage.");
233 HookMgrExecute(GetBootStageHookMgr(), INIT_FIRST_STAGE, NULL, NULL);
234 // Only ohos normal system support
235 // two stages of init.
236 // If we are in updater mode, only one stage of init.
237 if (InUpdaterMode() == 0) { //检查是否处于升级模式,如果没有处于升级模式,就进入init第二阶段
238 StartInitSecondStage();
239 }
240 }
- StartInitSecondStage
base/startup/init/services/init/standard/init.c
static void StartInitSecondStage(void)
{
int requiredNum = 0;
//从/proc/cmdline中读取fstab分区表
Fstab *fstab = LoadRequiredFstab();
char **devices = (fstab != NULL) ? GetRequiredDevices(*fstab, &requiredNum) : NULL;
if (devices != NULL && requiredNum > 0) {
//启动Ueventd进程
int ret = StartUeventd(devices, requiredNum);
if (ret == 0) {
//挂载分区
ret = MountRequriedPartitions(fstab);
}
FreeStringVector(devices, requiredNum);
devices = NULL;
ReleaseFstab(fstab);
fstab = NULL;
// It will panic if close stdio before execv("/bin/sh", NULL)
CloseStdio();
//启动init进程的第二阶段
INIT_LOGI("Start init second stage.");
SwitchRoot("/usr");
// Execute init second stage
char * const args[] = {
"/bin/init",
"--second-stage",
NULL,
};
//启动init进程并传递参数--second-stage
if (execv("/bin/init", args) != 0) {
INIT_LOGE("Failed to exec \"/bin/init\", err = %d", errno);
exit(-1);
}
}
- StartInitSecondStage 主要负责:
读取分区表并挂载同时启动Ueventd进程接着再次调用init并传递–second-stage参数。这里的执行execv函数将不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。所以接下来的都是init第二阶段的执行过程。再次启动init进程后,当然还是走到了/base/startup/init/services/init/main.c不过不同的是由于携带了–second-stage参数,所以会走到LogInit。接着串行执行SystemInit, SystemExecuteRcs,SystemConfig和SystemRun。
1.3.2.init挂载required分区
required分区就是系统启动引导过程的必要分区,必须在二级启动开始前进行挂载。比如system、vendor等必选镜像,挂载这些镜像前,需要先创建对应的块设备文件。这些块设备文件是通过内核上报UEVENT事件来创建的。init需要知道存储器的主设备目录,需要bootloader通过default_boot_device传递。
目前init支持两种方式获取required分区信息,一是通过保存在/proc/cmdline中的bootargs,init会首先尝试从cmdline读取required分区信息;二是通过读取ramdisk中的/system/etc/fstab.required文件,只有在前一种方式获取失败的情况下才会尝试通过这种方式获取。
-
块设备的创建逻辑
-
准备工作
- init从cmdline中读取required fstab,若获取失败,则尝试读fstab.required文件,从中获取必须挂载的块设备的PARTNAME,例如system和vendor.
- 创建接收内核上报uevent事件广播消息的socket,从/proc/cmdline里读取default_boot_device。
- 带着fstab信息和socket句柄遍历/sys/devices目录,准备开始触发内核上报uevent事件。
-
触发事件
- 通过ueventd触发内核上报uevent事件。
- 匹配uevent事件中的partitionName与required fstab中的device信息。
- 匹配成功后将会进一步处理,格式化设备节点路径,准备开始创建设备节点。
-
创建节点
- 为了便于用户态下对设备节点的访问以及提高设备节点的可读性,会对即将创建的required块设备节点同时创建软链接,这就需要先格式化软链接的路径。
- 以上工作都完成后,将执行最后的创建设备节点的步骤,根据传入的uevent中的主次设备号、前置步骤中构建的设备节点路径和软链接路径等创建设备节点,并创建相应软链接。
-
至此,块设备节点创建完毕。
- 与default_boot_device匹配关系
内核将bootargs信息写入/proc/cmdline,其中就包含了default_boot_device,这个值是内核当中约定好的系统启动必要的主设备目录。以ohos.required_mount.为前缀的内容则是系统启动必要的分区挂载信息,其内容与fstab.required文件内容应当是一致的。另外,分区挂载信息中的块设备节点就是default_boot_device目录中by-name下软链接指向的设备节点。例如,default_boot_device的值为soc/10100000.himci.eMMC,那么ohos.required_mount.system的值就包含了/dev/block/platform/soc/10100000.himci.eMMC/by-name/system这个指向system设备节点的软链接路径。
在创建块设备节点的过程中,会有一个将设备路径与default_boot_device的值匹配的操作,匹配成功后,会在/dev/block/by-name目录下创建指向真实块设备节点的软链接,以此在访问设备节点的过程中实现芯片平台无关化。
arch/arm64/boot/dts/rockchip/rk3568-linux.dtsi:
bootargs = "earlycon=uart8250,mmio32,0xfe660000 console=ttyFIQ0 ohos.boot.eng_mode=on root=PARTUUID=614e0000-0000 hardware=rk3568 default_boot_device=fe310000.sdhci rw rootwait ohos.required_mount.system=/dev/block/platform/fe310000.sdhci/by-name/system@/usr@ext4@ro,barrier=1@wait,required ohos.required_mount.vendor=/dev/block/platform/fe310000.sdhci/by-name/vendor@/vendor@ext4@ro,barrier=1@wait,required ohos.required_mount.misc=/dev/block/platform/fe310000.sdhci/by-name/misc@none@none@none@wait,required ohos.required_mount.bootctrl=/dev/block/platform/fe310000.sdhci/by-name/bootctrl@none@none@none@wait,required";
实例分析:
以OpenHarmony系统在rk3568 平台启动过程中必要的system分区为例,详细介绍init进程启动后,从读取required fstab信息到创建required分区块设备节点再到最后完成required分区挂载的全部流程。
- 获取required设备信息:
base/startup/init/services/init/standard/init_mount.c:
Fstab* LoadRequiredFstab(void)
{
Fstab *fstab = NULL;
fstab = LoadFstabFromCommandLine();
if (fstab == NULL) {
INIT_LOGI("Cannot load fstab from command line, try read from fstab.required");
const char *fstabFile = "/etc/fstab.required";
if (access(fstabFile, F_OK) != 0) {
fstabFile = "/system/etc/fstab.required";
}
INIT_ERROR_CHECK(access(fstabFile, F_OK) == 0, abort(), "Failed get fstab.required");
fstab = ReadFstabFromFile(fstabFile, false);
}
return fstab;
}
Fstab* LoadFstabFromCommandLine(void)
-> char *cmdline = ReadFileData(BOOT_CMD_LINE);
#define BOOT_CMD_LINE STARTUP_INIT_UT_PATH"/proc/cmdline"
获取fstab信息两种方式,首先调用LoadFstabFromCommandLine(),从cmdline中获取fstab信息,如果获取失败,则输出log,表示继续尝试从fstab.required文件中获取fstab信息。对于system分区来说,其读到devices中的关键信息如下所示:
/dev/block/platform/fe310000.sdhci/by-name/system
- 创建socket,触发内核上报uevent事件
base/startup/init/services/init/standard/init.c:
164 static int StartUeventd(char **requiredDevices, int num)
165 {
166 INIT_ERROR_CHECK(requiredDevices != NULL && num > 0, return -1, "Failed parameters");
167 int ueventSockFd = UeventdSocketInit();
168 if (ueventSockFd < 0) {
169 INIT_LOGE("Failed to create uevent socket");
170 return -1;
171 }
172 RetriggerUevent(ueventSockFd, requiredDevices, num); //
173 close(ueventSockFd);
174 return 0;
175 }
base/startup/init/ueventd/ueventd.c:
375 void RetriggerUevent(int sockFd, char **devices, int num)
376 {
377 int ret = GetParameterFromCmdLine("default_boot_device", bootDevice, CMDLINE_VALUE_LEN_MAX);
379 Trigger("/sys/block", sockFd, devices, num);
380 Trigger("/sys/class", sockFd, devices, num);
381 Trigger("/sys/devices", sockFd, devices, num);
382 }
GetParameterFromCmdLine:
取得default_boot_device的值应该是default_boot_device=fe310000.sdhci,也就对应了system分区设备所在目录,这一值存放在了bootDevice这个全局变量当中,将在后续创建system分区设备软链接前进行匹配。
处理required设备uevent事件:
如上所示:
base/startup/init/ueventd/ueventd.c:
379 Trigger("/sys/block", sockFd, devices, num);
380 Trigger("/sys/class", sockFd, devices, num);
381 Trigger("/sys/devices", sockFd, devices, num);
存在devices中的设备信息,就是在此处与内核上报的uevent事件进行匹配的。对于system分区设备的uevent消息,其uevent->partitionName值应该为system,与devices中存在的/dev/block/platform/fe310000.sdhci/by-name/system字段匹配成功,则开始处理system分区设备的uevent消息。
- 挂载required分区
设备节点创建完成后,即可挂载对应分区,主要接口如下:
base/startup/init/services/init/standard/init_mount.c:
26 int MountRequriedPartitions(const Fstab *fstab)
27 {
29 int rc;
30 INIT_LOGI("Mount required partitions");
31 rc = MountAllWithFstab(fstab, 1);
32 return rc;
33 }
因此,当看到"Mount required partitions"打印的时候,表示required分区设备已经准备完成,即将执行挂载动作。
- init执行system和vendor中的启动脚本,挂载vendor中更多的分区
挂载完必要的分区后,init扫描各个脚本文件。vendor中与芯片或开发板相关的初始化脚本入口如/vendor/etc/init.{ohos.boot.hardware}.cfg。vendor中扩展的挂载分区文件是/vendor/etc/fstab.{ohos.boot.hardware}。hardware的来源是bootloader传递给内核的bootargs。
1.3.3.服务启动
1.3.3.1.配置文件
Init配置文件基于JSON格式,用来配置系统启动时必要的命令和服务。Init在系统启动时解析配置文件,并根据配置文件执行对应的命令,启动相应的服务。
- 分组配置文件(device.xxxx.group.cfg)(标准系统支持)
文件由jobs、services和groups组成。用来限制能够执行的jobs和service。根据cmdline中的bootgroup属性决定当前的分区。当前支持下列分组:
base/startup/init/services/etc/device.boot.group.cfg:
device.boot.group 系统默认配置,触发执行配置文件中的所有的job和服务。
base/powermgr/battery_manager/charger/resources/device.charge.group.cfg:
device.charge.group charge模式,限制只启动改文件中允许的job和服务。
-
启动配置文件(init.cfg),文件由jobs、services和import组成。
- services(linux内核支持), 用于配置系统支持的native服务,服务具体配置参考服务管理。
- jobs, 配置等待执行命令集合,jobs具体参考 jobs管理。
- import(linux内核支持),import是导入cfg文件,目的是减少cfg大小,分离不同的功能。
1.3.3.2. init启动引导组件
- 每个系统服务启动时都需要编写各自的启动脚本文件init.cfg,定义各自的服务名、可执行文件路径、权限和其他信息。
- 每个系统服务各自安装其启动脚本到/system/etc/init目录下,init进程统一扫码执行。
- 当需要添加配置文件时,用户可以根据需要定义自己的配置文件,并拷贝到相应的目录下。
init进程启动时,首先完成系统初始化工作,然后开始解析配置文件。系统在解析配置文件时,会将配置文件分成三类:
- init.cfg(base/startup/init/services/etc/init.cfg)默认配置文件,由init系统定义,优先解析。
- /system/etc/init/*.cfg各子系统定义的配置文件。
- /vendor/etc/init/*.cfg厂商定义的配置文件。
init.cfg配置文件:
在init.cfg加载"/etc/init.usb.cfg", “/etc/init.usb.configfs.cfg”, /vendor/etc/init.${ohos.boot.hardware}.cfg"这几个config,会把它们的信息全部整合进来。
2 "import" : [
3 "/etc/init.usb.cfg",
4 "/etc/init.usb.configfs.cfg",
5 "/vendor/etc/init.${ohos.boot.hardware}.cfg"
6 ],
init.cfg主要负责:
执行job,如果开发者的进程在启动之前需要首先执行一些操作(例如创建文件夹),可以把操作放到pre-init中先执行。一般pre-init阶段主要是为后面启动服务做准备的,比如挂载目录,设置权限,启动uevent、watchdog等,uevent主要是有些服务需要响应插拔事件才会被拉起。
-
pre-init
最先执行的job,如果开发者的进程在启动之前需要首先执行一些操作(例如创建文件夹),可以把操作放到pre-init中先执行。 -
init
中间执行的job,例如服务启动。 -
post-init
最后被执行的job,如果开发者的进程在启动完成之后需要有一些处理(如驱动初始化后再挂载设备),可以把这类操作放到该job执行。单个job最多支持30条命令(当前仅支持start/mkdir/chmod/chown/mount/loadcfg),命令名称和后面的参数(参数长度≤128字节)之间有且只能有一个空格。
vendor/etc/init.rk3568.cfg:
主要是跟硬件产品相关的配置,这里rk3568, 可见主要是挂载了debugfs(调试子系统),修改设备角色为 peripheral,即设备。
1 {
2 "import" : [
3 "init.${ohos.boot.hardware}.usb.cfg"
4 ],
5 "jobs" : [{
6 "name" : "pre-init",
7 "cmds" : [
8 "write /proc/sys/vm/min_free_kbytes 10240",
9 "mount debugfs /sys/kernel/debug /sys/kernel/debug mode=755",
10 "write /sys/kernel/debug/hisi_inno_phy/role peripheral"
11 ]
12 }, {
13 "name" : "init",
14 "cmds" : [
15 "write /proc/1/oom_score_adj -1000",
16 "write /proc/sys/kernel/hung_task_timeout_secs 90",
17 "write /sys/kernel/hungtask/enable on",
18 "write /sys/kernel/hungtask/monitorlist whitelist,init,appspawn",
19 "chown system system /sys/kernel/hungtask/userlist",
20 "symlink /dev/block/platform/fe310000.sdhci/by-name /dev/block/by-name"
21 ]
22 }, {
refer to
- https://huaweicloud.csdn.net/64df3b15dc60580edc7735f4.html
- https://forums.openharmony.cn/forum.php?mod=viewthread&tid=3460
- https://blog.csdn.net/isoftstone_HOS/article/details/126864246
- https://blog.csdn.net/isoftstone_HOS/article/details/127420747
- https://blog.csdn.net/asd95279527p/article/details/143349259
- https://gitee.com/openharmony/docs/blob/master/zh-cn/device-dev/subsystems/subsys-boot-init-cfg.md
更多推荐
所有评论(0)