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\n");
  30. exit(-1);
  31. }
  32. if (pid <= 0)
  33. ERROR( "Failed to start kmodloader instance\n");
  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

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM