uefi启动linux过程_Linux系统的启动过程

启动过程分为三个部分

  1. 计算机硬件启动部分(BIOS)
  2. 引导加载程序启动部分(GRUB等)
  3. 内核启动部分(linux内核)

第一部分计算机硬件启动

开机上电时因为CPU中CS段描述符高速缓存器中的基址预置为0xFFFF0000,EIP的预置值是0x0000FFF0,所以处理器第一次取指令时发出的地址是0xFFFFFFF0,之所以这样是希望把BIOS放到4GB内存空间的最高端,这样4GB以下都是连续的RAM区域,给操作系统管理内存带来方便。

第一条指令是jmp far f000:e05b ,指令执行,因为是一个大跳,涉及到段选择子的改变,所以触发CPU更新段选择子CS的高速缓存器中的内容,由于处在16位实模式,所以处理器用0xF000的值左移4位(4个bit位,也就是一个16制位)变为0x000F0000,存到CS的高速缓存器中的段基址部分做为新基址。此时0x000FE05B处的指令是xor ax, ax 这是BIOS的第一条指令。这里为什么要从高端地址跳回到低端1MB地址再执行BIOS主要是为了兼容386以前的系统。

BIOS中的代码负责下面的事情:

  • 硬件检测,如果发现错误喇叭报警声示意。
  • 硬件初始化,先初始化显卡,再初始化其它设备,创建BIOS中断向量
  • 准备外围IO处理程序
  • 校对硬件配置是否发生变化,如果有变化则更新到CMOS中
  • 根据CMOS中记录的启动顺序,调用0x19号中断把启动盘的第一扇区(512字节)加载到内存的0x07C00到0x07E00中,然后跳转执行0x07c00处的代码,控制权移交给引导加载程序。

这里有几点说明

无论是在实模式还是保护模式,CPU在形成地址时都是用段选择子的不可见部分也就是高速缓存器中的基址加上EIP中的偏移来得到。只要段选择子不变,那么就不会触发CPU去改变CS高速缓存中的值。只是在实模中CPU会用CS选择子左移4位为填充高速缓存中的基址部分,其它部分是固定的。而在保护模式中CPU是要根据CS选择子的值去查全局描述符表,用查到的对应值去填充CS高速缓存。保护模式更精细一些。

关于A20地址线。这是一个历史遗留问题,A是address指地址线,A20就是第21根地址线(编号从0开始)。8086没有A20问题,因为它只有20根地址线,80286有24根,80386有32根。

8086实模式下的程序只能寻址1MB内存,段地址左移4位加上16位的偏移。而当逻辑段地址达到最大值0xFFFF时,再加上1就有可能绕回0x0000,因为段寄存器只能保留16位的结果,同样对于段内偏移也是一样的情况。看似bug,但很多程序员写程序故意利用了这个特性。

但是到了80286,地址线成了24位,cpu能操持24位的地址数据了。以前的地址回绕不灵了,IBM为了兼容以前的程序,使用了一个与门来控制第21根地址线,并把这个与门的控制阀门放在了键盘控制内,端口号是0x60。这实际就是强制A20默认为0。后面的地址进位都会被忽略,地址又会回绕了。但是这种控制方式在实现上和编程控制上都很烦琐。

从80486开始处理器本身就有了A20M#引脚(即A20屏蔽)。输入/输出控制器中心(ICH芯片)的处理器接口中有一个用于兼容老式设备的端口0x92。这个8位的端口第0位叫做INIT_NOW,向这个端口写1,处理器会复位,电脑重启。而这个端口的1位用于控制A20,它和来自键盘控制器的A20控制线组成一个或门,连接到处理器的A20M#引脚。和使用键盘控制线不同,使用0x92端口显得非常迅速,故称为FastA20。

处理器复位时,0x92端口的1位默认置1,也就是说A20地址线在计算机启动时是默认打开的。A20M#信号仅用于单处理器系统,多核处理器一般不用。

