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. }
原文地址:https://www.cnblogs.com/sky-heaven/p/13750692.html