OpenWRT启动流程剖析

一、前言

        OpenWRT是一个嵌入式的Linux发行版,所以,它的启动流程会依赖于Linux内核的启动,例如:Linux汇编启动阶段时的0号进程的创建,以及后续C程序启动阶段的1号和2号进程的创建,也就是用户空间和内核空间的祖先进程,当然,它也有和原生Linux不一样的地方,这些启动阶段的差异也就是下面我们要重点介绍的内容。

二、OpenWRT启动进程转换图

        下图是本文的核心流程图,后续内容的展开也是依赖此图,这里从整体上展示了启动阶段的进程间的转换,它是整个启动流程的框架,框架搭好后我们再来深挖下各个进程中的细节,由整体到局部的逐步深入学习。

        这里可以看到整个启动流程一共涉及到7个进程,其中4个是PID1的父进程的转换,3个是/sbin/init的子进程,需要注意的是,在这里仅展示了和原生Linux内核启动流程的差异部分,其他通用的启动流程并不在本文的讨论范围。

三、OpenWRT启动代码流程图

        下图是OpenWRT的启动代码流程图,这里也是以各个进程为核心进行数据,下图中的蓝色部分就是和启动相关的进程。

四、启动进程功能介绍

        上面展示的启动阶段进程转换图已经大体上展现了OpenWRT的启动全貌,但是这仅仅是个架子,接下来我们就来深挖下各个进程的功能,从整体到局部的梳理完成后,我们对整个OpenWRT的启动流程就会有更深入的认识和理解。

        4.1、kernel_init进程

                OpenWRT的启动是基于Linux Kernel的,所以,其启动过程会依赖Linux内核的启动,位于Linux_kernel-5.4/init/main.c中的kernel_init函数会指定内核启动的第一个用户空间进程,原生的Linux内核默认启动的第一个用户空间进程是/sbin/init (由busybox实现),但在OpenWRT中默认启动的第一个用户空间进程是/etc/preinit。

                OpenWRT中第一个用户空间进程的指定一般是在设备树中指定的,如下图所示:

        4.2、/etc/preinit进程

                /etc/preinit实际上是一个shell脚本,它位于openwrt/package/base-files/files/etc/preinit,该脚本是OpenWRT启动的关键,下图是preinit脚本实现,下面我们来一一介绍。

#!/bin/sh
# Copyright (C) 2006-2016 OpenWrt.org
# Copyright (C) 2010 Vertical Communications

[ -z "$PREINIT" ] && exec /sbin/init    # 因为PREINIT是空,所以直接执行/sbin/init进程

export PATH="%PATH%"

. /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

                首先执行第5行[ -z "$PREINIT" ] && exec /sbin/init,此时PREINIT变量并没有定义,所以会直接执行exec /sbin/init,这里会将当前进程重新替换成/sbin/init进程,这个脚本后续的内容在后续的进程中会继续执行,当前进程几乎没有干什么事。这里需要注意的是原生的Linux内核中/sbin/init进程是由busybox实现的,而在OpenWRT中是由procd实现的。

        4.3、/sbin/init进程

                紧接着进入/sbin/init进程,它位于openwrt/build_dir/target-aarch64-openwrt-linux-musl_musl/procd-default/procd-2021-03-08-2cfc26f8/initd/init.c中,代码实现如下所示:

