【openwrt】【procd】Openwrt系统启动流程分析

发布时间:2024年01月06日

在Openwrt系统中执行ps命令可以看到 1号进程就是procd.

root@OpenWrt:/# ps -w
  PID USER       VSZ STAT COMMAND
    1 root      1856 S    /sbin/procd

但实际上内核启动完成后,运行的第一个用户进程并不是procd,在运行procd之前还执行了其他准备工作,换句话说,procd并不一开始就是“老大”,它只是最终接替了老大的位置。

下面就从内核即将启动用户空间第一个进程开始介绍Openwrt系统的启动流程。

kernel_init

在kernel启动的尾声,内核会去查找并调用 用户空间的init进程,从而进行内核态到用户态的切换,init进程就是用户空间的第一个进程,它的进程号为1 。
init进程路径可以通过如下方式指定:

如下2种方式是通过cmdline或者设备树获取init进程路径

  • ramdisk_execute_command
  • execute_command

如下方式是通过内核配置指定init进程路径

  • CONFIG_DEFAULT_INIT

如下4种方式是直接运行指定的程序作为init进程(按顺序查找,如果同时存在也只会运行第一个):

  • /sbin/init
  • /etc/init
  • /bin/init
  • /bin/sh
static int __ref kernel_init(void *unused) 
{
	/*
	  省略部分初始化
	*/
	if (ramdisk_execute_command) {
		ret = run_init_process(ramdisk_execute_command);
		if (!ret)
			return 0;
		pr_err("Failed to execute %s (error %d)\n",
		       ramdisk_execute_command, ret);
	}
	
	if (execute_command) {
		ret = run_init_process(execute_command);
		if (!ret)
			return 0;
		panic("Requested init %s failed (error %d).",
		      execute_command, ret);
	}
	
	if (CONFIG_DEFAULT_INIT[0] != '\0') {
		ret = run_init_process(CONFIG_DEFAULT_INIT);
		if (ret)
			pr_err("Default init %s failed (error %d)\n",
			       CONFIG_DEFAULT_INIT, ret);
		else
			return 0;
	}

	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;
}

static int run_init_process(const char *init_filename) {
	argv_init[0] = init_filename;
	pr_info("Run %s as init process\n", init_filename);
	return do_execve(getname_kernel(init_filename),
		(const char __user *const __user *)argv_init,
		(const char __user *const __user *)envp_init);
}

static int try_to_run_init_process(const char *init_filename) {
	ret = run_init_process(init_filename);
}

我目前使用的平台就是使用execute_command方式指定init进程的。

$ cat /proc/cmdline
console=ttyserial0,115200,n8  init=/etc/preinit

启动log中也有相关打印:

[1:swapper/0][name:main&]Run /etc/preinit as init process

接下来就开始分析/etc/preinit

/etc/preinit

etc/preinit是openwrt openwrt/package/base-files/files目录下的一个shell 脚本,其内容如下:

#!/bin/sh
[ -z "$PREINIT" ] && exec /sbin/init

export PATH="%PATH%"

. /lib/functions.sh
. /lib/functions/preinit.sh
. /lib/functions/system.sh

boot_hook_init preinit_essential
boot_hook_init preinit_main
boot_hook_init failsafe
boot_hook_init initramfs
boot_hook_init preinit_mount_root

