Openwrt内核启动流程及相关脚本简易分析
本人刚开始学openwrt,鉴于网上资料太过繁杂,故自己结合资料研究源代码重新整理一下,供学习交流!
1.简介
关于 OpenWrt
openwrt是嵌入式设备上运行的linux系统。
OpenWrt 的文件系统是可写的,开发者无需在每一次修改后重新编译,
令它更像一个小型的 Linux 电脑系统,也加快了开发速度。
你会发现无论是 ARM, PowerPC 或 MIPS 的处理器,都有很好的支持。
并且附带3000左右的软件包,用户可以方便的自定义功能来制作固件。
也可以方便的移植各类功能到openwrt下。
在openwrt中用PS查看进程会发现,进程号为1的程序是procd!
2.系统启动流程分析
如图所示(此图引用于网络图片):
内核启动过程:【可在…/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的过程
上图截取自/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会重新被执行,从而启动系统其它各个配置!