Openwrt学习笔记(四)——系统开机启动

1. 内核启动

bootloader将kernel从flash中拷贝到RAM以后,bootloader将退出舞台,并将这个舞台交给了kernel。中间有些交接的细节过程,这里不赘述,我们直接从kernel的启动开始分析。

不同平台的kernel启动时,最开始部分的汇编脚本会有些不一样,但是从汇编跳转到C语言的代码过程中的第一条命令大多数都是start_kernel函数,比如arm平台,它汇编代码的最后一个跳转是“b   start_kernel” (linux-3.14/arch/arm/kernel/head-common.S),然后执行start_kernel函数(linux-3.14/init/main.c),这个函数完成一些cpu,内存等初始化以后就会执行rest_init(linux-3.14/init/main.c)函数,该函数创建两个内核线程init和kthreadd之后,进入死循环,即所谓的0号进程。

Openwrt学习笔记(四)——系统开机启动_第1张图片

kenrel_init()(init/main.c)函数,在kernel_init函数中,该函数首先会调用kernel_init_freeable,该函数主要完成以下工作:

1.打开/dev/console,而且该打开句柄的文件描述符是0(标准输出),接着调动sys_dup复制两个文件描述符,分别是1和2,用于标准输入和标准出错。因为它是第一个打开的文件,所以文件描述符是0,如果打开的是其他文件,标准输出就在是0了。

2.第二件事是看以下uboot有没有传启动ramdisk的命令过来,如果没有,就判断/init文件是否存在,如果存在则调用prepare_namespace函数,这个函数会完成根文件系统的挂载工作。

Openwrt学习笔记(四)——系统开机启动_第2张图片

因为从开机的log可以看到uboot传来的启动命令[    0.000000] Kernel command line:  rootwait rootfsname=rootfs rootwait clk_ignore_unused,

所以saved_root_name=rootfs, 那么prepare_namespace()会调用name_to_dev_t()得到主次设备号并存放在ROOT_DEV(31:12),

Openwrt学习笔记(四)——系统开机启动_第3张图片

得到主次设备号后会调用 mount_root, 该函数会调用  mount_block_root("/dev/root", root_mountflags);

Openwrt学习笔记(四)——系统开机启动_第4张图片

mount_block_root 首先调用 get_fs_names 得到根文件系统的类型(通常由rootfstype=来指定), 然后调用 do_mount_root, 该函数会调用 sys_mount 完成任务,将根文件系统 mount 到 /root 后以后,会调用 chroot 将根目录切换到 /root 目录, 使其根文件系统变成真正的根。而原来的根只是一个虚拟的内存根。

Openwrt学习笔记(四)——系统开机启动_第5张图片

成功log:[    1.681344] VFS: Mounted root (squashfs filesystem) readonly on device 31:12.

31:12是mtd12 的主次设备号,我们可以用下面的命令来确认:

root@test:/dev# file /dev/mtdblock12
/dev/mtdblock12: block special (31/12)

而从flash分区情况可以知道该分区存放的是rootfs,分区表如下:

[    1.453252] Creating 14 MTD partitions on "spi0.0":
[    1.458100] 0x000000000000-0x000000040000 : "0:SBL1"   //0号分区
[    1.464274] 0x000000040000-0x000000060000 : "0:MIBIB"
[    1.469425] 0x000000060000-0x0000000c0000 : "0:QSEE"
[    1.474479] 0x0000000c0000-0x0000000d0000 : "0:CDT"
[    1.479346] 0x0000000d0000-0x0000000e0000 : "0:DDRPARAMS"
[    1.484785] 0x0000000e0000-0x0000000f0000 : "0:APPSBLENV"
[    1.490212] 0x0000000f0000-0x000000170000 : "0:APPSBL"
[    1.495430] 0x000000170000-0x000000180000 : "0:ART"
[    1.500384] 0x000000180000-0x000000190000 : "config"
[    1.505436] 0x000000190000-0x0000001a0000 : "pot"
[    1.510249] 0x0000001a0000-0x0000001b0000 : "data"
[    1.515434] 0x0000001b0000-0x000001fc0000 : "0:HLOS"
[    1.520486] 0x000000540000-0x000001fc0000 : "rootfs"  //12号分区
[    1.525471] mtd: device 12 (rootfs) set to be root filesystem
[    1.530832] 1 squashfs-split partitions found on MTD device rootfs
[    1.536393] 0x000001130000-0x000001fc0000 : "rootfs_data"