int
main(int argc, char **argv)
{
    pid_t pid;

    ulog_open(ULOG_KMSG, LOG_DAEMON, "init");

    /* 监控和处理接收到的SIGTERM、SIGUSR1、SIGUSR2和SIGPWR信号 */
    sigaction(SIGTERM, &sa_shutdown, NULL);
    sigaction(SIGUSR1, &sa_shutdown, NULL);
    sigaction(SIGUSR2, &sa_shutdown, NULL);
    sigaction(SIGPWR, &sa_shutdown, NULL);

    if (selinux(argv))
        exit(-1);
    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: %m\n");
        exit(EXIT_FAILURE);
    }
    if (pid <= 0) {
        ERROR("Failed to start kmodloader instance: %m\n");
    } else {
        const struct timespec req = {0, 10 * 1000 * 1000};
        int i;

        for (i = 0; i < 1200; i++) {
            if (waitpid(pid, NULL, WNOHANG) > 0)
                break;
            nanosleep(&req, NULL);
            watchdog_ping();
        }
    }
    uloop_init();
    preinit();
    uloop_run();

    return 0;
}

                该进程的功能如下:

                        1)挂载一些系统用的虚拟分区,如/proc,/sysfs,/sys/fs/cgroup, /tmp, /dev, /dev/pts等 【early() -> early_mounts()】

                        2)创建设备节点和/dev/null空设备, 任何进入此文件的数据都会被删除, 一般用于删除输出的内容 【early() -> early_dev()】

                        3)初始化/dev/console【early() -> early_console()】

                        4)设置PATH的环境变量值【early() -> early_env()】