BIOS的缺点:性能差,不支持异步工作,安全性低,不支持硬盘2TB以上的地址引导。

于是出现了BIOS的替代者UEFI

UEFI的地位和BIOS是一样的,作用也是一样的,只是实现方式先进的多。UEFI采用C语言开发,不同于BIOS只能使用汇编开发。UEFI可扩展性好,每个驱动是一个独立的模块,可以包含在固件中也可以放在硬盘设备上,运行时动态加载,升级也方便。UEFI舍弃了中断这种比较耗时的操作外部设备的方式,改用为事件+异步操作。安全性也有很大提高。UEFI实现了一个系统表来代替中断向量,然后提供了一个软件运行环境,有自己的可执行文件格式(.efi文件),驱动格式等。虽然UEFI同BIOS有很多的差异,但是它们所实现的目的是一样的,这里就不细说了。有对细节感兴趣的同学可以百度相关知识研究。

计算机硬件启动部分是固定的,不管你的电脑上安装的什么系统,硬件的启动都是固定的模式,硬件自检初始化,然后从你在BIOS设置程序中指定的启动顺序加载操作系统引导加载程序,进入第二部分的启动。

第二部分引导加载程序

操作系统引导加载程序启动这部分主要是一个过渡,具体的引导加载程序有很多种,grub、syslinux、bootmgr等等。这部分在磁盘上,BIOS或UEFI会把这部分加载到内存中,然后执行这部分程序。这部分的功能是为了加载更大的操作系统内核并传递参数给内核。

在此以GRUB为例进行说明

引导装载程序要使用BIOS或UEFI的中断或驱动来访问设备,等到内核自己运行时便去掉了这些低性能的中断和驱动,内核使用的是自己的高性能驱动。

上节我们说到,BIOS会把硬盘第一扇区调入内存运行,对于grub来说是哪一部分呢。

这里我们要先介绍一下硬盘前几个位置的结构,DOS的系统映像是不能跨柱面存放的,所以在DOS时代,磁盘的第一个分区索性并没有紧接在MBR的后面,而是从下一个柱面开始的。这对于现代操作系统同样适用。于是在MBR与第一个分区之间,就出现了一块空闲区域。从那时起这种分区方式成为了一种约定俗成,基本上所有的分区工具都把这种分区方式保留了下来。按照默认的每个磁道63个扇区,硬盘的第一个分区起始于第63个扇区(编号从0开始),也就是说对于第0磁道,除了MBR占用了一个扇区,其余62个扇区是空闲的(31KB)。于是这一块区域成了很多引导加载程序的憩息地。

当使用grub-install /dev/sda来把grub安装到硬盘时。安装程序会把grub的映像写入硬盘,其中在MBR之后的62个扇区因为只有31K空间,为了控制嵌入到这个区域中的映像尺寸不超过31K,grub采用了模块化的设计方案。

Grub在嵌入的映像中包含硬件及文件系统的驱动,因此,一旦嵌入的映像载入内存,grub即可访问文件系统,其它的模块完全可以从文件系统中加载,以此控制映像尺寸。所以grub把映像分成了三个部分:

  1. 安装到MBR中的boot.img
  2. 嵌入到31K空扇区的core.img
  3. 存储在文件系统中的其它模块

BIOS会把MBR的第一扇区调入内存并执行,在这里便是把boot.img调入了内存执行。

Boot.img的主要功能是将core.img中的第一个扇区载入内存,只能载入一个扇区是因为MBR的512字节,除去64字节的分区表以及最后2个字节的引导标识,还要给BIOS保留一段参数空间,boot.img可用的空间已经很少了,索性boot.img中仅记录core.img的第一个扇区号,并仅将这个扇区中的内容加载到内存。

Boot.img只能利用BIOS提供的中断向量0x13来读取扇区。在grub的源码中我们看不到分区表的部分,因为在安装时,安装程序负责将分区表写到boot.img中,然后再整体把boot.img写入硬盘第一个扇区。