执行完上面的代码后,会返回kernel_init函数,接着执行下面的代码,它首先会检查内核的启动参数中是否有设置init参数,如果有,则会使用该参数指定的程序作为init程序,否则会按照如下代码中所示的顺序依次尝试启动,如果都无法启动就会kernel panic。

Openwrt学习笔记(四)——系统开机启动_第6张图片

如果没有给init传递参数,那么系统就会从“/etc/preinit” 开始执行,启动文件系统。

2. “/etc/preinit”

(openwrt/package/base-files/files/etc)

    #!/bin/sh  
    # Copyright (C) 2006 OpenWrt.org  
    # Copyright (C) 2010 Vertical Communications  
      
    [ -z "$PREINIT" ] && exec /sbin/init  
      
    export PATH=/bin:/sbin:/usr/bin:/usr/sbin  
      
    pi_ifname=  
    pi_ip=192.168.1.1  
    pi_broadcast=192.168.1.255  
    pi_netmask=255.255.255.0  
      
    fs_failsafe_ifname=  
    fs_failsafe_ip=192.168.1.1  
    fs_failsafe_broadcast=192.168.1.255  
    fs_failsafe_netmask=255.255.255.0  
      
    fs_failsafe_wait_timeout=2  
      
    pi_suppress_stderr="y"  
    pi_init_suppress_stderr="y"  
    pi_init_path="/bin:/sbin:/usr/bin:/usr/sbin"  
    pi_init_cmd="/sbin/init"  
      
    . /lib/functions.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  
这个初始化过程遵循如下主线:

 

下面我们一步一步分析这个过程。
在/etc/preinit脚本中,第一条命令如下:
        [ -z "$PREINIT" ] && exec /sbin/init

在从内核执行这个脚本时,PREINIT这个变量时没有定义的,所以会直接执行/sbin/init。/sbin/init程序主要做了一些初始化工作,如环境变量设置、文件系统挂载、内核模块加载等,之后会创建两个进程,分别执行/etc/preinit和/sbin/procd,执行/etc/preinit之前会设置变量PREINIT,/sbin/procd会带-h的参数,当procd退出后会调用exec执行/sbin/proc替换当前init进程(具体过程可参见procd程序包中的init和procd程序)。这就是系统启动完成后,ps命令显示的进程号为1的进程名最终为/sbin/procd的由来,中间是有几次变化的。

继续看/etc/preinit脚本,出来变量设置外,接下来是执行了三个shell脚本:

                . /lib/functions.sh

                . /lib/functions/preinit.sh

                . /lib/functions/system.sh

注意“.”和“/”之间是有空格的,这里的点相当与souce命令,但souce是bash特有的,并不在POSIX标准中,“.”是通用的用法。使用“.”的意思是在当前shell环境下运行,并不会在子shell中运行。这几个shell脚本主要定义了shell函数,特别是preinit.sh中,定义了hook相关操作的函数。

之后会使用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

