Openwrt内核启动流程及相关脚本简易分析

本人刚开始学openwrt,鉴于网上资料太过繁杂,故自己结合资料研究源代码重新整理一下,供学习交流!
1.简介

关于 OpenWrt
openwrt是嵌入式设备上运行的linux系统。
OpenWrt 的文件系统是可写的,开发者无需在每一次修改后重新编译,
令它更像一个小型的 Linux 电脑系统,也加快了开发速度。
你会发现无论是 ARM, PowerPC 或 MIPS 的处理器,都有很好的支持。
并且附带3000左右的软件包,用户可以方便的自定义功能来制作固件。
也可以方便的移植各类功能到openwrt下。

在openwrt中用PS查看进程会发现,进程号为1的程序是procd!
2.系统启动流程分析
如图所示(此图引用于网络图片):
Openwrt内核启动流程及相关脚本简易分析
内核启动过程:【可在…/init/mian.c中查看详细过程】
uboot–>start_kernel()–>rest_init()—>kernel_thread(kernel_init)—> kernel_init_freeable()

初始化过程:
Linux 内核(kernel_init)—>/etc/preinit —>/sbin/init —>/etc/preinit、/sbin/procd—>/sbin/procd。

/etc/preinit脚本是系统其它脚本的入口,在/etc/preinit脚本中,第一条命令为:
[ -z “$PREINIT” ] && exec /sbin/init
由于一开始PREINIT没有被设置,所以为空,执行/init脚本

其中/sbin/init 由procd/init.c编译产生的(它先执行一些ulog_open、early、cmdline等函数。最后执行preinit()函数。)
init.c部分代码如下:

	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);//初始化watchdog

	pid = fork();
	if (!pid) {
		char *kmod[] = { "/sbin/kmodloader", "/etc/modules-boot.d/", NULL };

	...............................
		}
		execvp(kmod[0], kmod);//kmodloader,加载部分KO模块
		ERROR("Failed to start kmodloader\n");
		exit(-1);
	}
...........................
	}
	uloop_init();
	preinit();//调用preinit函数,下面再做分析
	uloop_run();

看启动流程,当procd退出后会调用execvp函数执行/sbin/proc,替换当前的init进程,这就是系统启动完成后,进程号为1最终为/sbin/procd的由来,中间改变了几次。

preinit()函数代码如下(有删减,具体请查看源码):

void
preinit(void)
{
	char *init[] = { "/bin/sh", "/etc/preinit", NULL };
	char *plug[] = { "/sbin/procd", "-h", "/etc/hotplug-preinit.json", NULL };

	LOG("- preinit -\n");

	plugd_proc.cb = plugd_proc_cb;
	plugd_proc.pid = fork();
	if (!plugd_proc.pid) {
		execvp(plug[0], plug);//创建进程执行procd
	}
.................................
	uloop_process_add(&plugd_proc);

	setenv("PREINIT", "1", 1);//这里配置了环境变量,第二次执行/preinit的时候就不再运行/init

	preinit_proc.cb = spawn_procd;//回调函数
	preinit_proc.pid = fork();
	if (!preinit_proc.pid) {
		execvp(init[0], init);//创建进程执行preinit
	}
..................................................
	uloop_process_add(&preinit_proc);
...............................................
}

preinit函数配置了环境变量PREINIT,然后再去fork进程来执行/preinit,执行完毕后,再调用回调函数spawn_procd,在回调函数spawn_procd中调用了execvp函数来启动/sbin/procd这个脚本,/procd最后执行/etc/init.d/目录下的文件,从而启动系统各个服务。

spawn_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];
.............................................
	unsetenv("INITRAMFS");
	unsetenv("PREINIT");
	DEBUG(2, "Exec to real procd now\n");
	if (wdt_fd)
		setenv("WDTFD", wdt_fd, 1);
	check_dbglvl();
...............................................
	}

	execvp(argv[0], argv);
}

下面开始介绍第二次开始执行 /etc/preinit的过程
Openwrt内核启动流程及相关脚本简易分析
上图截取自/preinit脚本,当PREINIT被设置好了,往下运行就会执行上面的代码。
可以看到三个脚本被启动:
. /lib/functions.sh
. /lib/functions/preinit.sh
. /lib/functions/system.sh
这几个脚本主要定义了shell函数,在preinit.sh中,定义了一些函数挂到hook上,当运行时,这些hook们会按函数加入的顺序来启动函数。如boot_hook_init()等函数,之后使用boot_hook_init定义了五个hook节点:
boot_hook_init preinit_essential
boot_hook_init preinit_main
boot_hook_init failsafe
boot_hook_init initramfs
boot_hook_init preinit_mount_root
后面就是当前shell下依次在执行/lib/preinit/目录下的脚本:

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

定义那些要添加到hook结点的函数,然后通过boot_hook_add将该函数添加到对应的hook结点。
最后,/etc/preinit就会执行boot_run_hook函数执行对应hook结点上的函数。在当前环境下只执行了preinit_essential和preinit_main结点上的函数,如下:
boot_run_hook preinit_essential
boot_run_hook preinit_main
到此,/etc/preinit执行完毕并退出。

强调!
当/etc/preinit执行完毕并退出,进程消失了,但此时已经调用了回调函数spawn_procd(),而回调函数spawn_procd()里面execvp(“procd”),所以最终procd会重新被执行,从而启动系统其它各个配置!