for pi_source_file in /lib/preinit/*; do
	. $pi_source_file
done

boot_run_hook preinit_essential

pi_mount_skip_next=false
pi_jffs2_mount_success=false
pi_failsafe_net_message=false

boot_run_hook preinit_main

当前我们只需要关注第一行

[ -z "$PREINIT" ] && exec /sbin/init

这行意思很明显,如果"$PREINIT"为空,就执行/sbin/init.
很显然,这时候$PREINIT是没有任何人给它赋值,因此它的值是NULL,所以接下来就会执行/sbin/init

/sbin/init

那么这个/sbin/init又是从哪里来的呢?
查看procd的packge makefile(openwrt/package/system/procd/Makefile)

define Package/procd/install
	$(INSTALL_DIR) $(1)/sbin $(1)/etc $(1)/lib/functions
  
  # /sbin/init 来自下面这一句
	$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/{init,procd,askfirst,udevtrigger,upgraded} $(1)/sbin/
	$(INSTALL_DATA) $(PKG_INSTALL_DIR)/usr/lib/libsetlbf.so $(1)/lib
	$(INSTALL_BIN) ./files/reload_config $(1)/sbin/
	$(INSTALL_CONF) ./files/hotplug*.json $(1)/etc/
	$(INSTALL_DATA) ./files/procd.sh $(1)/lib/functions/
endef

init,procd,askfirst,udevtrigger,upgraded这些命令都属于procd组件,最终都会被安装到/sbin.
/sbin/init执行流程如下:

在这里插入图片描述

  • init服务首先初始化ulog,设置其log的TAG为init(debug log中带有init tag的log都是由init服务打印的)
  • 然后挂载一些必要的文件系统,例如proc、sysfs、tmpfs等,并设置初始环境变量,以便接下来可以运行/bin,/sbin/usr/bin等目录下面的命令
  • 接下来获取并设置默认log打印等级,从cmdline中的init_debug=xxx字段获取
  • 随后初始化并启动看门狗,看门狗超时时间为30s
  • 接下来会fork一个子进程执行/sbin/kmodloader,即加载内核模块,并且会一直等待所有模块加载完成(即子进程结束)
  • 接着调用preinit()
  • 最后进入uloop_run

到目前为止,我们的主角procd还没有出场,上述过程仍然是在进行预初始化。下面再接着分析preinit()过程:

在这里插入图片描述

  • preinit()使用了uloop_process功能,这是libubox提供的一个组件,用于管理子进程。
  • preinit()一共fork了2个子进程,一个运行/sbin/procd -h /etc/hotplug-preinit.json ,另一个执行/etc/preinit脚本,这些子进程在执行完毕后,都会调用对应的uloop_process.cb函数。
  • /sbin/procd -h /etc/hotplug-preinit.json 的作用是监听内核uevent事件,并根据不同事件做出相应的处理(例如创建/dev/null设备节点)
  • plugd_proc.cb函数执行的内容很简单,就是设置plugd_proc.pid=0
  • 在本文的一开始就提到内核运行的第一个启动的进程是/etc/preinit,这里又一次运行了这个脚本,只不过目前PREINIT=1,所以并不会再次执行/sbin/init,这里会执行/etc/preinit脚本后面的内容。
  • /etc/preinit执行完成后,会调用preinit_proc.cb函数,这个函数里非常重要的一个步骤就是execvp(/sbin/procd),execvp函数会将当前进程的可执行文件替换成/sbin/procd并执行/sbin/procd,这里实际上实现了initprocd的转换,procd在这里就成为了pid=1的进程。

/sbin/procd

/sbin/procd的执行流程如下:

在这里插入图片描述

  • procd执行过程中,最为关键的就是procd_state_next(),它会进行状态的流转,初始状态是STATE_EARLY
    • STATE_EARLY阶段调用hotplug("/etc/hotplug.json")监听内核uevent事件,然后调用procd_coldplug()函数执行udevtrigger命令触发uevent事件,这一步完成后创建/dev/xxx设备节点,然后进入STATE_UBUS
    • STATE_UBUS阶段主要做2件事:1.启动ubusd 2.连接ubusd,最后进入状态STATE_INIT
    • STATE_INIT阶段首先解析/etc/inittab脚本,然后依次运行respawnaskconsoleaskfirstsysinit对应的handler,最后进入STATE_RUNNING.
    • STATE_RUNNING阶段会依次运行respawnlate,askconsolelate对应的handler
    • 最终程序进入uloop_run

参考

文章来源:https://blog.csdn.net/qq_24835087/article/details/127820396
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。