之后会向这些结点中添加hook函数。在之后就是一个循环,依次在当前shell下执行/lib/preinit/目录下的脚本,
                for pi_source_file in /lib/preinit/*; do
                . $pi_source_file

                done


这些脚本包括:

02_default_set_state
10_indicate_failsafe
10_indicate_preinit
10_sysinfo
30_failsafe_wait
40_run_failsafe_hook
50_indicate_regular_preinit
70_initramfs_test

80_mount_root     //这里会对overlay目录进行挂载

Openwrt学习笔记(四)——系统开机启动_第7张图片

99_10_failsafe_login
99_10_run_init
由于脚本众多,因此openwrt的设计者将这些脚本分成下面几类:
preinit_essential
preinit_main
failsafe
initramfs
preinit_mount_root
每一类函数按照脚本的开头数字的顺序运行。
等目录用于安装真正的根。
/lib/preinit/目录下的脚本具体类似的格式,定义要添加到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的最开始添加一条命令set -x,这样就会打印出执行命令的过程, 当并不会真正执行。


#####################################

preinit执行的最后一个脚本为99_10_run_init,运行
exec env - PATH=$pi_init_path $pi_init_env $pi_init_cmd
pi_init_cmd为
pi_init_cmd="/sbin/init"

因此开始运行busybox的init命令

##########################################

上面这些是在旧的openwrt下面的实现,在新的openwrt中没有pi_init_cmd这样的命令了,它在procd中实现。因为/sbin/init进程的最后一个函数preinit()函数会创建两个新的进程,一个是procd,一个是/etc/preinit,下面来仔细分析一下:

Openwrt学习笔记(四)——系统开机启动_第8张图片

/sbin/init进程是来自procd这个package里面的,不再使用busybox了,而且它是从内核调用过来的,所以它的pid是1,pid 0是内核本身。fork创建父子进程,子进程做一些procd的配置后退出,注意这时procd并不算真正起来,它的pid不是1;父进程继续创建父子进程,子进程调用/etc/preinit后退出。在这过程中/sbin/init的pid为1,始终没有让位。

    创建子进程执行/etc/preinit脚本时,此时PREINIT环境变量被设置为1,主进程(pid=1)同时使用uloop_process_add()把/etc/preinit子进程加入uloop进行监控,当/etc/preinit执行结束时回调plugd_proc_cb()函数把监控/etc/preinit进程对应对象中pid属性设置为0,表示/etc/preinit已执行完成
    创建子进程执行/sbin/procd -h/etc/hotplug-preinit.json,主进程同时使用uloop_process_add()把/sbin/procd子进程加入uloop进行监控,当/sbin/procd进程结束时回调spawn_procd()函数,spawn_procd()函数繁衍后继真正使用的/sbin/procd进程,这时procd的进程号将是1。

下面这个函数会用procd将/sbin/init进程替换,从而procd的进程号为1:

Openwrt学习笔记(四)——系统开机启动_第9张图片


    从/tmp/debuglevel读出debug级别并设置到环境变量DBGLVL中,把watchdog fd设置到环境变量WDTFD中,最后调用execvp()繁衍/sbin/procd进程 

 
  

3. “/sbin/init”(下面内容主要来自网络)

这个进程以前是由busy box实现,但是现在由procd来实现了,找代码时不要找错位置。

int  main(int argc, char **argv)  
{  
    pid_t pid;  
  
    sigaction(SIGTERM, &sa_shutdown, NULL);  
    sigaction(SIGUSR1, &sa_shutdown, NULL);  
    sigaction(SIGUSR2, &sa_shutdown, NULL);  
  
    early();//-------->early.c  
    cmdline();  
    watchdog_init(1); //------->../watchdog.c  
  
    pid = fork();  
    if (!pid) {  
        char *kmod[] = { "/sbin/kmodloader", "/etc/modules-boot.d/", NULL };  
  
        if (debug < 3) {  
            int fd = open("/dev/null", O_RDWR);  
  
            if (fd > -1) {  
                dup2(fd, STDIN_FILENO);  
                dup2(fd, STDOUT_FILENO);  
                dup2(fd, STDERR_FILENO);  
                if (fd > STDERR_FILENO)  
                    close(fd);  
            }  
        }  
        execvp(kmod[0], kmod);  
        ERROR("Failed to start kmodloader\n");  
        exit(-1);  
    }  
    if (pid <= 0)  
        ERROR("Failed to start kmodloader instance\n");  
    uloop_init();  
    preinit();    //-------------->watchdog.c  
    uloop_run();  
  
    return 0;  
}  

early()

  • mount /proc /sys /tmp /dev/dev/pts目录(early_mount)
  • 创建设备节点和/dev/null文件结点(early_dev)
  • 设置PATH环境变量(early_env)
  • 初始化/dev/console

cmdline()

  • 根据/proc/cmdline内容init_debug=([0-9]+)判断debug级别

watchdog_init()

  • 初始化内核watchdog(/dev/watchdog)

加载内核模块

  • 创建子进程/sbin/kmodloader加载/etc/modules-boot.d/目录中的内核模块

preinit()

  • 创建子进程执行/etc/preinit脚本,此时PREINIT环境变量被设置为1,主进程同时使用uloop_process_add()把/etc/preinit子进程加入uloop进行监控,当/etc/preinit执行结束时回调plugd_proc_cb()函数把监控/etc/preinit进程对应对象中pid属性设置为0,表示/etc/preinit已执行完成

  • 创建子进程执行/sbin/procd -h/etc/hotplug-preinit.json,主进程同时使用uloop_process_add()把/sbin/procd子进程加入uloop进行监控,当/sbin/procd进程结束时回调spawn_procd()函数

  • spawn_procd()函数繁衍后继真正使用的/sbin/procd进程,从/tmp/debuglevel读出debug级别并设置到环境变量DBGLVL中,把watchdog fd设置到环境变量WDTFD中,最后调用execvp()繁衍/sbin/procd进程

watchdog

如果存在/dev/watchdog设备,设置watchdog timeout等于30秒,如果内核在30秒内没有收到任何数据将重启系统。用户状进程使用uloop定时器设置5秒周期向/dev/wathdog设备写一些数据通知内核,表示此用户进程在正常工作

/**
 * 初始化watchdog
 */
