对于start_kernel函数的具体内容如下:
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];
...
printk(linux_banner);
setup_arch(&command_line);
...
parse_early_param();
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);
...
/* Do the rest non-__init'ed, we're now alive */
rest_init();
}
在start_kernel函数中,会调用setup_arch函数,具体内容如下:
void __init setup_arch(char **cmdline_p)
{
struct tag *tags = (struct tag *)&init_tags;
struct machine_desc *mdesc;
char *from = default_command_line;
setup_processor();
mdesc = setup_machine(machine_arch_type);
machine_name = mdesc->name;
if (mdesc->soft_reboot)
reboot_setup("s");
if (mdesc->boot_params)
tags = phys_to_virt(mdesc->boot_params);
/*
* If we have the old style parameters, convert them to
* a tag list.
*/
if (tags->hdr.tag != ATAG_CORE)
convert_to_tag_list(tags);
if (tags->hdr.tag != ATAG_CORE)
tags = (struct tag *)&init_tags;
if (mdesc->fixup)
mdesc->fixup(mdesc, tags, &from, &meminfo);
if (tags->hdr.tag == ATAG_CORE) {
if (meminfo.nr_banks != 0)
squash_mem_tags(tags);
parse_tags(tags);
}
init_mm.start_code = (unsigned long) &_text;
init_mm.end_code = (unsigned long) &_etext;
init_mm.end_data = (unsigned long) &_edata;
init_mm.brk = (unsigned long) &_end;
memcpy(boot_command_line, from, COMMAND_LINE_SIZE);
boot_command_line[COMMAND_LINE_SIZE-1] = '\0';
parse_cmdline(cmdline_p, from);
...
}
在该函数中通过调用setup_machine(machine_arch_type),以板子的机器ID找到struct machine_desc类型的结构体,里面指出了Uboot传入的参数所保存的地址位置。接着调用tags = phys_to_virt(mdesc->boot_params),内核去该地址上获取Uboot传入的参数,然后解析这些参数。
接着调用以下两条语句:
parse_early_param();
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);
这两条指令都是从__setup_start与__setup_end之间取出相应的数据结构后然后进行处理。
至于__setup_start与__setup_end则是在链接脚本中定义的,打开链接脚本可以看见有以下几句话:
__setup_start = .;
*(.init.setup)
__setup_end = .
在__setup_start与__setup_end之间存放.init.setup段,这里有一个疑问,这些数据结构内容是如何放到.init.setup段去的,通过搜索发现,找到以下一个定义:
#define __setup(str, fn) \
__setup_param(str, fn, fn, 0)
#define __setup_param(str, unique_id, fn, early) \
static char __setup_str_##unique_id[] __initdata = str; \
static struct obs_kernel_param __setup_##unique_id \
__attribute_used__ \
__attribute__((__section__(".init.setup"))) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }
这里以__setup(“root=”, root_dev_setup)为例,其中参数一是要匹配的名字,参数二则是匹配成功后需要执行的函数。
展开的内容则为:
static char __setup_str_root_dev_setup[] __initdata = root=;
static struct obs_kernel_param __setup_root_dev_setup
__attribute_used__
__attribute__((__section__(".init.setup")))
__attribute__((aligned((sizeof(long)))))
= { __setup_str_root_dev_setup, root_dev_setup, early }
而函数root_dev_setup内容为:
static int __init root_dev_setup(char *line)
{
strlcpy(saved_root_name, line, sizeof(saved_root_name));
return 1;
}
其中early则是标记该数据结构是否在parse_early_param()中处理,该函数根据early值进行早期的参数处理,在里面会调用do_early_param 函数。若early标记为0的话,例如__setup(“root=”, root_dev_setup),则该数据结构的处理则会在unknown_bootoption函数中处理。而该函数会再去调用obsolete_checksetup函数。在该函数中做具体处理。obsolete_checksetup函数内容如下:
static int __init obsolete_checksetup(char *line)
{
struct obs_kernel_param *p;
int had_early_param = 0;
p = __setup_start;
do {
int n = strlen(p->str);
if (!strncmp(line, p->str, n)) {
if (p->early) {
if (line[n] == '\0' || line[n] == '=')
had_early_param = 1;
} else if (!p->setup_func) {
printk(KERN_WARNING "Parameter %s is obsolete,"
" ignored\n", p->str);
return 1;
} else if (p->setup_func(line + n))
return 1;
}
p++;
} while (p < __setup_end);
return had_early_param;
}
在该函数中,在__setup_start和__setup_end之间找出相对应的结构体。
以bootargs环境变量为例,它的值为:
noinitrd root=/dev/nfs nfsroot=192.168.3.16:/work/nfs_root/first ip=192.168.3.123:192.168.3.16:192.168.3.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0,
然后通过该值来查找有无对应的结构体,例如已知有定义__setup(“root=”, root_dev_setup),所以就会找到这么一项root=/dev/nfs,接着在obsolete_checksetup函数中执行p->setup_func语句调用与之对应的结构体中保存的函数,对于root=来说,则是调用root_dev_setup函数,它的作用则是将root=的内容保存到saved_root_name变量中。接着循环其它的__setup(),查看有无与第一个参数匹配的项,有的话则调用与之对应的结构体中保存的函数。
接下来回到start_kernel函数,在该函数最后,调用了rest_init函数。在rest_init函数中又会去调用kernel_init函数,继续跟踪,在这函数中会调用到prepare_namespace函数。对prepare_namespace函数进行分析。函数内容如下:
void __init prepare_namespace(void)
{
...
if (saved_root_name[0]) {
root_device_name = saved_root_name;
if (!strncmp(root_device_name, "mtd", 3)) {
mount_block_root(root_device_name, root_mountflags);
goto out;
}
ROOT_DEV = name_to_dev_t(root_device_name);
if (strncmp(root_device_name, "/dev/", 5) == 0)
root_device_name += 5;
}
is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;
if (initrd_load())
goto out;
if (is_floppy && rd_doload && rd_load_disk(0))
ROOT_DEV = Root_RAM0;
mount_root();
}
从以上代码可以发现根文件系统的名字保存在变量saved_root_name中,而该变量的内容则通过之前的obsolete_checksetup函数已经实现了赋值。函数最后则会调用mount_root(),挂载该根文件系统。返回到kernel_init函数后,在该函数中最后还会调用init_post函数,该函数的作用则是执行应用程序。
整个内核的启动流程大致总结为:
arch/arm/kernel/head.S
__switch_data
start_kernel
setup_arch(&command_line) //获取及解析u-boot传入的启动参数
parse_early_param
//从__setup_start到__setup_end,调用early函数
do_early_param //早期参数的一些初始化
unknown_bootoption
//从__setup_start到__setup_end,调用非early函数
obsolete_checksetup
rest_init
kernel_init
prepare_namespace
mount_root //挂载根文件系统
init_post //执行应用程序
到现在初步了解了整个内核的启动过程,但是我知道还远远不够。。很多内容没有去深究,都只是粗略的顺藤摸瓜,说的比较浅,但是让自己对内核有所大概的了解,在以后的路上我也一定会加油,谢谢各位阅读,希望有错或者有疑问的地方可以提出来,一块交流。