此时boot.img将core.img的第一扇区加载到了内存,在往下讲之前我们先看一下core.img的整体结构。

Core.img包括多个映像和模块,以硬盘启动为例,core.img包含以下部分:

  • Diskboot.img:占用一个扇区,也就是boot.img加载的那部分。
  • Lzma_decompress.img:未压缩的解压程序代码。
  • Kernel.img、Biosdisk.mod、Part_msdos.mod、Ext2.mod、othermods这几部分是压缩的。

其中diskboot.img会加载余下的部分并转交控制权,使用的同样是bios中断来加载扇区。

为了控制core.img的体积,grub将它进行了压缩,显然diskboot.img是不能压缩的。lzma_decompress.img是用来解压的代码,使用的是lzma算法,从grub2.0开始,只能使用lzma这种压缩算法了。

Kernel.img是grub的核心代码,其中包括了为底层具体的磁盘驱动以及文件系统驱动提供公共的服务层。

后面的磁盘驱动模块biosdisk.mod、MBR分区模式模块part_msdos.mod、以及文件系统的驱动模块ext2.mod(虽然名字是ext2但是这个模块支持EXT系列文件系统)。这些模块的用途是驱动对应磁盘类型上的文件系统。

从这以后,grub就支持文件系统了,访问磁盘不需要再依靠BIOS的中断向量以扇区为单位读取了。后继的读取操作都是直接开始使用文件系统接口。

于是在我们使用命令安装grub到磁盘时,首先安装程序要创建core.img,grub对应的工具是grub-mkimage。然后安装程序把boot.img和core.img写入磁盘,对应的工具是grub-setup。为了使用方便,grub把这两个工具封装成了grub-install脚本。

在创建core.img的时候,grub-mkimage需要生成core.img的每个组成部分。Grub也提供了相应了工具grub-probe。利用这个工具自动探测目标介质所需要的对应模块。

各模块生成后,grub-setup将会把boot.img安装到MBR,然后把core.img装入后面的扇区。

BIOS把控制权移交给grub后,kernel.img的grub_main函数会调用grub_load_modules函数装配模块,然后接着调用grub_load_normal_mode加载normal模块,这个模块拉开了加载Linux内核和initramfs的大幕。

Normal模块读取解析grub的配置文件grub.cfg,然后根据这个文件中的具体命令,加载相应的模块。命令和模块的对应关系(也就是命令所在的模块名)记录在文件command.lst中,这个文件一般在/boot/grub/i386-pc目录下,normal模块加载时也会加载这个文件。

比如,在配置文件中遇到linux、initrd这几个命令时,normal模块会查command.lst文件,然后发现这两个命令是在linux模块中,然后normal模块会加载linux模块到内存中,调用linux模块中的linux和initrd命令加载linux内核以及initramfs。

引导加载程序和内核之间需要沟通一些数据,比如引导加载程序需要知道内核想加载到什么位置,内核映像是不是可重定位。而内核也需要知道引导加载程序把initramfs加载到了内存的什么位置,initramfs的大小是多少。引导程序和内核之间沟通数据的方式叫做引导协议。原来16位的引导协议约定了引导加载程序和内核之间分享的数据存储的位置、大小以及分别由谁来提供,因为是16位协议,所以CPU保护模式的切换是在内核中实施的,内核中有一部分实模式代码(也就是setup.bin,俗说的内核零页)。

随着BIOS的发展,如EFI、LinuxBIOS的出现,现在普遍使用的是32位引导协议,对比16位引导协议,CPU的保护模式切换是在引导加载程序中进行的,这样引导加载程序加载完内核后不再需要跳到内核的实模式部分,而直接路到内核的保护模式部分就可以了。

内核中引导协议相关部分的代码在arch/x86/boot/header.S中。