void watchdog_init(int preinit)

/**
 * 设备通知内核/dev/watchdog频率(缺省为5秒)
 * 返回老频率值
 */
int watchdog_frequency(int frequency)

/**
 * 设备内核/dev/watchdog超时时间
 * 当参数timeout<=0时,表示从返回值获取当前超时时间
 */
int watchdog_timeout(int timeout)

/**
 * val为true时停止用户状通知定时器,意味着30秒内系统将重启
 */
void watchdog_set_stopped(bool val)

signal

信息处理,下面为procd对不同信息的处理方法

  • SIGBUS、SIGSEGV信号将调用do_reboot() RB_AUTOBOOT重启系统
  • SIGHUP、SIGKILL、SIGSTOP信号将被忽略
  • SIGTERM信号使用RB_AUTOBOOT事件重启系统
  • SIGUSR1、SIGUSR2信号使用RB_POWER_OFF事件关闭系统

procd

procd有5个状态,分别为STATE_EARLYSTATE_INITSTATE_RUNNINGSTATE_SHUTDOWNSTATE_HALT,这5个状态将按顺序变化,当前状态保存在全局变量state中,可通过procd_state_next()函数使用状态发生变化

STATE_EARLY状态 - init前准备工作

  • 初始化watchdog
  • 根据"/etc/hotplug.json"规则监听hotplug
  • procd_coldplug()函数处理,把/dev挂载到tmpfs中,fork udevtrigger进程产生冷插拔事件,以便让hotplug监听进行处理
  • udevstrigger进程处理完成后回调procd_state_next()函数把状态从STATE_EARLY转变为STATE_INIT

