Openwrt学习笔记(四)——系统开机启动【转】
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后,将注册service
main_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
-
-
-
START=80
-
STOP=20
-
-
USE_PROCD=1
-
-
start_service()
-
{
-
procd_open_instance
-
procd_set_param command /sbin/daemon
-
procd_set_param respawn
-
procd_close_instance
-
}