openwrt/build_dir/target-aarch64-openwrt-linux-musl_musl/procd-default/procd-2021-03-08-2cfc26f8/initd/init.h

                        5)从/proc/cmdline中解析init_debug的值来控制debug等级,默认是0 【cmdline() 】

                        6)初始化内核看门狗(/dev/watchdog), 如果存在/dev/watchdog设备,设置超时时间为30s,如果内核在30s内没有收到任何数据将重启系统;用户进程使用uloop定时器设置5s周期向/dev/watchdog设备写一些数据通知内核,表示此用户在正常工作。 【watchdog_init() 】

                        7)创建子进程/sbin/kmodloader加载启动阶段的驱动,它指定了/etc/modules-boot.d/目录,若不指定的话,则会加载/etc/modules.d/下的驱动。【fork() & execvp("/sbin/kmodloader")】

                        注意,执行fork后会复制一份当前的进程,首先会返回子进程的pid到父进程中,这里会等待子进程的返回,然后在执行子进程/sbin/kmodloader。

                        4.3.1、/sbin/kmodloader进程

                                子进程/sbin/kmodloader从/etc/modules-boot.d/ (openwrt/build_dir/target-aarch64_cortex-a55+neon-vfpv4_musl/root-gem6xxx/etc/modules-boot.d)中加载设备驱动(注意,这里加载的是boot阶段的drivers而不是/etc/modules.d/)

                        8)初始化uloop 【uloop_init()】

                        9)接下来执行preinit函数,该函数的实现如下,首先创建/sbin/procd子进程用于建立netlink通讯机制,完成内核的交互,监听uevent事件【preinit() -> fork() & execvp("/sbin/procd", "-h", "/etc/hotplug-preinit.json")】

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: %m\n");
		exit(EXIT_FAILURE);
	}
	if (plugd_proc.pid <= 0) {
		ERROR("Failed to start new plugd instance: %m\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: %m\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: %m\n");
		exit(EXIT_FAILURE);
	}
	if (preinit_proc.pid <= 0) {
		ERROR("Failed to start new preinit instance: %m\n");
		return;
	}
	uloop_process_add(&preinit_proc);

	DEBUG(4, "Launched preinit instance, pid=%d\n", (int) preinit_proc.pid);
}

                        4.3.2、/sbin/procd -h /etc/hotplug-preinit.json进程

                                子进程/sbin/procd来执行/sbin/procd -h /etc/hotplug-preinit.json,该进程用于注册热插拔事件处理函数,/etc/hotplug-preinit.json文件为接收到热插拔事件后所需要调用的脚本。

                                /sbin/init父进程使用uloop_process_add()把/sbin/procd子进程加入uloop监控,当/sbin/procd进程结束时就会调用回调函数plugd_proc_cb。

                                接着设置PREINIT == 1,并创建子进程/bin/sh用于继续执行之前未完成的/etc/preinit脚本。

                        4.3.3、/bin/sh /etc/preinit进程

                               子进程/bin/sh用于执行一开始没有执行完成的/etc/preinit脚本,此时的PREINIT已经被设置成1,则开始执行后续内容,接下来的这三个shell脚本主要定义了一些shell函数,特别是preinit.sh中定义了hook相关的函数。

                               NOTICE:.和/之间是有空格的,这里的点相当于source命令,但source是bash特有的,并不在POSIX标准中,'.'是通用的用法,使用'.'的意思是在当前shell环境下运行,并不会在子shell中运行。

                                之后使用定义在/lib/functions/preinit.sh中的boot_hook_init初始化如下hook,然后依次在当前shell下执行/lib/preinit/目录下的脚本,这里有很多脚本,OpenWRT将这些脚本分为preinit_essentialpreinit_mainfailsafeinitramfspreinit_mount_root这五类,每一类函数按照脚本的开头数字的顺序运行。

                                /lib/preinit/目录下的脚本具有类似的格式,定义要添加到hook结点的函数,然后通过boot_hook_add将该函数添加到对应的hook结点,最后,/etc/preinit就会执行boot_run_hook函数对应hook结点上的函数。

                                当前环境下只执行了preinit_essential和preinit_main结点上的函数,到此,/etc/preinit执行完毕并退出。

                                NOTICE:如果需要跟踪调试这些脚本,可以在/etc/preinit的最开始添加一条set -x命令,这样就会打印执行命令的过程,但不会真正执行。

                                /sbin/init父进程使用uloop_process_add()把/bin/sh子进程加入uloop监控,当/etc/preinit执行结束时回调函数spawn_procd()。

                                spwan_procd()将wdtfd设置到env中的WDTFD,从/tmp/debug_level读取的debug level设置到env中的DBGLVL,最后调用execvp()替换当前进程为/sbin/procd,这就是系统启动后,PID1的进程是/sbin/procd的由来。

                        10)执行uloop,由uloop接管 【uloop_run()】

        4.4、/sbin/procd进程

                该进程的实现位于openwrt/build_dir/targer-aarch64-openwrt-linux-musl_musl/procd-default/procd-2021-03-08-2cfc26f8/procd.c,这里主要是做了一些初始化工作,设置自己成为进程组的所有者,初始化uloop,设置好signals,之后进入procd_state_next函数,该函数内部调用state_enter,这里是procd的状态机,一共有6个状态,启动阶段的状态转换有4个,分别是STATE_EARLYSTATE_UBUSSTATE_INITSTATE_RUNNING

                        4.4.1、STATE_EARLY

                                o 初始化watchdog

                                o 根据/etc/hotplug.json中定义的规则来监视热插拔事件

                                o procd_coldplug()函数用于把/dev挂载到tmpfs中,fork udevtrigger进程产生冷插拔事件,以便让hotplug监听进行处理

                                o udevtrigger进程处理完成后回调procd_state_next()函数把状态从STATE_EARLY转变为STATE_INIT

                        4.4.2、STATE_UBUS

                                o 再次初始化watchdog, 防止在coldplug之前watchdog不可用

                                o 设置stdin/stdout/stderr到/dev/console

                                o procd_connect_ubus()函数定义一个定时器去连接ubusd,即使这里ubusd还没有创建好,当procd之后连上ubusd,它将注册services main_object, system_object和watch_event,然后把状态转化成STATE_INIT

                                o service_start_early()函数用于开始ubusd后台服务

                        4.4.3、STATE_INIT

                                o procd_inittab()函数读取/etc/inittab的内容,然后将其加入到actions list中,在add_action中会将inittab中sysinit、shutdown和respawnlate等操作的处理函数也加入到action链表中。

                                o 接着执行了一系列procd_inittab_run函数,根据inittab脚本定义,这里只会执行到procd_inittab_run("sysinint"),这里就会执行到上面handler数组中"sysinit"对应的处理函数runrc,这里面会调用rcS函数,紧接着继续执行_rc函数,该函数会执行/etc/rc.d/S*的启动脚本文件。

                                o 这里需要注意的是,/etc/rc.d/目录下的启动文件其实是/etc/init.d/目录下文件的软链接,该目录下的启动脚本分为两类,一类是以S开头,另一类是以K开头,S代表start,K代表stop,启动脚本的执行顺序是按照S/K后面的数字大小从小到大依次执行,如果多个init脚本具有相同的起始值,则调用顺序由init脚本名称的字母从小到大依次确定。

                                o 当我们以enable参数调用/etc/init.d/目录下的启动脚本时,系统会创建一个该脚本文件的符号链接放在/etc/rc.d/目录下,当以disable参数调用时,系统会从/etc/rc.d/目录下删除该符号链接。

                                o 到这里,我们可以知道STATE_INIT状态下的主要任务是为了执行/etc/rc.d/目录下的启动脚本文件。

                                o 最后,在rcdone中完成状态机的切换,STATE_INIT -> STATE_RUNNING

                        4.4.4、STATE_RUNNING

                                o 接下来执行respawnlateaskconsolelate,根据inittab的内容可知这里只会执行procd_inittab_run("respawnlate"),这里的目的是执行程序并在程序退出时重新执行它,进入STATE_RUNNING状态后procd运行uloop_run()主循环了,这里的分析过程和上面的sysinit类似。

                        4.4.5、STATE_SHUTDOWN

                                o reboot命令会触发procd转变为该状态,处理流程和sysinit一致,唯一的区别是入参为shutdown,处理完/etc/rc.d/K* shutdown脚本后,状态就会从STATE_SHUTDOWN转变为STATE_HALT。

                        4.4.6、STATE_HALT

                                o 这里会发送SIGTERM和SIGKILL信号到所有进程。

