OpenWrt 开机流程分析

如果在路由器的终端上输入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()函数做了两件微不足道的小事(暴力膜不可取啊):

  1. 运行我们的男二号procd:/sbin/procd  -h  /etc/hotplug-preinit.json
  2. 运行 /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我们后面再来学习。


本文章由作者:佐须之男 整理编辑,原文地址: OpenWrt 开机流程分析
本站的文章和资源来自互联网或者站长的原创,按照 CC BY -NC -SA 3.0 CN协议发布和共享,转载或引用本站文章应遵循相同协议。如果有侵犯版权的资 源请尽快联系站长,我们会在24h内删除有争议的资源。欢迎大家多多交流,期待共同学习进步。

相关推荐