上接博文《u-boot之u-boot-2009.11启动过程分析》linux
Linux内核启动及文件系统载入过程
缓存
当u-boot開始运行bootcmd命令,就进入Linux内核启动阶段。与u-boot相似,普通Linux内核的启动过程也可以分为两个阶段,但针对压缩了的内核如uImage就要包含内核自解压过程了。本文以linux-2.6.37版源代码为例分三个阶段来描写叙述内核启动全过程。第一阶段为内核自解压过程,第二阶段主要工做是设置ARM处理器工做模式、使能MMU、设置一级页表等,而第三阶段则主要为C代码,包含内核初始化的全部工做,如下是具体介绍。函数
/******************************************************************************************************************************************/ui
原创做品,转载时请务必以超连接形式标明文章原始出处:http://blog.csdn.net/gqb_driver/article/details/26954425,做者:gqb6661、Linux内核自解压过程编码
在linux内核启动过程当中通常能看到图1内核自解压界面,本小节本文重点讨论内核的自解压过程。spa
图1 解压内核.net
内核压缩和解压缩代码都在文件夹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是为支持不少其它的压缩格式而新引入的。指针
在BootLoader完毕系统的引导之后并将Linux内核调入内存以后,调用do_bootm_linux()。这个函数将跳转到kernel的起始位置。假设kernel没有被压缩。就可以启动了。假设kernel被压缩过。则要进行解压,在压缩过的kernel头部有解压程序。压缩过的kernel入口第一个文件源代码位置在arch/arm/boot/compressed/head.S。它将调用函数decompress_kernel()。这个函数在文件arch/arm/boot/compressed/misc.c中。decompress_kernel()又调用proc_decomp_setup(),arch_decomp_setup()进行设置,而后打印出信息“Uncompressing Linux...”后,调用gunzip()将内核放于指定的位置。rest
如下简介一下解压缩过程,也就是函数decompress_kernel实现的功能:解压缩代码位于kernel/lib/inflate.c,inflate.c是从gzip源程序中分离出来的,包括了一些对全局数据的直接引用。在使用时需要直接嵌入到代码中。gzip压缩文件时老是在前32K字节的范围内寻找反复的字符串进行编码, 在解压时需要一个至少为32K字节的解压缓冲区,它定义为window[WSIZE]。inflate.c使用get_byte()读取输入文件,它被定义成宏来提升效率。code
输入缓冲区指针必须定义为inptr,inflate.c中对之有减量操做。
inflate.c调用flush_window()来输出window缓冲区中的解压出的字节串。每次输出长度用outcnt变量表示。在flush_window()中,还必须对输出字节串计算CRC并且刷新crc变量。在调用gunzip()開始解压以前,调用makecrc()初始化CRC计算表。最后gunzip()返回0表示解压成功。咱们在内核启动的開始都会看到这种输出:
UncompressingLinux...done, booting the kernel.
这也是由decompress_kernel函数输出的。运行完解压过程。再返回到head.S中的583行,启动内核
call_kernel: bl cache_clean_flush bl cache_off mov r0, #0 @ must be zero mov r1, r7 @ restore architecture number mov r2, r8 @ restore atags pointer mov pc, r4 @ call kernel
当中r4中已经在head.S的第180行处预置为内核镜像的地址,例如如下代码:
#ifdef CONFIG_AUTO_ZRELADDR @determine final kernel image address mov r4, pc and r4, r4, #0xf8000000 add r4, r4, #TEXT_OFFSET #else ldr r4, =zreladdr #endif
这样就进入Linux内核的第一阶段,咱们也称之为stage1。
2、Linux内核启动第一阶段stage1
承接上文,这里因此说的第一阶段stage1就是内核解压完毕并出现Uncompressing Linux...done,booting the kernel.以后的阶段。该部分代码实现在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保存了与处理器相关的struct proc_info_list结构地址。
Head.S核心代码例如如下:
ENTRY(stext) setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @设置SVC模式关中断 mrc p15, 0, r9, c0, c0 @ 得到处理器ID。存入r9寄存器 bl __lookup_processor_type @ 返回值r5=procinfo r9=cpuid movs r10, r5 THUMB( it eq ) @ force fixup-able long branch encoding beq __error_p @假设返回值r5=0,则不支持当前处理器' bl __lookup_machine_type @ 调用函数,返回值r5=machinfo movs r8, r5 @ 假设返回值r5=0,则不支持当前机器(开发板) THUMB( it eq ) @ force fixup-able long branch encoding beq __error_a @ 机器码不匹配。转__error_a并打印错误信息 bl __vet_atags #ifdef CONFIG_SMP_ON_UP @ 假设是多核处理器进行对应设置 bl __fixup_smp #endif bl __create_page_tables @最后開始建立页表
检測机器码类型是在汇编子函数__lookup_machine_type (相同在文件head-common.S实现) 中完毕的。与__lookup_processor_type相似,经过代码:“bl __lookup_machine_type”来实现对它的调 用。该函数返回时,会将返回结构保存放在r五、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所看到的:
图3 start_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) 建立和初始化系统cache。为各类内存调用机制提供缓存。包含;动态内存分配,虚拟文件系统(VirtualFile System)及页缓存。
6) 初始化内存管理。检測内存大小及被内核占用的内存状况;
7) 初始化系统的进程间通讯机制(IPC); 当以上所有的初始化工做结束后,start_kernel()函数会调用rest_init()函数来进行最后的初始化,包含建立系统的第一个进程-init进程来结束内核的启动。
挂载根文件系统并启动init
Linux内核启动的下一过程是启动第一个进程init。但必须以根文件系统为载体,因此在启动init以前,还要挂载根文件系统。
4、挂载根文件系统
根文件系统至少包含下面文件夹:
/etc/:存储重要的配置文件。
/bin/:存储常用且开机时必须用到的运行文件。
/sbin/:存储着开机过程当中所需的系统运行文件。
/lib/:存储/bin/及/sbin/的运行文件所需的连接库,以及Linux的内核模块。
/dev/:存储设备文件。
注:五大文件夹必须存储在根文件系统上,缺一不可。
以仅仅读的方式挂载根文件系统。之因此採用仅仅读的方式挂载根文件系统是因为:此时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启动过程当中必须依序运行下面几个Script:
/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)设置系统时间
将硬件时间设置为系统时间
(4)启用交换内存空间
运行swpaon –a –e,以便依据/etc/fstab的设置启用所有的交换内存空间。
(5)检查并挂载所有文件系统
检查所有需要挂载的文件系统。以确保这些文件系统的完整性。
检查完成后以可读可写的方式挂载文件系统。
(6)初始化硬件设备
Linux除了在启动内核时以静态驱动程序驱动部分的硬件外。在运行rc.sysinit时,也会试着驱动剩余的硬件设备。rc.sysinit驱动的硬件设备包括下面几项:
a)定义在/etc/modprobe.conf的模块
b)ISA PnP的硬件设备
c)USB设备
(7)初始化串行port设备
Init服务会管理所有的串行port设备。比方调制解调器、不断电系统、串行port控制台等。Init服务则经过rc.sysinit来初始化linux的串行port设备。当rc.sysinit发现linux才干在这/etc/rc.serial时。才会运行/etc/rc.serial,借以初始化所有的串行port设备。
所以,你可以在/etc/rc.serial中定义怎样初始化linux所有的串行port设备。
(8)清除过时的锁定文件与IPC文件
(9)创建用户接口
在运行完3个基本的RC Script后,init服务的最后一个工做,就是创建linux的用户界面,好让用户可以使用linux。此时init服务会运行下面两项工做:
(10)创建虚拟控制台
Init会在若干个虚拟控制台中运行/bin/login。以便用户可以从虚拟控制台登录linux。linux默认在前6个虚拟控制台,也就是tty1~tty6,运行/bin/login登录程序。
当所有的初始化工做结束后,cpu_idle()函数会被调用来使系统处于闲置(idle)状态并等待用户程序的运行。至此。整个Linux内核启动完成。
整个过程见图4。
图4:linux内核启动及文件系统载入全过程