内核会在这个文件中标明自己的对齐要求、是否可以重定位以及希望的加载地址等信息。这个文件中还会留有空位,由引导加载程序在加载内核时填充,比如initramfs的加载位置和大小等信息。

引导加载程序和内核均为此定义了一个结构体linux_kernel_params,称为引导参数。

Grub会在把控制权移交给内核之前填充好这个结构体,除了使用从内核中读取的信息,比如内核希望加载的地址等,也将实际加载的情况(比如initramfs的位置)填充到这里面。如果用户要通过grub向内核传递启动参数,即grub.cfg中linux后面的命令行参数。Grub也会把这部分信息关联到引导参数结构体中。

这样grub使用linux命令加载了内核,使用initrd命令加载了initramfs,并填充好了引导参数结构体。

至此,grub完成了其作为操作系统引导加载程序的使命。下面将跳转到加载的内核映像去执行,将控制权交给内核。

第三部分内核启动部分

Grub把控制权移交给内核后,系统进入到内核执行阶段。

现在我们要回过头来先看一下内核的结构和组成。我们可以把内核文件切分为三部分:

  1. Setup.bin内核的实模式部分
  2. 内核的非压缩部分
  3. 内核核心部分vmlinux

Setup.bin曾经用于将cpu切换到保护模式,并跳转到内核的保护模式部分。后来这部分功能被grub这类的引导加载程序取代了,再后来到了32位启动协议时代,setup.bin的收集启动信息功能也由grub这类的引导加载程序完成了。Setup.bin现在只被用于在引导加载程序和内核间传递信息了。例如,在加载内核时,引导加载程序从setup.bin中获取内核是否可重定位、对齐要求、建议的加载地址等信息。在构建映像时,内核构建系统需要将这些信息写到setup.bin中的一个结构体里。所以,虽然setup.bin已经失去了其以往的大部分作用,但还不能完全放弃,它还要做为内核与引导加载程序之间传递数据的桥梁,而且还要照顾到一些不能用32位启动协议的老机器。

内核的保护模式部分是经过压缩的,因此运行前要解压。所以在内核的压缩映像外围,有一段非压缩部分,这部分负责解压内核的压缩部分,并且负责内核的重定位。内核可以配置为可重定位的(relocateable),所谓可重定位是指内核可以被引导加载程序加载到内存的任何位置,但是在链接内核时,链接器需要假定一个加载地址为参考,为各个符号分配运行时地址。显然,如果加载和链接假定的地址不相同,那么需要对符号的地址进行重新修订,这就是内核重定位。

Vmlinux是内核的核心部分。在编译内核时,kbuild分别构建各个内核源码目录中的目标文件,然后把它们链接成vmlinux。为了缩小体积,kbuild删除了vmlinux中的一些不必要的信息,并将其命名为vmlinux.bin,最后把vmlinux.bin压缩为vmlinux.bin.gz。在linux2.6.26之前内核的压缩部分都是裸二进制文件(代码和数据按顺序依次存放)压缩的。从2.6.26开始内核的压缩部分采用了ELF格式再压缩的。这样的话核心部分的开头不是cpu可执行的机器指令了,变成了elf文件的文件头。现在内核的核心不是裸二进制了所我们需要一个ELF的加载器来加载这个ELF核心。这个加载器现在在内核的非压缩部分。这部分中有一个parse_elf函数负责将ELF格式的内核转换为裸二进制形式。

最终kbuild把vmlinux.bin.gz前面加上非压缩部分重命名回vmlinux.bin,再把这个新的vmlinux.bin和setup.bin拼装到一起组成bzImage,也就是我们看到的内核文件。

接下来我看一下initramfs文件的组成。

