简介:
本文主要介绍在2.6.22.6版内核中,代码的运行过程。而在kernel的第二阶段主要介绍在内核代码中如何解析从u-boot和前面汇编代码中获得TAG参数与machine_desc结构体。
声明:
本文主要是看了韦东山老师的视频后所写,希望对你有所帮助。
start_kernel :
从start_kernel函数开始,程序就是C语言代码了。而相较于前面的汇编代码,这里的C语言代码可以实现更加复杂的功能,同时也更好理解一些。我们知道内核要做的事有两件:第一件是挂载根文件系统,而第二件则是执行第一个应用程序。而内核挂载什么样的根文件系统,同时内核执行的第一个应用程序是什么?这些都是我们想要弄清楚的。同时我们在上面一篇文章:嵌入式Linux——分析kernel运行过程(1):kernel第一阶段 中讲到了从u-boot向内核传入三个参数:0,单板对应的机器ID和TAG参数首地址。我们使用第二个参数——机器ID找到了与单板相关的结构体machine_desc。而从u-boot传入的TAG参数至今还没有使用,那么程序是否会在下面用到这些已知的信息那?我们看代码来慢慢了解这些(init\main.c)。
我们初看start_kernel函数会觉得这个函数很大,同时内部的函数很多,但是如果我们带着上面的问题来看这些代码,并将那些没用的函数删除,那么start_kernel可以包含下面一些函数:
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];
printk(KERN_NOTICE);
printk(linux_banner);
setup_arch(&command_line);
setup_command_line(command_line);
page_alloc_init();
printk(KERN_NOTICE "Kernel command line: %s\n", boot_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();
}
而在上面的函数中主要做两件事:
1. 解析TAG参数与machine_desc结构体,获得单板信息
2. 挂接根文件系统以及执行第一个应用程序
解析TAG参数与machine_desc结构体主要由setup_arch函数和setup_command_line函数来完成。我们先看在setup_arch函数中做了什么样的设置(arch\arm\kernel\setup.c):
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);
paging_init(&meminfo, mdesc);
request_standard_resources(&meminfo, mdesc);
/*
* Set up various architecture-specific pointers
*/
init_arch_irq = mdesc->init_irq;
system_timer = mdesc->timer;
init_machine = mdesc->init_machine;
}
从上面的程序可以看出在setup_arch函数中不仅有对TAG参数的解析,而其中更为重要的是对machine_desc结构体的解析,而我们从上一章获得的关于本开发板的machine_desc结构体为:
#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 \
};
MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch
* to SMDK2410 */
/* Maintainer: Jonas Dietsche */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
MACHINE_END
所以我们要对照着machine_desc结构体的内容来分析setup_arch函数。setup_arch函数做的第一件事就是通过参数machine_arch_type来找到单板对应的machine_desc结构体,并获得machine_desc结构体中单板的名称。而对于我们所使用的单板,他的name为:SMDK2410 ,而上面两步操作的代码为:
mdesc = setup_machine(machine_arch_type);
machine_name = mdesc->name;
static struct machine_desc * __init setup_machine(unsigned int nr)
{
struct machine_desc *list;
/*
* locate machine in the list of supported machines.
*/
list = lookup_machine_type(nr);
if (!list) {
printk("Machine configuration botched (nr %d), unable "
"to continue.\n", nr);
while (1);
}
printk("Machine: %s\n", list->name);
return list;
}
观察machine_desc结构体发现其中有一个用于存放TAG参数的首地址的项:boot_params ,而程序可以通过访问这项的内容来获得TAG参数所存放的首地址,进而获得TAG参数的内容。而此时程序已经运行在了内核中,所以这时代码运行在虚拟地址空间。而从boot_params中获得的地址为物理地址,因此要使用phys_to_virt函数将物理地址转化为虚拟地址:
#define __phys_to_virt(x) ((x) - PHYS_OFFSET + PAGE_OFFSET)
static inline void *phys_to_virt(unsigned long x)
{
return (void *)(__phys_to_virt((unsigned long)(x)));
}
if (mdesc->boot_params)
tags = phys_to_virt(mdesc->boot_params);
获得TAG参数首地址后,就要从TAG首地址开始解析从u-boot传入到内核的TAG参数了。而TAG参数有两种形式,一种是老的TAG参数形式(他没有 ATAG_CORE参数,因此不能生成列表形式),而另一种是新的TAG参数形式(这种新的TAG参数可以使用列表来列出这些参数)。TAG参数的列表形式为:
如果从u-boot传入的TAG参数为老式的,那么程序要将其转化为有列表的TAG参数形式,而如果是列表形式的TAG参数,就可以直接对其进行操作。上面所讲的功能的代码为:
/*
* 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);
}
程序在上面将TAG参数放入到列表中方便查找,而在下面程序中要做的是:查找并设置默认的命令行参数。如果u-boot没有向内核传递命令行参数,这时默认的命令行参数就会派上用场。而下面的程序主要就是介绍如何解析并获得默认的命令行参数:
/* Untouched command line saved by arch-specific code. */
char __initdata boot_command_line[COMMAND_LINE_SIZE];
char *from = default_command_line;
memcpy(boot_command_line, from, COMMAND_LINE_SIZE);
boot_command_line[COMMAND_LINE_SIZE-1] = '\0';
parse_cmdline(cmdline_p, from);
上面程式首先做的事情是获得默认的命令行参数:default_command_line 。然后将默认的命令行参数拷贝到cmdline_p中。
做完上面的事情,对于传入的参数的解析就完成的差不多了,而下面要做的就是初始化页表了,程序在前面汇编阶段就初始化过页表,但是那个页表只是暂时使用,而现在函数中要做的才是真正的初始化页表,而此时初始化的页表将覆盖原有的页表。由于我们单板中有mmu(内存管理单元),因此我们使用路径:arch\arm\kernel\setup.c下的页表初始化函数:
/*
* paging_init() sets up the page tables, initialises the zone memory
* maps, and sets up the zero page, bad page and bad page tables.
*/
void __init paging_init(struct meminfo *mi, struct machine_desc *mdesc)
{
void *zero_page;
build_mem_type_table();
prepare_page_table(mi);
bootmem_init(mi);
devicemaps_init(mdesc);
top_pmd = pmd_off_k(0xffff0000);
/*
* allocate the zero page. Note that we count on this going ok.
*/
zero_page = alloc_bootmem_low_pages(PAGE_SIZE);
memzero(zero_page, PAGE_SIZE);
empty_zero_page = virt_to_page(zero_page);
flush_dcache_page(empty_zero_page);
}
初始化好页表之后,程序就要使用虚拟地址去为内核的代码和数据去申请资源了:
static void __init
request_standard_resources(struct meminfo *mi, struct machine_desc *mdesc)
{
struct resource *res;
int i;
kernel_code.start = virt_to_phys(&_text);
kernel_code.end = virt_to_phys(&_etext - 1);
kernel_data.start = virt_to_phys(&__data_start);
kernel_data.end = virt_to_phys(&_end - 1);
for (i = 0; i < mi->nr_banks; i++) {
unsigned long virt_start, virt_end;
if (mi->bank[i].size == 0)
continue;
virt_start = __phys_to_virt(mi->bank[i].start);
virt_end = virt_start + mi->bank[i].size - 1;
res = alloc_bootmem_low(sizeof(*res));
res->name = "System RAM";
res->start = __virt_to_phys(virt_start);
res->end = __virt_to_phys(virt_end);
res->flags = IORESOURCE_MEM | IORESOURCE_BUSY;
request_resource(&iomem_resource, res);
if (kernel_code.start >= res->start &&
kernel_code.end <= res->end)
request_resource(res, &kernel_code);
if (kernel_data.start >= res->start &&
kernel_data.end <= res->end)
request_resource(res, &kernel_data);
}
if (mdesc->video_start) {
video_ram.start = mdesc->video_start;
video_ram.end = mdesc->video_end;
request_resource(&iomem_resource, &video_ram);
}
/*
* Some machines don't have the possibility of ever
* possessing lp0, lp1 or lp2
*/
if (mdesc->reserve_lp0)
request_resource(&ioport_resource, &lp0);
if (mdesc->reserve_lp1)
request_resource(&ioport_resource, &lp1);
if (mdesc->reserve_lp2)
request_resource(&ioport_resource, &lp2);
}
其中最主要的还是为内核的代码和数据申请资源,同时也会为设备申请video资源。
做完上面这些事情之后,最后程序将单板特有的中断函数,定时器函数以及单板初始化函数赋给内核中对应的函数,这样在内核中就不必关心硬件中这些函数的具体实现细节,而他只要调用内核提供的函数指针就好。具体代码为:
/*
* Set up various architecture-specific pointers
*/
init_arch_irq = mdesc->init_irq;
system_timer = mdesc->timer;
init_machine = mdesc->init_machine;
讲到这里setup_arch函数就讲完了。而setup_command_line函数中所做的是存储修改和没有修改的命令行参数。分析这两个函数我们可以知道在这两个函数中主要做的还是解析工作,即将单板特有的信息machine_desc结构体以及TAG参数分别进行解析,并将重要的信息放到内核的参数或者调用函数中。但是我们观察会发现上面做的事情还只是解析,而并没有运用这些信息,更没有讲到挂载根文件系统。
rest_init : 挂载根文件系统并运行第一个应用程序
不好意思,写到这里发现下面的代码有些难懂,因此不敢将他们写出来误导大家,所以这部分的代码就写到这里,十分抱歉。