五、启动脚本文件介绍

        openwrt下的启动脚本风格有两类,一类是SysV风格,另一类是procd风格,下面是对应的介绍和说明。

        5.1、SysV风格的init脚本

                下面的脚本示例就是openwrt中启动脚本最简单的实现,通常,若我们需要新增启动脚本,则需要在/etc/init.d/目录下添加,从这里来看,这个脚本是很简单的,但其实在/etc/rc.common脚本文件中提供了很多默认和必要的函数。

#!/bin/sh /etc/rc.common
# Example script
# Copyright (C) 2007 OpenWrt.org

START=10
STOP=15

start() {
        echo start
        # commands to launch application
}

stop() {
        echo stop
        # commands to kill application
}

                通过这个 rc.common 模板,初始化脚本的可用命令如下,这些命令是可以在执行启动脚本时传入的。

start   Start the service
stop    Stop the service
restart Restart the service
reload  Reload configuration files (or restart if that fails)
enable  Enable service autostart
disable Disable service autostart
enabled check service wether be enabled

                这里对上述的脚本的解读:

                1)启动脚本中必需的start()和stop()函数确定启动和停止此服务所需的核心步骤

                2)START=和STOP=后面的数字大小决定了脚本执行顺序,数字范围是0-99,数字越小就优先执行,有多个相同数字的脚本,会继续根据字母的优先顺序执行,靠前的字母优先执行。

                3)创建的启动脚本需要保证有执行权限,不然启动过程中执行会报错。

                4)启动阶段,系统会从/etc/rc.d/目录下执行启动脚本,/etc/rc.d/目录下都是软链接,实际指向的文件就是/etc/init.d/目录下,若使用enable或disable命令,就可以自动创建或删除对应在/etc/rc.d/目录下的软链接。

                5)如果脚本中有实现boot()函数,则会直接执行boot()函数,若没有boot(),实际上执行的还是boot(),只不过默认的boot函数会直接调用start()函数。

                6)上面的enabled命令可以检查/etc/init.d/目录下的脚本是否是开机启动,若返回0表示开机自启动,返回1表示开机不启动,这里可以通过以下命令来打印:

