Linux内核启动过程
本文主要是对《嵌入式Linux应用开发完全手册》中内容的整理和总结,在进行这一部分学习之前,有必要对
Linux内核源码组织结构进行了解。
Linux内核在启动过程中执行了很多的函数,不可能像学习U-Boot时一样将所有相关的代码查看一遍,主要了解其总体的启动过程和其中的一些函数进行了解。
启动过程概述
启动过程分成两个部分,第一阶段用汇编代码编写,第二阶段是用C语言进行编写的。
第一阶段
- 检查内核是否支持CPU、开发板型号。
- 连接内核时使用的是虚拟地址,所以需要设置页表、使能MMU。
- 调用C函数start_kernel之前的常规工作,包括复制数据段、清除BBS段、调用start_kernel函数。
第二阶段
第二阶段使用C语言编写,它进行内核的初始化的全部工作,最后调用rest_init函数启动init过程,创建系统第一个进程:init进程。在第二阶段,仍有部分架构/开发板相关的代码,比如setup_arch函数用于进行架构/开发板相关的设置(比如重新设置页表、设置系统时钟、初始化串口等)。
图片摘自《嵌入式Linux应用开发完全手册》
第一阶段代码分析
SECTIONS
{
.text.head : {
_stext = .;
_sinittext = .;
*(.text.head)
}
...
}
上述的代码摘自连接文件,可以看出段.text.head位于Linux内核镜像中的最前面的部分,也是最先执行的。
.section ".text.head", "ax"
ENTRY(stext)
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode
@ and irqs disabled
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
bl __vet_atags
bl __create_page_tables
上述代码摘自head.S文件,这一部分是Linux内核镜像的入口点。
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9
首先将处理器设置为SVC模式,同时禁止FIR和IRQ。
mrc p15, 0, r9, c0, c0
bl __lookup_processor_type
movs r10, r5
beq __error_p
从协处理寄存器中获得CPU的型号,然后与内核支持的CPU型号进行比较,如果没有相符合的就跳转到错误处理,错误处理会将CPU挂起来,这样只能重启CPU了。
CPU型号信息由结构体proc_info_list保存,该结构体的原型在
/arch/arm/include/asm/procinfo.h中定义。
具体支持的型号在/arch/arm/mm/proc-arm920.S中
.section ".proc.info.init",
.type __arm920_proc_info,
__arm920_proc_info:
.long 0x41009200
.long 0xff00fff0
...
可见该结构体在连接时,将会出现在.proc.info.init段中,相应的在连接文件中有如下代码:
__proc_info_begin = .
*(.proc.info.init)
__proc_info_end = .
所以在检测是否支持目标板的CPU类型时,只要将该段中的CPU逐一比较即可。
bl __lookup_machine_type
movs r8, r5
beq __error_a
接下来是比较U-boot传递的开发板ID是否符合编译的内核Linux的开发板ID,如果不一致同样会进入错误代码处理,将CPU挂起,只能重启CPU。
U-boot是通过将机器ID保存在R1寄存器中进行传递的,Linux中的机器ID保存在一个machine_desc结构中,它定义了开发板相关的一些属性及函数,比如机器类型ID、起始I/O物理地址,Bootloader传入的参数的地址、中断初始化函数、I/O映射函数。该结构体和体系架构关系密切,所以定义在体系架构代码部分,比如对于SDMK2440开发板,在/arch/arm/mach-s3c2440/mach-smdk2440.c中定义,如下:
MACHINE_START(S3C2440, "SMDK2440")
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.init_irq = s3c24xx_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.timer = &s3c24xx_timer,
MACHINE_END
它是以宏的形式表示的,宏的定义在
arch/arm/include/asm/mach/arch.h中
#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
};
展开后会有一个MACH_TYPE_S3C2440宏,该宏定义在arch/arm/tools/mach-types中定义,最后会被转换成一个头文件mach-types.h供其他文件包含。
该结构体将会被连接到一个单独的段
.arch.info.init中,该段在连接文件中如下:
__arch_info_begin = .
*(.arch.info.init)
__arch_info_end = .
第一阶段接下来的部分是用来创建一级页表,使能MMU的,具体的代码不分析,用到的时候再仔细研究。
第二阶段代码分析
在进入start_kernel函数之后,如果在串口上没有看到内核的启动信息,一般而言有两个原因:Bootloader传入的命令行参数不对,或者setup_arch函数针对开发板的设置不正确。
在调用setup_arch函数之前已经调用了printk(linux_banner),但是并不会立即在控制台中显示,而只是放在缓冲区中,当控制台初始化完毕之后才会显示该内容。
setup_arch函数分析
在第二阶段中,setup_arch函数是非常重要的一个函数,它完成和开发板有关的初始化。该函数在arch/arm/kernel/setup.c中定义。
上面的图片摘自《嵌入式Linux应用开发完全手册》
下面是函数setup_arch:
void __init setup_arch(char **cmdline_p)
{
...
setup_processor();
mdesc = setup_machine(machine_arch_type);
...
if (__atags_pointer)
tags = phys_to_virt(__atags_pointer);
else if (mdesc->boot_params)
tags = phys_to_virt(mdesc->boot_params);
...
if (tags->hdr.tag == ATAG_CORE) {
if (meminfo.nr_banks != 0)
squash_mem_tags(tags);
save_atags(tags);
parse_tags(tags);
}
...
memcpy(boot_command_line, from, COMMAND_LINE_SIZE);
boot_command_line[COMMAND_LINE_SIZE-1] = '