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_EARLY、STATE_INIT、STATE_RUNNING、STATE_SHUTDOWN、STATE_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."
順序載入respawn、askconsole、askfirst、sysinit命令 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.commonSTART=80STOP=20USE_PROCD=1start_service(){ procd_open_instance procd_set_param command /sbin/daemon procd_set_param respawn procd_close_instance}