• Openwrt学习笔记(四)——系统开机启动【转】


    转自:https://blog.csdn.net/lee244868149/article/details/57396776?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

    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号进程。

    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函数,这个函数会完成根文件系统的挂载工作。

    因为从开机的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),


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

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

    成功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。

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

    2. “/etc/preinit”

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

    1. #!/bin/sh
    2. # Copyright (C) 2006 OpenWrt.org
    3. # Copyright (C) 2010 Vertical Communications
    4.  
    5. [ -z "$PREINIT" ] && exec /sbin/init
    6.  
    7. export PATH=/bin:/sbin:/usr/bin:/usr/sbin
    8.  
    9. pi_ifname=
    10. pi_ip=192.168.1.1
    11. pi_broadcast=192.168.1.255
    12. pi_netmask=255.255.255.0
    13.  
    14. fs_failsafe_ifname=
    15. fs_failsafe_ip=192.168.1.1
    16. fs_failsafe_broadcast=192.168.1.255
    17. fs_failsafe_netmask=255.255.255.0
    18.  
    19. fs_failsafe_wait_timeout=2
    20.  
    21. pi_suppress_stderr="y"
    22. pi_init_suppress_stderr="y"
    23. pi_init_path="/bin:/sbin:/usr/bin:/usr/sbin"
    24. pi_init_cmd="/sbin/init"
    25.  
    26. . /lib/functions.sh
    27.  
    28. boot_hook_init preinit_essential
    29. boot_hook_init preinit_main
    30. boot_hook_init failsafe
    31. boot_hook_init initramfs
    32. boot_hook_init preinit_mount_root
    33.  
    34. for pi_source_file in /lib/preinit/*; do
    35. . $pi_source_file
    36. done
    37.  
    38. boot_run_hook preinit_essential
    39.  
    40. pi_mount_skip_next=false
    41. pi_jffs2_mount_success=false
    42. pi_failsafe_net_message=false
    43.  
    44. 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目录进行挂载


    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,下面来仔细分析一下:

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



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

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

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

    1. int main(int argc, char **argv)
    2. {
    3. pid_t pid;
    4.  
    5. sigaction(SIGTERM, &sa_shutdown, NULL);
    6. sigaction(SIGUSR1, &sa_shutdown, NULL);
    7. sigaction(SIGUSR2, &sa_shutdown, NULL);
    8.  
    9. early();//-------->early.c
    10. cmdline();
    11. watchdog_init(1); //------->../watchdog.c
    12.  
    13. pid = fork();
    14. if (!pid) {
    15. char *kmod[] = { "/sbin/kmodloader", "/etc/modules-boot.d/", NULL };
    16.  
    17. if (debug < 3) {
    18. int fd = open("/dev/null", O_RDWR);
    19.  
    20. if (fd > -1) {
    21. dup2(fd, STDIN_FILENO);
    22. dup2(fd, STDOUT_FILENO);
    23. dup2(fd, STDERR_FILENO);
    24. if (fd > STDERR_FILENO)
    25. close(fd);
    26. }
    27. }
    28. execvp(kmod[0], kmod);
    29. ERROR("Failed to start kmodloader ");
    30. exit(-1);
    31. }
    32. if (pid <= 0)
    33. ERROR("Failed to start kmodloader instance ");
    34. uloop_init();
    35. preinit(); //-------------->watchdog.c
    36. uloop_run();
    37.  
    38. return 0;
    39. }

    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设备写一些数据通知内核,表示此用户进程在正常工作

    1. /**
    2. * 初始化watchdog
    3. */
    4. void watchdog_init(int preinit)
    5.  
    6. /**
    7. * 设备通知内核/dev/watchdog频率(缺省为5秒)
    8. * 返回老频率值
    9. */
    10. int watchdog_frequency(int frequency)
    11.  
    12. /**
    13. * 设备内核/dev/watchdog超时时间
    14. * 当参数timeout<=0时,表示从返回值获取当前超时时间
    15. */
    16. int watchdog_timeout(int timeout)
    17.  
    18. /**
    19. * val为true时停止用户状通知定时器,意味着30秒内系统将重启
    20. */
    21. 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任务队列

    数据结构

    1. struct trigger {
    2. struct list_head list;
    3.  
    4. char *type;
    5.  
    6. int pending;
    7. int remove;
    8. int timeout;
    9.  
    10. void *id;
    11.  
    12. struct blob_attr *rule;
    13. struct blob_attr *data;
    14. struct uloop_timeout delay;
    15.  
    16. struct json_script_ctx jctx;
    17. };
    18.  
    19. struct cmd {
    20. char *name;
    21. void (*handler)(struct job *job, struct blob_attr *exec, struct blob_attr *env);
    22. };
    23.  
    24. struct job {
    25. struct runqueue_process proc;
    26. struct cmd *cmd;
    27. struct trigger *trigger;
    28. struct blob_attr *exec;
    29. struct blob_attr *env;
    30. };

    接口说明

    1. /**
    2. * 初始化trigger任务队列
    3. */
    4. void trigger_init(void)
    5.  
    6. /**
    7. * 把服务和服务对应的规则加入trigger任务队列
    8. */
    9. void trigger_add(struct blob_attr *rule, void *id)
    10.  
    11. /**
    12. * 把服务从trigger任务队列中删除
    13. */
    14. void trigger_del(void *id)
    15.  
    16. /**
    17. *
    18. */
    19. void trigger_event(const char *type, struct blob_attr *data)

    service

    NameHandlerBlob_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

    NameHandlerBlob_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

      1. #!/bin/sh /etc/rc.common
      2.  
      3. START=80
      4. STOP=20
      5.  
      6. USE_PROCD=1
      7.  
      8. start_service()
      9. {
      10. procd_open_instance
      11. procd_set_param command /sbin/daemon
      12. procd_set_param respawn
      13. procd_close_instance
      14. }
  • 相关阅读:
    实验一 命令解释程序的编写
    试验二
    实验一 命令解释程序的编写(重交)
    Sqlserver数据库帮助类(EFTools)
    js验证
    sqlserver中从日期字段取得月份
    IIS不可用或者有问题解决方法
    professional email address collections
    从psd文件到html
    空白符对HTML结构的影响与解决方案
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/13750692.html
Copyright © 2020-2023  润新知