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

来源:互联网 发布:陈柏霖人品怎么样知乎 编辑:程序博客网 时间:2024/05/22 01:36

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)

    #!/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目录进行挂载


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来实现了,找代码时不要找错位置。

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

NameHandlerBlob_msg policysetservice_handle_setservice_set_attrsaddservice_handle_setservice_set_attrslistservice_handle_listservice_attrsdeleteservice_handle_deleteservice_del_attrsupdate_startservice_handle_updateservice_attrsupdate_completeservice_handle_updateservice_attrseventservice_handle_eventevent_policyvalidateservice_handle_validatevalidate_policy

system

NameHandlerBlob_msg policyboardsystem_board infosystem_info upgradesystem_upgrade watchdogwatchdog_setwatchdog_policysignalproc_signalsignal_policynandupgradenand_setnand_policy

shell调用接口

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

/etc/init.d/daemon

#!/bin/sh /etc/rc.commonSTART=80STOP=20USE_PROCD=1start_service(){    procd_open_instance    procd_set_param command /sbin/daemon    procd_set_param respawn    procd_close_instance}


0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 月子见风怎么办暴雪将 老氏电视机出现雪花点怎么办 飞利浦电视显示雪花片怎么办 电视无信号出雪花怎么办 电视打开都是雪花没有电视台怎么办 电视视频1无信号怎么办 新买的电视无信号怎么办 卫星有信号没有视频怎么办 户户通没有信号怎么办视频 雪花泥粘衣服上怎么办 遗产按份额处分判决后怎么办 宝马后驱车下雪天怎么办 宝马后驱车路滑怎么办 车子陷入泥地里怎么办 深圳居住证签注过期了怎么办 手机不能播放视频乱码了怎么办 被加密的视频无法观看怎么办 苹果5忘记id密码怎么办 七个月宝宝脾胃不好怎么办 八个月宝宝脾虚怎么办 七个月宝宝脾胃虚怎么办 海岛奇兵点错了怎么办 螳螂的脚断了怎么办 海岛奇兵打不过玩家怎么办 海岛奇兵资源满了怎么办 海岛奇兵杯越来越多打不玩家怎么办 海岛奇兵控杯技巧 杯数太高怎么办 海岛奇兵发现求救信号怎么办 海岛奇兵被打了怎么办 小鱼翅卡喉咙了怎么办 鱼翅卡在喉咙里怎么办 斗鱼身份证被使用怎么办 做的鱼丸太腥了怎么办 做鱼丸太稀了怎么办 斗鱼手机号换了怎么办 斗鱼直播掉帧怎么办 手机一直卡顿点不动怎么办呢 斗鱼直播分值底怎么办 斗鱼6000鱼丸怎么办卡 斗鱼直播没人看怎么办 淘宝直播间没人气怎么办