当前位置: 首页 > 科技观察

详细图文讲解!嵌入式Linux内核启动主要分为这三个阶段

时间:2023-03-21 22:10:08 科技观察

嵌入式Linux内核启动的整个过程主要分为三个阶段。第一阶段是内核自解压过程,第二阶段主要是设置ARM处理器的工作模式,使能MMU,设置一级页表等,第三阶段主要是C代码,包括内核初始化的所有工作,下面做详细介绍。一、Linux内核自解压过程在Linux内核启动过程中,一般可以看到图1中的内核自解压界面,这里重点介绍内核自解压过程。内核压缩解压代码都在kernel/arch/arm/boot/compressed目录下。编译完成后会生成head.o、misc.o、piggy.gzip.o、vmlinux、decompress.o等几个文件,head.o是内核的头文件,负责初始化设置;misc.o主要负责内核的解压,在head.o之后;piggy.gzip.o是一个中间文件,实际上是一个压缩内核(kernel/vmlinux),但是没有初始化文件和解压文件的链接;vmlinux没有压缩内核(zImage是压缩内核),由piggy.gzip.o、head.o、misc.o、decompress组成。o是新引入的,支持更多的压缩格式。图1解压内核当BootLoader完成系统启动并将Linux内核载入内存后,调用do_bootm_linux(),该函数会跳转到内核的起始位置。如果内核未压缩,则可以启动。如果内核已经被压缩,则需要解压,压缩后的内核头文件中有解压程序。第一个压缩内核入口文件的源码位置在arch/arm/boot/compressed/head.S。它会调用文件arch/arm/boot/compressed/misc.c中的函数decompress_kernel(),decompress_kernel()调用proc_decomp_setup()、arch_decomp_setup()进行设置,然后打印出信息“UncompressingLinux...”,调用gunzip()将内核放置在指定位置。下面简单介绍解压过程,是函数decompress_kernel实现的功能:解压代码位于kernel/lib/inflate.c,inflate.c是从gzip源程序中分离出来的,包含一些直接的对全局数据的引用,使用时需要直接嵌入到代码中。gzip压缩文件时,总是在前32K字节范围内寻找重复的字符串进行编码。解压时,至少需要32K字节的解压缓冲区,定义为window[WSIZE]。inflate.c使用get_byte()读取输入文件,定义为宏以提高效率。输入缓冲区指针必须定义为inptr,在inflate.c中有对它的减量操作。inflate.c调用flush_window()输出windowbuffer中解压后的字节串,每次输出的长度由outcnt变量表示。在flush_window()中,还必须对输出字节字符串和刷新的crc变量计算CRC。在调用gunzip()开始解压之前,先调用makecrc()初始化CRC计算表。***gunzip()返回0表示解压成功。我们将在内核启动开始时看到此输出:UncompressingLinux...done,bootingthekernel。这也是decompress_kernel函数的输出。执行解压过程后,返回到head.S中的583行,在r4中启动内核在head.S的180行已经预设了内核映像的地址,如下:这样就进入了Linux内核的第一阶段,我们也称之为stage1。2、Linux内核启动阶段stage1跟上。这里所说的第一阶段stage1是内核解压完成后UncompressingLinux...done,启动内核的阶段。出现。这部分代码在arch/arm/kernel的head.S中实现。该文件中的汇编代码通过找到处理器内核类型和机器码类型调用相应的初始化函数,然后建立页表,最后跳转到start_kernel()函数开始内核的初始化。检测处理器类型在汇编子函数__lookup_processor_type中完成,可通过以下代码调用:bl__lookup_processor_type(在head-commom.S文件中实现)。当__lookup_processor_type调用结束返回原程序时,返回结果会保存在寄存器中。其中,r5寄存器返回一个用于描述处理器的结构地址,对r5进行判断。如果r5的值为0,表示不支持这个处理器,就会进入__error_p。r8保存页表的标志位,r9保存处理器的ID号,r10保存处理器相关的structproc_info_list结构的地址。Head.S的核心代码如下:机器码类型的检测是在汇编子函数__lookup_machine_type中完成的(也在head-common.S文件中实现)。类似于__lookup_processor_type,通过代码调用:“bl__lookup_machine_type”。当函数返回时,返回结构会保存在r5、r6、r7这三个寄存器中。其中,r5寄存器返回一个用于描述机器(即开发板)的结构体地址,判断r5。如果r5的值为0,表示不支持本机(开发板),会进入__error_a并打印内核不支持u-boot导入的机器码的错误,如图2所示。r6保存I/O基地址,r7保存I/O页表偏移地址。检测到处理器类型和机器码类型后,会调用__create_page_tables子函数创建页表。它需要做的是将RAM基地址开始的1M空间的物理地址映射到0xC0000000开始的虚拟地址。对于本项目的开发板DM3730,RAM附加到物理地址0x80000000。调用__create_page_tables后,物理地址0x80000000~0x80100000会映射到虚拟地址0xC0000000~0xC0100000。当所有初始化完成后,使用如下代码跳转到C程序的入口函数start_kernel(),开始后续的内核初始化工作:bSYMBOL_NAME(start_kernel)。图2机器码不匹配错误3.Linux内核第二阶段启动stage2从start_kernel函数开始Linux内核第二阶段启动从start_kernel函数开始。start_kernel是所有Linux平台进入系统内核初始化后的入口函数。主要完成剩下的与硬件平台相关的初始化工作。在与内核相关的一系列初始化之后,它会调用第一个用户进程——init进程,并等待用户进程的执行,从而启动整个Linux内核。该函数位于init/main.c文件中,主要工作流程如图3所示:图3start_kernel流程图该函数所做的具体工作如下:1)调用setup_arch()函数进行相关配置对架构的初始化工作;这个函数对于不同的架构有不同的定义。对于ARM平台,这个函数定义在arch/arm/kernel/setup.c中。它首先通过检测到的处理器类型初始化处理器内核,然后通过bootmem_init()函数根据系统定义的meminfo结构初始化内存结构,最后调用paging_init()打开MMU,创建内核页表,并映射所有物理内存和IO空间。2)创建异常向量表,初始化中断处理函数;3)初始化系统核心进程调度器和时钟中断处理机制;4)初始化串口控制台(console_init);ARM-Linux一般都会初始化一个串口作为内核的控制台,但是串口Uart驱动把串口设备的名字写死了。比如本例中linux2.6.37的串口设备名是ttyO0,而不是常用的ttyS0。借助控制台,内核可以在启动过程中通过串口输出信息,方便开发者或用户了解系统的启动过程。5)创建并初始化系统缓存,为各种内存调用机制提供缓存,包括;动态内存分配、虚拟文件系统(VirtualFileSystem)和页面缓存。6)初始化内存管理,检测内存大小和内核占用的内存;7)初始化系统的进程间通信机制(IPC);当以上所有初始化工作完成后,start_kernel()函数会调用rest_init()函数进行第一次初始化,包括创建系统的第一个进程——init进程来结束内核的启动。挂载根文件系统并启动initLinux内核启动的下一个进程是启动第一个进程init,但是必须以根文件系统为载体,所以在启动init之前必须挂载根文件系统。4.挂载根文件系统根文件系统至少包括以下目录:/etc/:存放重要的配置文件。/bin/:存放开机时必须使用的常用可执行文件。/sbin/:存放开机过程中需要的系统执行文件。/lib/:存放/bin/和/sbin/执行文件所需的链接库,以及Linux内核模块。/dev/:存储设备文件。注意:5大目录必须存放在根文件系统中,一个都不能少。以只读方式挂载根文件系统。之所以以只读方式挂载根文件系统,是因为Linux内核还处于启动阶段,不是很稳定。如果以可读可写的方式挂载根文件系统,如果Linux不小心死机,可能会破坏根文件系统上的数据,Linux开机时需要很长时间检查和修复根文件系统下一次。挂载根文件系统有两个目的:一是安装合适的内核模块来驱动某些硬件设备或启用某些功能;另一种是启动保存在文件系统中的init服务,让init服务接管后续的启动工作。执行init服务Linux内核启动后的最后一个动作是从根文件系统中查找并执行init服务。Linux内核会按照以下顺序寻找init服务:1)/sbin/是否有init服务2)/etc/是否有init服务3)/bin/是否有init服务4)如果找不到***执行/bin/sh找到init服务后,Linux会让init服务负责后续初始化系统环境的工作。init启动后,表示系统已经成功启动linux内核。当init服务启动时,init服务会读取/etc/inittab文件,根据/etc/inittab中的设置数据初始化系统环境。/etc/inittab定义了init服务在linux启动过程中必须依次执行以下脚本:/etc/rc.d/rc.sysinit/etc/rc.d/rc/etc/rc.d/rc.local/etc/rc.d/rc.sysinit的主要作用是设置系统的基本环境。init服务执行rc.sysinit时,必须依次完成以下一系列任务:(1)启动udev(2)设置内核参数,执行sysctl–p,从/etc/sysctl中设置内核参数。conf(3)setthesystemtime设置硬件时间为系统时间(4)启用swap内存空间执行swpaon–a–e根据/etc/fstab的设置启用所有Swap内存空间。(5)检查并挂载所有文件系统检查所有需要挂载的文件系统,确保这些文件系统的完整性。检查后,以可读写的方式挂载文件系统。(6)初始化硬件设备除了在启动内核时用静态驱动驱动部分硬件外,Linux在执行rc.sysinit时也会尝试驱动其余的硬件设备。rc.sysinit驱动的硬件设备包括以下几项:a)/etc/modprobe.conf中定义的模块b)ISAPnP硬件设备c)USB设备(7)初始化串口设备Init服务将管理所有的串口Serialport调制解调器、不间断电源系统、串口控制台等设备。Init服务通过rc.sysinit对Linux串口设备进行初始化。当rc.sysinit发现linux可以在/etc/rc.serial时,就会执行/etc/rc.serial初始化所有的串口设备。因此,你可以在/etc/rc.serial中定义如何初始化linux中的所有串口设备。(8)清除过期的锁文件和IPC文件(9)建立用户界面在执行完三个主要的RC脚本之后,init服务的最后一个工作就是建立一个linux用户界面,让用户可以使用linux。此时init服务会执行以下两个任务(10)建立虚拟控制台init会在几个虚拟控制台中执行/bin/login,以便用户可以从虚拟控制台登录到linux。Linux默认在前6个虚拟控制台执行/bin/login登录程序,即tty1~tty6。当所有的初始化工作完成后,会调用cpu_idle()函数使系统空闲(idle),等待用户程序的执行。至此,整个Linux内核启动完毕。整个过程如图4所示。图4:linux内核启动和文件系统加载的全过程