Initramfs是一个临时的文件系统,其中包括了必要的设备如硬盘、网卡、文件系统等的驱动程序以及加载这些驱动的工具和运行环境,如基本的C库、动态库的链接加载器等等。由引导加载程序(如grub)负责将initramfs加载到内存中。以驱动硬盘为例,内核就不必再从硬盘加载硬盘驱动了(解决循依赖问题),而是可以从已经加载到内存中的initramfs中获取硬盘控制器等相关驱动,继而可以驱动硬盘,访问硬盘上的根文件系统。

当然,我们也可以把所有内核需要的驱动编译进内核,这样的话我们就不需要initramfs了。但是通常我们不会这么做。这样在做内核升级的时候很麻烦。

在linux2.4及以前,内核使用的是initrd,不是initramfs。Initrd是基于ramdisk技术的,而ramdisk就是一个基于内存的块设备,因为是块设备,所以容量是固定的,不能动态调整。还要按照一定的文件系统格式进行组织,因此制作initrd时需要使用如mke2fs这样的工具格式化initrd,访问initrd时需要通过文件系统驱动。更重要的,虽然initrd是一个伪设备,但是从内核角度看它与真实的块设备并无区别,因此,内核访问initrd也需使用缓存机制,显然这是多此一举,因为本身initrd就在内存中。

后来Linus Torvalds把这个点给优化了,他基于已有的缓存机制实现了ramfs。Ramfs与ramdisk有着本质的区别,ramdisk本质上是基于内存的一个块设备,而ramfs是基于缓存的一个文件系统。有文件系统和缓存两方面的优点,动态申缩和访问非常方便快速。再后来开发人员基于ramfs开发了initramfs替代initrd。

内核在挂载真正的根文件系统之前,首先将挂载一个名为rootfs的文件系统,作为虚拟文件系统目录树的总根。因为rootfs是在内存中的,内核不需要特殊的驱动就可直接挂载,这样就有了文件系统和根了,内核可以在真正的根文件系统挂载进来之前提前做一些工作。

事实上,虚拟文件系统这棵代表整个文件系统的大树的根对用户并不可见,我们平在进程中所见到的根目录,仅是这棵树上的一个分支而已,因此我们看到内核中文件系统中有namespace的概念,就是每个进程都有属于自己的文件系统空间,现实中多数进程的文件系统的namespace都是相同的。进程在任务结构体task_struct中的fs_struct中记录进程的文件系统的根,也就是进程的namespace,init_mount_tree调用set_fs_root就是这个目的。

挂载了rootfs以后,内核将引导加载程序(grub)加载到内存中的initramfs中打包的文件解压到rootfs中,而这些文件中包含了驱动以及挂载真正的根文件系统的工具。等待内核挂载完真正的根文件系统之后,rootfs也就完成了使命,被真正的根文件系统覆盖,但是rootfs作为虚拟文件系统目录树的总根,并不被卸载。当然因为rootfs基于ramfs,生长在缓存中,只要其中的文件删除了空间自然也就释放了。

最后还要说一点,事实上,即使配置内核不支持initramfs,内核在内部依然会构建一个最小的initramfs。那么内核为什么要这么做呢?因为虽然有了rootfs做为根,但是如果第一个进程打不开控制台设备(/dev/console)的话将异常终止,最终导致内核panic。

内核的核心代码开始接过控制权后,将进行内核的初始化。建立虚拟内存管理结构,比如页表,页目录,全局描述符表等等。

然后初始化进程0,因为此时没有进程可以fork,内核采用静态创建的方式创建了进程0。以后的进程都将会以这个进程号为0的进程为模板,在没有其它任何进程可运行时,进程0将开始运行,所以其又被称作idle进程。

内核初始化的最后,将调用kernel_thread函数通过复制进程0创建进程1,从这个进程开始进入用户空间,执行的程序文件是根文件系统中的/sbin/init。

Init是linux上的一个用户空间程序。它主要负责启动和终止系统中的基础服务进程。

Linux系统中init一版有两种主要的实现

  • System V init:这是传统的init
  • Systemd:新出现的init。这是未来的趋势。

到此为止,系统的启动过程结束。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值