for F in /etc/init.d/* ; do $F enabled && echo $F on || echo $F **disable**; done

        5.2、procd风格的init脚本

                5.2.1、基本介绍

                        在OpenWRT系统中,procd风格的init脚本用于控制服务进程,控制主要表现在两个方面:

                                1)定义服务进程的配置(例如:服务进程的命令和携带什么参数启动等)

                                2)控制服务进程是否可以重新启动

                        定义服务进程的配置是由start_service()处理,在里面指定服务进程的命令和它需要带的参数,这些都是由procd进程管理的。下面是一个procd风格的init脚本,这里有以下几点需要特别注意,

                                1)脚本中要指定PROCD方式的声明 USE_PROCD=1

                                2)脚本中必须要实现start_service()函数

                                3)procd执行的程序不能是守护进程后台程序,因为后台程序的主进程退出后在procd看来就是程序退出了,然后会进入respawn流程,之后重复启动和退出,最后失败

#!/bin/sh /etc/rc.common
# Copyright (C) 2008 OpenWrt.org    

START=98
STOP=15
 
# 声明使用procd
USE_PROCD=1
 
BINLOADER_BIN="/usr/bin/test"
 
# start_service 函数必须要定义
start_service() {
  # 创建一个实例, 在procd看来一个应用程序可以多个实例
  procd_open_instance [instance_name]
  procd_set_param respawn      # 告知procd当test程序退出后尝试进行重启
  procd_set_param command "$BINLOADER_BIN"
  procd_close_instance        # 关闭实例
}
 
# 定义退出服务器后需要做的操作
stop_service() {
  rm -f /var/run/test.pid
}
 
reload_service() {
  procd_send_signal clash
}

restart() {
  stop
  start
}

                5.2.2、procd接口介绍

                        1)procd_open_instance [Instance_name]

                                打开一个实例,每次配置服务进程之前都需要打开一个实例,配置完成后,调用procd_close_instance关闭实例。

                        2)procd_set_param

                                为服务进程设置相应的参数,指定服务进程命令,设置进程命令行参数,环境变量等,设置的参数一般有如下类型:

                                a)command

                                        指定服务进程的命令,例如:

procd_set_param command /sbin/test

                                b)respawn

                                        用于设置服务进程退出后,procd自动将其重启,有三个参数控制服务进程重启,

                                        o respawn_threshold

                                                如果服务进程的存在时间没有respawn_threhold设置的长,则认为进程崩溃,并在respawn_retry指定的次数重试后,将其停止;

                                                如果服务进程的存在时间比respawn_threhold设置的的长,则不管服务进程如何退出,它都会被procd重启。

                                        o respawn_timeout

                                                表示一个服务进程在尝试重启前需要等待多长时间。

                                        o respawn_retry

                                                表示服务进程退出后,procd尝试重启服务进程的次数,当尝试这些次数后,procd认为该服务进程已经崩溃。

procd_set_param respawn ${respawn_threhold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}

                                c)env

                                        传递环境变量到服务进程,服务进程可以通过getenv函数来获取,例如:

procd_set_param env ENV_VAUEL=111

                                d)file

                                        指定服务进程的配置文件,若指定的文件内存发生改变,手动运行该服务reload后,该服务进程将会重启。例如:

procd_set_param file /etc/config/network

                                e)stdout

                                        如果被设置为1,procd则会将服务进程的标准输出内容转发到logd进程,在syslog中输出,输出的优先级为LOG_INFO

procd_set_param stdout 1

                                f)stderr

                                        如果被设置为1,procd则会将服务进程的标准错误内容转发到logd进程,在syslog中输出,输出的优先级为LOG_ERR

procd_set_param stderr 1

                                g)pidfile

                                        procd将服务进程的pid写入到指定文件,用于让其他进程查看该服务进程是否存在。

procd_set_param pidfile /var/run/test_service.pid

                3)procd_append_param

                        为服务进程追加命令行参数,例如:

procd_set_param command syslogd
procd_append_param command -p 8

                4)procd_add_reload_trigger

                        当系统的配置发生改变时,若需要服务进程可以自动执行reload操作,那么,可以在service_trigger()函数用procd_add_reload_trigger指定哪些配置改变了要触发reload。例如,下面的例子表明若/etc/config/network文件有变化时,就会触发当前服务进程的reload。

service_trigger()
{
	procd_add_reload_trigger "network"
}

                        注意,当前服务进程start时,就会运行service_trigger函数,执行procd_add_reload_trigger "network"。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值