如果在路由器的终端上输入ps命令,可以看到pid为1的进程是procd。procd就是今天的故事了。
没错,今天来学习一下OpenWrt开机之后都发生了 什么 。主要有三个主角:
- /etc/preinit,对应源码在 package/base-files/files/etc/preinit
- /sbin/procd,对应源码在 build_dir/target-mipsel_mips32_uClibc-0.9.33.2/procd-2015-10-29.1
- /sbin/init,对应源码在 build_dir/target-mipsel_mips32_uClibc-0.9.33.2/procd-2015-10-29.1/initd
当kernel初始化完成后,就会运行/etc/preinit脚步:
// build_dir/toolchain-mipsel_mips32_gcc-4.8-linaro_uClibc-0.9.33.2/linux/init/main.c if (!try_to_run_init_process("/etc/preinit") || !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;
就这样男一号登场了。我们的男一号的第一句对白(代码)是:
[ -z "$PREINIT" ] && exec /sbin/init
不用怀疑,这时候变量$PREINIT是没有设置的,所以就执行了 后面的后句,运行了/sbin/init。
接下来让我们来揭开女一号init的裙子,啊不,神秘的面纱。当我们查看init.c的main函数的时候,会看到这里先是设置了信号处理函数,然后调用了一个early()函数。这个early函数又分别调用了early_mounts()和 early_env():
early(void)
{ if (getpid() != 1) return;
early_mounts();
early_env();
LOG("Console is alive\n");
}
early_mounts()挂载了一些系统用的虚拟分区,例如/proc, /sysfs, /dev/shm等等,还顺便把标准输入输出错误等设置成/dev/console。因为init是所有进程的爹,当其他进程被创建时就会把标准输入、输出、错误这三个基本的文件描述符继承过去。
而early_env简单的设置了PATH变量。
接着往下看init.c的main函数。接下来运行了/sbin/kmodloader /etc/modules-boot.d/,加载目录下的内核驱动。
最后运行preinit()函数。preinit()函数做了两件微不足道的小事(暴力膜不可取啊):
- 运行我们的男二号procd:/sbin/procd -h /etc/hotplug-preinit.json
- 运行 /bin/sh /etc/preinit 脚步
啊,又回到我们的男一号了。不过,往回退一点, /sbin/init在运行/etc/preinit之前,设置了一个重要的环境变量PREINIT :
setenv("PREINIT", "1", 1);
如果你往前看我们男一号登场的场景,就会发现这个PREINIT变量就是我们男一号/etc/preinit的第一句对白里提到的变量。上次我们找不到他,现在有了,是1。所以脚步会跳过第一句,往下走了。/etc/preinit 脚步后面的部分我们先不讲。我们接着看男二号procd。打开procd.c的main函数,我们只关心一句话:
procd_state_next();
这就开始了procd的状态机了。procd的状态机代码在state.c中,有以下这几个状态:
enum { STATE_NONE = 0,
STATE_EARLY,
STATE_UBUS,
STATE_INIT,
STATE_RUNNING,
STATE_SHUTDOWN,
STATE_HALT, __STATE_MAX,
};
state变量用来存储当前的状态,被初始化为STATE_NONE:
static int state = STATE_NONE;
我们把重点放在STATE_INT这个状态,在state_enter函数可以看到状态的实现:
case STATE_INIT: LOG("- init -\n");
procd_inittab();
procd_inittab_run("respawn");
procd_inittab_run("askconsole");
procd_inittab_run("askfirst");
procd_inittab_run("sysinit");
procd_inittab()函数负责解析/etc/inittab配置,我们先来看一下这个文件的内容:
::sysinit:/etc/init.d/rcS S boot
::shutdown:/etc/init.d/rcS K shutdown
::askconsole:/bin/ash --login
打开procd_inittab()的源码,可以看到这个函数用正则表达式解析inittab每一行,这里我们只关注第一行sysinit的解析结果:
得到的action为sysinit,argv为{"/etc/init.d/rcS", "S", "boot"}
回到STATE_INIT状态机,最后运行了procd_inittab_run("sysinit");
再看procd_inittab_run源码:
static struct init_handler handlers[] = {
{
.name = "sysinit",
.cb = runrc,
}, {
.name = "shutdown",
.cb = runrc,
}, {
.name = "askfirst",
.cb = askfirst,
.multi = 1,
}, {
.name = "askconsole",
.cb = askconsole,
.multi = 1,
}, {
.name = "respawn",
.cb = rcrespawn,
.multi = 1,
}
}; // 中间略去 void procd_inittab_run(const char *handler) { struct init_action *a;
list_for_each_entry(a, &actions, list) if (!strcmp(a->handler->name, handler)) { if (a->handler->multi) {
a->handler->cb(a); continue;
}
a->handler->cb(a); break;
}
}
所以,这里就相当于运行了runrc,runrc再调用_rc,_rc()函数调用/etc/rc.d/下面所有文件名以S开头(就是刚才解析/etc/inittab文件得到的argv[1])的脚步,调用参数为"boot"(也就是argv[2])。注意/etc/rc.d目录下的脚步运行顺序是按照所谓的glob顺序,所以文件名S后面的两位数字就是用来规定运行先后的顺序的。
而这个目录下的脚步其实是OpenWrt的UCI机制产生的符号链接,当/etc/init.d/下的脚步被用enable参数调用时,就会在/etc/rc.d目录下创建一个对应的符号链接,从而在开机时自动运行。UCI我们后面再来学习。