Linux 内核启动及文件系统加载过程

2019-07-13 06:03发布

 转自:http://www.tuicool.com/articles/NzAb2i
 u-boot 开始执行 bootcmd 命令,就进入 Linux 内核启动阶段,与 u-boot 类似,普通 Linux 内核的启动过程也可以分为两个阶段,但针对压缩了的内核如 uImage 就要包括内核自解压过程了。本文以项目中使用的 linux-2.6.37 版源码为例分三个阶段来描述内核启动全过程。第一阶段为内核自解压过程,第二阶段主要工作是设置ARM处理器工作模式、使能 MMU 、设置一级页表等,而第三阶段则主要为C代码,包括内核初始化的全部工作,下面是详细介绍。 /******************************************************************************************************************************************/
/******************************************************************************************************************************************/ 

一、 Linux 内核自解压过程  linux 内核启动过程中一般能看到图1内核自解压界面,本小节本文重点讨论内核的自解压过程。                                                        图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 是为支持更多的压缩格式而新引入的。  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() 将内核放于指定的位置。 下面简单介绍一下解压缩过程,也就是函数 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() 来输出 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  二、 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 ”来实现对它的调 用。该函数返回时,会将返回结构保存放在 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 机器码不匹配错误 三、 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 之前,还要挂载根文件系统。 四、挂载根文件系统 根文件系统至少包括以下目录:   /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时,也会试着驱动剩余的硬件设备。 r c.sysinit 驱动的硬件设备包含以下几项:   a)定义在 /etc/modprobe.conf 的模块   b) ISA PnP 的硬件设备   c) USB 设备 (7)初始化串行端口设备   Init 服务会管理所有的串行端口设备,比如调制解调器、不断电系统、串行端口控制台等。 Init 服务则通过 rc.sysinit 来初始化 linux 的串行端口设备。当 rc.sysinit 发现linux 才能在这 /etc/rc.serial 时,才会执行 /etc/rc.serial ,借以初始化所有的串行端口设备。因此,你可以在 /etc/rc.serial 中定义如何初始化 linux 所有的串行端口设备。 (8)清除过期的锁定文件与IPC文件 (9)建立用户接口 在执行完3个主要的 RC Script 后, ini t服务的最后一个工作,就是建立 linux 的用户界面,好让用户可以使用 linux 。此时 init 服务会执行以下两项工作: (10)建立虚拟控制台   Init 会在若干个虚拟控制台中执行 /bin/login ,以便用户可以从虚拟控制台登陆 linux  linux 默认在前6个虚拟控制台,也就是 tty1~tty6 ,执行 /bin/login 登陆程序。当所有的初始化工作结束后, cpu_idle() 函数会被调用来使系统处于闲置( idle )状态并等待用户程序的执行。至此,整个 Linux 内核启动完毕。整个过程见图4。                                  图4:linux内核启动及文件系统加载全过程