2.Linux内核启动流程分析

上篇博客讲解了Linux内核的代码结构以及编译流程等。这篇博客主要讲解的是内核的启动流程,对于移植内核有帮助。要理解内核的启动流程就需要知道内核的目的,内核启动前的一些列复杂而繁琐的操作主要目的就是为了让内核运行起来,那内核运行的目的又是什么呢?肯定是为了跑运用程序,所以内核的主要目的是挂载根文件系统,然后运行自己的程序。由于Linux内核和u-boot一样支持多种CPU框架以及多种单板所以存在架构、开发板相关的初始化功能。内核启动流程也可以分为两个部分:架构/开发板相关的引导过程、后续的通用启动过程。下图是ARM架构处理器上Linux内核vmlinux的启动流程,这里使用vmlinux内核,是因为别的格式(Image、uImage等)的内核进行一些特殊操作之后都会得到vmlinux,然后启动vmlinux。

第一个阶段通常使用汇编语言编程,由于内核支持多种CPU架构以及多种开发板,所以需要检测内核是否支持这种CPU架构以及开发板。在BootLoader引导最后的时候,会把机器码传入给内核。检测通过后,由于链接内核时使用的是虚拟地址,所以需要设置页表、使能MMU——>准备调用下一个阶段的C(start_kernel)环境,所以需要复制数据段、清除BBS段、设置栈、调用start_kernel函数(即内核使用的虚拟地址)

下面分析第一阶段的代码流程,(arch/arm/kernel/head.S)

上面是arch/$(ARCH)/head.S中的内容,主要是用来检测内核是否支持CPU架构以及单板(单板是在内核启动时BootLoader传入进来的第一个参数(R1中保存))、创建一级页表、使能MMU。以下是详解。

①检测内核是否支持此开发板的CPU,CPU的ID可以从协处理器CP15中的C0获取格式为:

厂商编号(8bit)ARM使用0x41 产品子编号(4bit) ARM体系版本号(4bit) 产品主编号(8bit) 处理器版本号(4bit)

在head_common.S中定义了上面这个函数。对于__lookup_processor_type函数中第一行的内容是获取标号3:处的物理地址给R3中(此时还没有开启MMU功能,所以CPU使用的还是物理地址)Note:ADR 指令获取的地址是基于PC寄存器计算出来的。第二行的代码是把__proc_info_begin __proc_info_end 以及.的值(这些是链接脚本里面使用的地址,属于虚拟地址)保存在r5-r7中。这个是计算出物理地址与虚拟地址的偏差,后面两个是计算出__proc_info_begin和__proc_info_end 的物理地址。标号1:里面做的事情是从__proc_info_begin到__proc_info_end中读取proc_info_list信息(proc_info_list结构体原型在include/asm-arm/procinfo.h中定义,表示内核支持的CPU。对于ARM架构的CPU这些结构体定义在arch/arm/mm目录中,如proc_arm920.S,表示的是arm920架构CPU的proc_info_list的结构等信息的定义文件。不同的proc_info_list结构被用来支持不同机构下的CPU,它们都是定义在.proc_info_init段中。这些结构体被组织在一起__proc_info_begin是开始地址),然后和从处理器中读取的CPUID(r9)值比较,看下是不是属于相同的CPU,匹配的话调到标号2:处返回,要是不匹配的话继续读取,再匹配。直到最后都没有匹配成功,r5赋值为0结束。note:不同的开发板其CPU可能不同,ARM架构的CPU在arch/arm/mm/目录下定义,里面定义了CPU的相关信息,如proc_arm920.S文件中定义了__arm920_proc_info(proc_info_list结构体)。所以使用这个CPU的时候需要把这个文件包含进来,在配置内核的时候需要配置上(配置菜单是system Type->Support ARM920T processor),使用别的芯片也是同样的道理

②检测内核是否支持此开发板的机器ID,机器ID在BootLoader启动内核的时候传入(保存在R1中)。__look_machine_type函数也是和__look_processor_type类似,同样定义在head_common.S中

内核中对于每种支持的开发板都会定义一个machine_desc结构,它定义了开发板相关的一些属性及函数,如机器类型ID、起始I/O物理地址,中断初始化函数等对于ARM架构的板级相关的定义文件存放在arch/arm/mach-xxx/xxxx.c中,里面包含着与开发板相关的一些信息,machine_desc结构原型体定义在include/asm-arm/mach/arch.h中,所有的machine_desc结构都处于.arch.info.init段中,该段的起始地址是__arch_info_begin,结束地址是__arch_info_end。要使用相关开发板时也需要配置内核,同样也需要把相关的文件包含进内核(如arch/arm/math-s3c2410/mach-smdk2410.c),配置菜单在system type->XXX machine->xx中。

        __lookup_machine_type函数和上面的__lookup_processor_type函数执行的流程类似,把.arch_info_init段的虚拟起始地址、结束地址获取到,并且转化为物理地址。然后从段的开始地址读取machine_desc中的machine ID然后和R1比较,要是匹配跳转到标号2处返回。不匹配则继续读取里面的信息,要是都不匹配则赋值R5=0结束。

③创建页表使能MMU,初始化栈、清除BSS段以及复制数据段等,调用start_kernel(head-common.S中)进入内核的第二个阶段。

在调用__enable_mmu使能MMU函数后,函数最后执行R13中的东西(即上面保存的__switch_data),__switch_data定义在head_common.S中,里面定义了很多函数。

上面这个函数的最后调用main.c中的start_kernel进入第二个阶段。

内核启动的第二阶段:

u-boot传给内核的参数有两类:一是预先存在某个地址的tag列表,二是调用内核时在R1寄存器中指定的机器ID。机器ID在引导阶段会使用到(上述中的检测机器ID时),tag列表将在setup_arch函数中进行初步处理。

set_arch函数的功能:进行处理器相关的一些设置——>进行开发板相关的一些设置——>处理tag列表——>处理命令行参数——>重新初始化页表paging_init(&meminfo,mdesc)。

在初始化页表paging_init中,参数二就是前面lookup_machine_type函数返回的machine_desc结构体,此结构体定义在arch/arm/mach-xxx/mach-smdkxxx.c函数中。如s3c2440开发板的定义如下。此结构体灰常重要,板级初始化都定义在这里

而paging_init->devicemap_init->mdesc->map_io()中的map_io就是使用了此结构体中的.map_io函数指针成员。需要修改下时钟,把16934400修改成12MHz。

控制台初始化函数:

调用__con_initcall_start到__con_initcall_end之间定义的每个函数。这些函数都是使用宏console_initcall()来指定。此宏用来定义一个具有.con_initcall_init段属性的函数。

在一系列的初始化之后,start_kernel会调用rest_init();在rest_init()函数中会创建内核线程kernel_init。主要是挂载文件系统和运行运用程序,在prepare_namespace()中挂载根文件系统

挂载文件系统之后就运行init_post(),里面主要是打开/dev/console设备文件,然后调用run_init_process()执行init程序。

内核运行到这里已经启动了第一个进程init,init会fork出各种应用程序。然后可以开始跑用户空间的内容了。

发布了35 篇原创文章 · 获赞 1 · 访问量 1870

猜你喜欢

转载自blog.csdn.net/lzj_linux188/article/details/102695522