内核版本:linux-3.4
启动流程:
1、比较机器ID
2、解析u-boot传入的启动参数
3、挂接根文件系统、执行第一个应用程序
1、比较机器ID
打开 vmlinux.lds.S 里面可以找到内核把初始化参数的段定义为了”.arch.info.init”
.init.arch.info : {
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
}
在内核源码的目录下可以输入指令查找一下谁调用了这个段:grep “.arch.info.init” * -nR
搜索到如下内容:
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \ /* ##是连词符号 */
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
定义了一个宏来指定,可以接着在 sourceinsight 工程里面去搜索 MACHINE_START 和 MACHINE_END 这两个宏,看下具体谁使用了它,搜索结果如下 :
MACHINE_START(SUNXI, "sun8i")
.atag_offset = 0x100,
.init_machine = sunxi_dev_init,
.init_early = sunxi_init_early,
.map_io = sunxi_map_io,
#ifndef CONFIG_OF
.init_irq = sun8i_gic_init,
#endif
.handle_irq = gic_handle_irq,
.restart = sun8i_restart,
.timer = &sunxi_timer,
.dt_compat = NULL,
.reserve = sun8i_reserve,
.fixup = sun8i_fixup,
.nr_irqs = NR_IRQS,
#ifdef CONFIG_SMP
.smp = smp_ops(sunxi_smp_ops),
#ifdef CONFIG_ARCH_SUN8IW6
.smp_init = smp_init_ops(sun8i_smp_init_ops),
#endif
#endif
MACHINE_END
MACHINE_START 和 MACHINE_END 这两个就是定义一个结构体,这个结构体被强制的设置为一个属性,把它的段设置为 “.arch.info.init”,那么如果哪个文件下面有这些结构体的话,这些结构体就会被 vmlinux.lds.S 这个链接脚本组合在一起,我们可以把这段定义带入到刚才搜索到的那段 MACHINE_START 和 MACHINE_END 的宏里面,代入后他们就被置换成了如下内容:
static const struct machine_desc __mach_desc_SUNXI \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_SUNXI, \
.name = "sun8i",
.atag_offset = 0x100,
.init_machine = sunxi_dev_init,
.init_early = sunxi_init_early,
.map_io = sunxi_map_io,
#ifndef CONFIG_OF
.init_irq = sun8i_gic_init,
#endif
.handle_irq = gic_handle_irq,
.restart = sun8i_restart,
.timer = &sunxi_timer,
.dt_compat = NULL,
.reserve = sun8i_reserve,
.fixup = sun8i_fixup,
.nr_irqs = NR_IRQS,
#ifdef CONFIG_SMP
.smp = smp_ops(sunxi_smp_ops),
#ifdef CONFIG_ARCH_SUN8IW6
.smp_init = smp_init_ops(sun8i_smp_init_ops),
#endif
};
上面的属性就是配置了struct machine_desc 这个结构体,和结构体内容一一对应,从内核得出这些内容后我们就可以和 uboot 传入的参数进行对比,uboot 传数据给内核是从 uboot源码里面的 arch/arm/lib/bootm.c 这个文件的 do_boota_linux 函数,具体调用以下函数:
kernel_entry(0, bd->bi_arch_number, bd->bi_boot_params);
这个函数传入的参数刚好和上面的 MACHINE_START(machine_desc结构体) 刚好对应起来,然后内核启动的时候就会从初始化的 begin 开始读到 end :
.init.arch.info : {
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
}
把uboot传入的ID和内核的ID做比较,如果吻合的话表示支持这个单板,然后还会接着比较后面的参数
2、解析u-boot传入的启动参数
上一章通过分析 linux 源码的 Makefile 和脚本后发现内容启动的第一个程序是:arch/arm/kernel/head.S
这里截取关键的代码如下:
__lookup_processor_type /* 判断是否支持这个CPU处理器 */
__create_page_tables /* 创建页表:因为链接脚本的起始地址并不对应于真实存在的地址,因此需要创建页表来启动MMU */
__enable_mmu /* 使能MMU */
__mmap_switched /* 当MMU使能之后会跳到这个switch里面去,复制数据段、清BSS段、设置栈指针、保存processor_id变量、保存机器类型ID */
start_kernel /* 启动内核,这是第一个C函数,在里面处理启动参数 */
init.... /* 一系列的初始化 */
setup_arch(&command_line); /* 这两句解析uboot传递过来的参数,把参数从 machine_desc 结构体取出来保存 */
setup_command_line(command_line);
...
parse_early_param /* 早期参数初始化 */
parse_early_options
do_early_param
/* 从 __setup_start 到 __setup_end 这是在 vmlinux.lds.S 里面定义的 INIT_SETUP 段 作用是调用那些用early来标识的函数,但是我们函数传递进来的early是0,所以这里用不着 */
unknown_bootoption
obsolete_checksetup
/* 处理从 __setup_start 到 __setup_end 这个段调用的非early的函数 */
...
rest_init();
kernel_init
sys_open((const char __user *) "/dev/console", O_RDWR, 0) /* 打开终端,标准输出 */
(void) sys_dup(0); /* dup是复制的意思,这里代表标准输入 */
(void) sys_dup(0); /*标准错误 ,打开的这三个文件都指向console这个设备,这个设备会给busybox应用 */
prepare_namespace
saved_root_name[0] /* 从uboot设置好的root名读取保存(root_dev_setup函数)到saved_root_name里面, */
/* 如何定义 saved_root_name 的 */
__setup("root=", root_dev_setup); /*这个宏定义了一个结构体,这个结构体里面有两个成员,其实就是一个字符串对应一个处理函数*/
#define __setup(str, fn) \
__setup_param(str, fn, fn, 0) /* 第四个参数是early,这里被设置为0,所以上面的 parse_early_param 可以忽略*/
#define __setup_param(str, unique_id, fn, early) \
static const char __setup_str_##unique_id[] __initconst \
__aligned(1) = str; \
static struct obs_kernel_param __setup_##unique_id \
__used __section(.init.setup) \
/* 这个结构体有个属性,这个属性被强制设备为(.init.setup)这个段,
这个段在链接脚本里面有定义(INIT_SETUP)里面可以找到__setup_start*/
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }
/* 查看它被谁使用,就知道命令行是怎么调用的了,搜索 __setup_start 查看谁调用了,有如下两个函数调用了:*/
obsolete_checksetup /* 被 unknown_bootoption 调用 */
do_early_param /* 被 parse_early_param 调用 */
mount_root(); /* 挂接根文件系统 */
/* 开始执行应用程序 */
init_post
/* 一旦执行了以下任意一个应用程序就是一个死循环,不会再执行后面的了 */
run_init_process(execute_command); /* 搜索关键字execute_command可以发现init_setup调用了它 */
__setup("init=", init_setup) /* init是一个从uboot命令行参数,uboot如果设置了init,则 execute_command 等于设置的值 */
run_init_process("/sbin/init"); //执行应用程序,如果不成功则依次往下执行
...
3、挂接根文件系统、执行第一个应用程序
在上面代码中,先是调用了挂接根文件系统的函数,然后使用函数 run_init_process 执行应用程序,我们可以在 uboot 参数里面定义 init 变量来告诉内核第一个应用程序执行哪个文件,如果没有设置第一个是执行 /sbin/bin,而一般情况下我们第一个程序是 busybox,我们可以先启动系统然后使用 ps 指令查看下当前运行的进程,但是并不会发现有 ls 、cd、cp 等这些程序,但我们一启动就能使用这些命令,那这些程序哪里来的呢?就来源于 busybox 这个应用程序,我们可以使用指令来验证一下:
# ls -l /bin/ls
lrwxrwxrwx 1 root root 7 Oct 18 2017 /bin/ls -> busybox
可以看出 ls 软链接到了 busybox,所以执行 ls 就等于执行 busybox 里面的 ls,busybox 这个应用存放于 /bin 这个目录。
我的内容在 uboot 中是将 init 设置为了 /init,即 init = /init ,它其实也是链接到了 busybox:
# ls /init -l
lrwxrwxrwx 1 root root 11 Dec 14 2017 /init -> bin/busybox
而且如果我们没定义 init ,它就会根据我们上面分析的代码去执行 /sbin/init ,而 /sbin/init 也是链接到了 busybox:
# ls /sbin/init -l
lrwxrwxrwx 1 root root 14 Oct 18 2017 /sbin/init ->../bin/busybox
既然第一个执行的程序是 busybox,下面可以来简单分析一下,看下 init 程序做了些什么
busybox 里面的 init 程序主要执行了以下的一些内容:
1、读取配置文件 –> 这个配置文件指定了后续要读取哪些程序
2、解析配置文件
3、根据配置文件来启动执行“用户程序”
busybox 源码分析:
init_main
parse_inittab
file = fopen(INITTAB, "r"); /* 打开"/etc/inittab"这个配置文件 */
new_init_action /* 根据配置文件调用这个函数,创建一个init_action结构,填充结构体,把结构放入 init_action_list 链表 */
new_init_action(ASKFIRST, bb_default_login_shell, VC_2) 为例分析 new_init_action
const char bb_default_login_shell[] ALIGN1 = LIBBB_DEFAULT_LOGIN_SHELL;
#define LIBBB_DEFAULT_LOGIN_SHELL "-/bin/sh"
# define VC_2 "/dev/tty2"
带入即等于: new_init_action(ASKFIRST,"-/bin/sh","/dev/tty2")
for (a = last = init_action_list; a; a = a->next) /* 创建一个init_action结构,填充结构体,把结构放入链表 */
run_actions(SYSINIT); /* 运行 SYSINIT 这一类型的动作 */
waitfor(a, 0); /* 执行应用程序,等待它执行完毕 */
delete_init_action(a); /* 从 init_action_list 链表里删除 */
run_actions(WAIT); /* 运行 WAIT 这一类型的动作,过程同上 */
run_actions(ONCE); /* 运行 ONCE 这一类型的动作 */
while(1){ /* 这个循环用于命令行运行应用程序使用,结束一个线程就等待用户重新输入 */
run_actions(RESPAWN);
if (a->pid == 0){ /* 一开始PID是等于0的 */
a-pid = run(a);
}
run_actions(ASKFIRST);
if (a->pid == 0){
a-pid = run(a); /* 等待回车,创建子线程 */
打印"\nPlease press Enter to activate this console. "
}
wpid = wait(NULL); /* 等待子进程退出 */
while (wpid > 0) {
a->pid = 0; /* 退出后,就设置 pid=0,重新回到while循环继续执行上面已退出的子进程 */
}
}
上面的 /etc/inittab 配置文件的书写格式如下:
<id>:<runlevels>:<action>:<process>
id : /dev/id
runlevels : 忽略
action : 何时执行,有以下选项
# <action>: Valid actions include: sysinit, respawn, askfirst, wait, once,
# restart, ctrlaltdel, and shutdown.
process : 应用程序或脚本
从 busybox 可以看出最小根文件系统至少需要什么东西:
1. /dev/console 或 /dev/null(如果没有定义console,stdout/in/err 就定位到null去)
2. /etc/inintab
3. 配置文件里指定的应用程序
4. C库
5. init本身,即busybox