Linux 2.6 内核引导过程分析
上一篇文章介绍了Linux内核的编译过程,接下来本文利用生成的内核,介绍Linux的内核的引导过程。
本文以x86为例,x86与嵌入式系统的区别在于多了一个BIOS转移到BootLoader的过程。
Linux内核引导的过程包含多个阶段,接下来将依次解读:
1. 系统加电阶段 1:
系统加电时,处理器会执行一个位于已知位置处的代码。PC中即我们熟知的BIOS,它保存在主板的闪存中。
BIOS从0xFFFF0处开始执行,首先执行POST(加电自检),检查系统必备的引导设备是否存在,如内存/磁盘等硬件设备。然后BIOS进行本地设备的枚举和初始化。BIOS由两部分组成:POST代码和运行时服务,POST完成后会被清出内存,而运行时服务会驻留于内存供目标操作系统调用服务。
然后BIOS读取CMOS中的引导设备信息来搜索处于活动状态的可引导设备(可以是软盘,CD-ROM,硬盘上某一分区等),从引导设备中读取第一个扇区,该扇区包含着引导程序。由上图可知引导包含两个阶段。
当找到第一个引导设备后,第一阶段的引导程序就会被装载入RAM并执行,该引导程序大小一般小于一个扇区,其作用是加载第二阶段的引导加载程序,MBR就是熟知的第一阶段引导加载器。通常,Linux都是从硬盘上引导的,其中主引导目录(MBR)中包含主引导加载程序。MBR 是一个 512 字节大小的扇区,位于磁盘上的第一个扇区中(0 道 0 柱面1 扇区)。当 MBR 被加载到 RAM 中之后,BIOS 就会将控制权交给 MBR。
当第二阶段的引导加载程序被装入 RAM 并执行时,通常会显示一个动画屏幕,并将 Linux 和一个可选的初始 RAM磁盘(临时根文件系统)加载到内存中。在加载映像时,第二阶段的引导加载程序就会将控制权交给内核映像,然后内核就可以进行解压和初始化了。在这个阶段 中,第二阶段的引导加载程序会检测系统硬件、枚举系统链接的硬件设备、挂载根设备,然后加载必要的内核模块。
在引导程序GRUB被BIOS装入到内存并获得控制权后,读取/boot/grub.conf中的引导列表选择默认要启动的内核镜像bzImage。根据bzImage中前512byte的bootsect中的setup,vmLinux.bin所占扇区的大小,以及内核是否为大内核镜像将bootsect->0x00090000, setup->0x00090020, vmLinux.bin -> 0x00100000(big) 0x00010000(else)。
将内核搬到相应的地址后,引导程序跳转到物理地址0x00090020,即setup所在的物理地址,开始执行setup代码。
接下来会详细介绍下两阶段的引导过程及有关GRUB的知识。
1) 第一阶段引导加载程序
MBR 中的主引导加载程序是一个 512 字节大小的映像,其中包含程序代码和一个小分区表(参见图 2)。前446个字节是主引导加载程序,其中包含可执行代码和错误消息文本。接下来的 64 个字节是分区表,其中包含 4 个分区的记录(每个记录的大小是 16 个字节)。MBR 以两个特殊数字的字节(0xAA55)结束。这个数字会用来进行MBR的有效性检查。
主引导加载程序的工作是查找并加载次引导加载程序(第二阶段)。它是通过在分区表中查找一个活动分区来实现这种功能的。当找到一个活动分区时,它会扫描分 区表中的其他分区,以确保它们都不是活动的。当这个过程验证完成之后,就将活动分区的引导记录从这个设备中读入 RAM 中并执行它。
2) 第二阶段引导加载程序
次引导加载程序(第二阶段引导加载程序)可以更形象地称为内核加载程序。这个阶段的任务是加载 Linux内核和可选的初始 RAM 磁盘。
GRUB有关的信息:
在 x86 PC 环境中,第一阶段和第二阶段的引导加载程序一起称为 Linux Loader(LILO)或 GRand Unified Bootloader(GRUB)。由于 LILO 有一些缺点,而 GRUB 克服了这些缺点,因此下面让我们就来看一下 GRUB。
关于 GRUB,很好的一件事情是它包含了有关 Linux 文件系统的知识。GRUB 不像 LILO 一样使用裸扇区,而是可以从 ext2或 ext3 文件系统中加载 Linux 内核。它是通过将两阶段的引导加载程序转换成三阶段的引导加载程序来实现这项功能的。阶段 1 (MBR)引导了一个阶段 1.5 的引导加载程序,它可以理解包含 Linux 内核映像的特殊文件系统。这方面的例子包括reiserfs_stage1_5(要从 Reiser 日志文件系统上进行加载)或 e2fs_stage1_5(要从 ext2 或 ext3 文件系统上进行加载)。当阶段 1.5 的引导加载程序被加载并运行时,阶段 2 的引导加载程序就可以进行加载了。当阶段 2 加载之后,GRUB 就可以在请求时显示可用内核列表(在 /etc/grub.conf 中进行定义,同时还有几个软符号链接/etc/grub/menu.lst 和 /etc/grub.conf)。我们可以选择内核甚至修改附加内核参数。另外,我们也可以使用一个命令行的shell 对引导过程进行高级手工控制。将第二阶段的引导加载程序加载到内存中之后,就可以对文件系统进行查询了,并将默认的内核映像和 initrd 映像加载到内存中。当这些映像文件准备好之后,阶段 2 的引导加载程序就可以调用内核映像了。
2. 阶段1 ~ 阶段2:
该阶段由文件seup.S生成的代码块进行系统设备的探索,将探测到的相关信息保存到内存地址中,在系统最终初始化时由系统使用。探测完成后跳转到物理地址0x00100000,开始执行由/src/arch/i386/compressed/head.S生成的代码块。
3. 阶段2 ~ 阶段3:
该阶段由head.S生成的代码块执行,0x00100000处及startup_32处,该汇编代码为调用C语言函数设置相应的参数及栈指针后调用C函数decompress_kernel对压缩的内核进行解压缩,并将解压的内核片段放置到两个不同物理内存区域中,然后head.s中复制move,搬移的代码段移动到物理地址0x00010000处。然后跳转到该地址,执行该代码块。
4. 阶段3 ~ 阶段4:
该阶段执行复制到物理地址0x00010000的搬迁代码,该部分的搬迁代码将位于两个物理区域的解压缩内核代码片段移动到物理地址0x00100000.
5. 阶段4 ~ 阶段5:
执行完move后,跳到0x00100000,执行src/arch/i386/kernel/head.S中的startup_32的代码段,注意与上面startup_32的不同,开始执行系统核心的初始化。
6. 阶段5 ~ 系统完全启动:
在startup_32函数(也就是系统初始化进程init_task,也叫0号进程,idle进程)中。该进程完成了系统内存管理子系统/进程管理子系统,中断异常子系统,时间度量子系统等初始化工作.会对页表进行初始化,并启用内存分页功能,为FPU检测CPU类型,调用start_kernel函数,执行与体系结构无关的Linux内核部分.
通过调用 start_kernel,会调用一系列初始化函数来设置中断,执行进一步的内存配置,并加载初始 RAM 磁盘。
注:在内核引导过程中,初始 RAM 磁盘(initrd)是由阶段 2 引导加载程序加载到内存中的,它会被复制到RAM中并挂载到系统上。这个initrd会作为RAM中的临时根文件系统使用,并允许内核在没有挂载任何物理磁盘的情况下完整地实现引导。由于与外围设备进行交互所需要的模块可能是initrd的一部分,因此内核可以非常小,但是仍然需要支持大量可能的硬件配置。在内核引导之后,就可以正式装备根文件系统了(通过 pivot_root):此时会将 initrd 根文件系统卸载掉,并挂载真正的根文件系统。initrd 函数让我们可以创建一个小型的 Linux内核,其中包括作为可加载模块编译的驱动程序。这些可加载的模块为内核提供了访问磁盘和磁盘上的文件系统的方法,并为其他硬件提供了驱动程序。由于根文件系统是磁盘上的一个文件系统,因此 initrd函数会提供一种启动方法来获得对磁盘的访问,并挂载真正的根文件系统。在一个没有硬盘的嵌入式环境中,initrd 可以是最终的根文件系统,或者也可以通过网络文件系统(NFS)来挂载最终的根文件系统。
最后调用kernel_thread()创建一个名为init的内核线程,然后0号进程调用调度器schedule(),释放了处理器的使用权,成为了系统的idle进程。
在内核线程init获得处理器后,首先完成对称多处理器系统中应用处理器的初始化;然后挂载系统的根文件系统,完成系统总线/网络协议栈等的初始化;最后通过调用execve()开始执行用户态程序/sbin/init,此时内核线程init转换成了用户进程。/sbin/init是系统中所有进程(除idle进程)的祖先,它首先读取系统的配置文件/etc/inittab,根据该配置文件完成系统的最终初始化.
这篇文章也是综合了我看到的多个资料,每个资料都或多或少的有些遗漏,所以综合起来,希望能全面。
接下来,我将介绍些有关kernel其他方面的知识。