在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进程,从而进行内核态到用户态的切换,init
进程就是用户空间的第一个进程,它的进程号为1 。
init
进程路径可以通过如下方式指定:
如下2种方式是通过cmdline或者设备树获取init进程路径
如下方式是通过内核配置指定init进程路径
如下4种方式是直接运行指定的程序作为init进程(按顺序查找,如果同时存在也只会运行第一个):
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
是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
又是从哪里来的呢?
查看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
(debug log中带有init tag的log都是由init服务打印的)/bin
,/sbin
、/usr/bin
等目录下面的命令init_debug=xxx
字段获取fork
一个子进程执行/sbin/kmodloader
,即加载内核模块,并且会一直等待所有模块加载完成(即子进程结束)preinit()
uloop_run
到目前为止,我们的主角procd
还没有出场,上述过程仍然是在进行预初始化。下面再接着分析preinit()
过程:
uloop_process
功能,这是libubox
提供的一个组件,用于管理子进程。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
,这里实际上实现了init
到procd
的转换,procd
在这里就成为了pid=1
的进程。/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
脚本,然后依次运行respawn
,askconsole
,askfirst
,sysinit
对应的handler
,最后进入STATE_RUNNING
.STATE_RUNNING
阶段会依次运行respawnlate
,askconsolelate
对应的handler
uloop_run