OpenWrt 根文件系统启动过程分析(一)
内核启动加载根文件系统后,执行的第一个脚本是init,具体参见内核源码kernel/init/main.c
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/init.txt for guidance.");
}
依次尝试执行/sbin/init
,/etc/init
,/bin/init
,/bin/sh
, openwrt 的根文件系统里面是使用的/sbin/init
,它是一个可执行程序,来自procd源码。查看procd的CMakeLists,判断函数入口在init.c中,
ADD_EXECUTABLE(init initd/init.c initd/early.c initd/preinit.c initd/mkdev.c watchdog.c
utils/utils.c ${SOURCES_ZRAM})
procd源码会编译出好几个执行文件,包括:prcod、init、udevtrigger、askfirst等,还有根据配置打开的ujail、utrace。
preinit阶段
init程序里面:
int
main(int argc, char **argv)
{
pid_t pid;
ulog_open(ULOG_KMSG, LOG_DAEMON, "init");
sigaction(SIGTERM, &sa_shutdown, NULL);
sigaction(SIGUSR1, &sa_shutdown, NULL);
sigaction(SIGUSR2, &sa_shutdown, NULL);
early();
cmdline();
watchdog_init(1);
pid = fork();
if (!pid) {
char *kmod[] = { "/sbin/kmodloader", "/etc/modules-boot.d/", NULL };
if (debug < 3)
patch_stdio("/dev/null");
execvp(kmod[0], kmod);
ERROR("Failed to start kmodloader\n");
exit(-1);
}
if (pid <= 0) {
ERROR("Failed to start kmodloader instance\n");
} else {
int i;
for (i = 0; i < 1200; i++) {
if (waitpid(pid, NULL, WNOHANG) > 0)
break;
usleep(10 * 1000);
watchdog_ping();
}
}
uloop_init();
preinit();
uloop_run();
return 0;
}
主要是:
early();
--->early_mounts(); //挂载proc、sysfs、tmpfs等这些
--->early_env(); //设置PATH环境变量
watchdog_init(1); //设置看门狗
cmdline(); //处理/proc/cmdline的内容,主要读取bootloader设置的bootargs,看init_debug的值,设置调试等级。
调用/sbin/kmodloader,按照/etc/modules-boot.d/的配置加载模块
preinit(); //详见下面分析
init的最后阶段调用了preinit()函数,
void
preinit(void)
{
char *init[] = { "/bin/sh", "/etc/preinit", NULL };
char *plug[] = { "/sbin/procd", "-h", "/etc/hotplug-preinit.json", NULL };
int fd;
LOG("- preinit -\n");
plugd_proc.cb = plugd_proc_cb;
plugd_proc.pid = fork();
if (!plugd_proc.pid) {
execvp(plug[0], plug);
ERROR("Failed to start plugd\n");
exit(-1);
}
if (plugd_proc.pid <= 0) {
ERROR("Failed to start new plugd instance\n");
return;
}
uloop_process_add(&plugd_proc);
setenv("PREINIT", "1", 1);
fd = creat("/tmp/.preinit", 0600);
if (fd < 0)
ERROR("Failed to create sentinel file\n");
else
close(fd);
preinit_proc.cb = spawn_procd;
preinit_proc.pid = fork();
if (!preinit_proc.pid) {
execvp(init[0], init);
ERROR("Failed to start preinit\n");
exit(-1);
}
if (preinit_proc.pid <= 0) {
ERROR("Failed to start new preinit instance\n");
return;
}
uloop_process_add(&preinit_proc);
DEBUG(4, "Launched preinit instance, pid=%d\n", (int) preinit_proc.pid);
}
在preinit里面主要干了这些事情:
execvp(plug[0], plug); //执行`/sbin/procd -h /etc/hotplug-preinit.json`
setenv("PREINIT", "1", 1); //设置环境变量PREINIT为1
fd = creat("/tmp/.preinit", 0600); //创建/tmp/.preinit标识文件
execvp(init[0], init); //父进程执行/etc/preinit
uloop_process_add(&preinit_proc); //注册一个回调,等上面执行/etc/preinit的父进程退出,执行spawn_procd
uloop_process_add
-
注册新进程到事件处理循环
int uloop_process_add(struct uloop_process *p)
将进程事件插入到进程事件链表中,链表根据PID从小到大排序。
其中p->proc.pid为注册到uloop监控的进程ID。
P->cb为进程退出的回调函数,类型为:
typedef void (*uloop_process_handler)(struct uloop_process *c, int ret)
/sbin/procd -h /etc/hotplug-preinit.json
preinit阶段的hotplug规则。
脚本/etc/preinit:
#!/bin/sh
# Copyright (C) 2006-2016 OpenWrt.org
# Copyright (C) 2010 Vertical Communications
[ -z "$PREINIT" ] && exec /sbin/init
export PATH="/usr/sbin:/usr/bin:/sbin:/bin"
. /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
执行过程中首先需要了解的是boot_hook_init、boot_run_hook、boot_add_hook这几个函数,他们定义在/lib/functions/preinit.sh
中,不深入分析这几个函数的实现,可以认为是:
boot_hook_init list_name //初始化一个名字为list_name的回调列表
boot_add_hook list_name cb_name //向名字为list_name的回调列表中添加一个回调函数cb_name
boot_run_hook list_name //调用回调列表list_name里面添加的回调函数
这样做是方便按照一定的功能分类和调用顺序去执行一系列的函数。具体到/etc/preinit
里面就是:
//初始化一系列的回调列表
boot_hook_init XXX
//从/lib/preinit/目录下按照名字顺序添加回调
for pi_source_file in /lib/preinit/*; do
. $pi_source_file
done
//执行从/lib/preinit/下面脚本添加的回调函数
boot_run_hook preinit_essential //<==我的lede17.01这个是空的,未添加任何cb
boot_run_hook preinit_main
spawn_procd
主要是清理preinit
阶段的一些产物,最后执行/sbin/procd
static void
spawn_procd(struct uloop_process *proc, int ret)
{
char *wdt_fd = watchdog_fd();
char *argv[] = { "/sbin/procd", NULL};
struct stat s;
char dbg[2];
if (plugd_proc.pid > 0)
kill(plugd_proc.pid, SIGKILL);
if (!stat("/tmp/sysupgrade", &s))
while (true)
sleep(1);
unsetenv("INITRAMFS");
unsetenv("PREINIT");
unlink("/tmp/.preinit");
DEBUG(2, "Exec to real procd now\n");
if (wdt_fd)
setenv("WDTFD", wdt_fd, 1);
check_dbglvl();
if (debug > 0) {
snprintf(dbg, 2, "%d", debug);
setenv("DBGLVL", dbg, 1);
}
execvp(argv[0], argv);
}