STATE_INIT状态 - 初始化工作

  • 连接ubusd,此时实际上ubusd并不存在,所以procd_connect_ubus函数使用了定时器进行重连,而uloop_run()需在初始化工作完成后才真正运行。当成功连接上ubusd后,将注册servicemain_object对象,system_object对象、watch_event对象(procd_connect_ubus()函数),
  • 初始化services(服务)和validators(服务验证器)全局AVL tree
  • 把ubusd服务加入services管理对象中(service_start_early)
  • 根据/etc/inittab内容把cmd、handler对应关系加入全局链表actions中
  • 执行inittab的脚本,该脚本来自
    package/base-files/files/etc/inittab
    ::sysinit:/etc/init.d/rcS S boot
    ::shutdown:/etc/init.d/rcS K stop
    tts/0::askfirst:/bin/ash --login
    ttyS0::askfirst:/bin/ash --login
    tty1::askfirst:/bin/ash --login
    sysinit为系统初始化运行的 /etc/init.d/rcS S boot脚本
    shutdown为系统重启或关机运行的脚本
    tty开头的是,如果用户通过串口或者telnet登录,则运行/bin/ash --login

    askfirst和respawn相同,只是在运行前提示"Please press Enter to activate this console."
  • 顺序加载respawnaskconsoleaskfirstsysinit命令
  • sysinit命令把/etc/rc.d/目录下所有启动脚本执行完成后将回调rcdone()函数把状态从STATE_INITl转变为STATE_RUNNING
    当前启动转到运行 /etc/init.d/rcS S boot,该脚本来自
    package/base-files/files/etc/init.d/rcS
    和preinit类似,rcS也是一系列脚本的入口,其运行/etc/rc.d目录下S开头的的所
    有脚本(如果运行rcS K stop,则运行K开头的所有脚本)
    K50dropbear S02nvram S40network S50dropbear S96led
    K90network S05netconfig S41wmacfixup S50telnet S97watchdog
    K98boot S10boot S45firewall S60dnsmasq S98sysntpd
    K99umount S39usb S50cron S95done S99sysctl
    上面的脚本文件来自:
    package/base-files/files/etc/init.d
    target/linux/brcm-2.4/base-files/etc/init.d
    还有一些脚本来自各个模块,在install时拷贝到rootfs,比如dropbear模块
    package/dropbear/files/dropbear.init
    这些脚本先拷贝到/etc/init.d下,然后通过/etc/rc.common脚本,将init.d的脚本链接到/etc/rc.d目录下,并且根据 这些脚本中的START和STOP的关键字,添加K${STOP}和S${START}的前缀,这样就决定了脚本的先后的运行次序。

STATE_RUNNING状态

  • 进入STATE_RUNNING状态后procd运行uloop_run()主循环

trigger任务队列

数据结构

struct trigger {
    struct list_head list;

    char *type;

    int pending;
    int remove;
    int timeout;

    void *id;

    struct blob_attr *rule;
    struct blob_attr *data;
    struct uloop_timeout delay;

    struct json_script_ctx jctx;
};

struct cmd {
    char *name;
    void (*handler)(struct job *job, struct blob_attr *exec, struct blob_attr *env);
};

struct job {
    struct runqueue_process proc;
    struct cmd *cmd;
    struct trigger *trigger;
    struct blob_attr *exec;
    struct blob_attr *env;
};

接口说明

/**
 * 初始化trigger任务队列
 */
void trigger_init(void)

/**
 * 把服务和服务对应的规则加入trigger任务队列
 */
void trigger_add(struct blob_attr *rule, void *id)

/**
 * 把服务从trigger任务队列中删除
 */
void trigger_del(void *id)

/**
 * 
 */
void trigger_event(const char *type, struct blob_attr *data)

service

Name Handler Blob_msg policy
set service_handle_set service_set_attrs
add service_handle_set service_set_attrs
list service_handle_list service_attrs
delete service_handle_delete service_del_attrs
update_start service_handle_update service_attrs
update_complete service_handle_update service_attrs
event service_handle_event event_policy
validate service_handle_validate validate_policy

system

Name Handler Blob_msg policy
board system_board  
info system_info  
upgrade system_upgrade  
watchdog watchdog_set watchdog_policy
signal proc_signal signal_policy
nandupgrade nand_set nand_policy

shell调用接口

代码库路径: package/system/procd/files/procd.sh 设备上路径: /lib/functions/procd.sh

/etc/init.d/daemon

#!/bin/sh /etc/rc.common

START=80
STOP=20

USE_PROCD=1

start_service()
{
    procd_open_instance
    procd_set_param command /sbin/daemon
    procd_set_param respawn
    procd_close_instance
}


你可能感兴趣的:(openwrt)