我一直有一个疑惑——计算机到底是怎么启动的?
而且似乎有点矛盾——要想启动,就必须运行程序,但是计算机没开机怎么加载和运行程序呢?
为了解决这个矛盾,工程师终于想出将一小段程序固化进一块存储设备,电脑只要开机就会开始执行这段程序。
并且上节课中提到了BIOS、GRUB、BootLoader等概念,与之相类似的还有诸如UEFI、MBR、GPT等名词,这些名词都是跟计算机的启动过程有关的。它们都是在操作系统之前进行的,主要完成计算机的硬件检查、初始化、加载操作系统等**,**现在我们梳理一下它们的关系便于更好的理解操作系统。
按照时间线来说一共有两套启动流程,**一种是早期IBM PC时代的BIOS+MBR的启动方式(也称为Legacy方式),比较典型的如Windows XP/7;另一种是如今普遍采用的UEFI+GPT的启动方式,**从Windows8开始默认采用这种方式。
Legacy启动流程
Legacy方式(BIOS+MBR)的启动方式大致经历了这样的流程:
我们就顺着整个流程来讲解每个步骤所涉及的概念。
BIOS
BIOS是Basic Input and Output System的简称,即基本输入输出系统。它是一段被固化在主板ROM中的程序,掉电不会丢失,其主要功能是为电脑提供最基本的输入输出功能、开机硬件自动检测、设置、初始化。电脑按下开机之后,首先要做的就是加载并运行BIOS。
由于BIOS一般被固化在主板上(现在多用闪存存储),我们也将BIOS称为固件。我们开启计算机按下F12或Delete之类的按键,弹出的设置界面就是访问的BIOS进行设置:
更详细一点来说,BIOS主要有三个功能:
- 自检和初始化
也称为加电自检(Power On Self Test),检查电脑硬件是否良好,常见标准是检查CPU、640KB基本内存、1M以上扩展内存、ROW、主板、**CMOS存储器(CMOS RAM中存储着系统基本启动信息,是用来保存BIOS的配置和用户的设定参数,掉电不丢失数据)、**显示卡、键盘等进行测试,一旦发现故障,就停机或报警,状态良好就进入下一个步骤。
- 初始化中断相关服务
这个部分主要包括创建中断向量、设置寄存器等,值得注意的是在操作系统启动之后,BIOS还是继续存在的,提供硬件中断处理以及提供程序与IO设备的底层逻辑支持。不过最新的操作系统开始绕过BIOS提供的抽象接口,而选择直接与硬件进行交互。
- 加载引导程序
这个部分就是加载引导操作系统,如DOS、Windows、Linux等,BIOS会根据CMOS RAM(可以看成是保存BIOS配置的存储器)中对存储设备(硬盘、U盘等)的排序,依次对存储设备前512个字节进行检验,看其最后是不是以**0x55,0xAA结尾,**如果是就加载这512字节的代码,如果不是就检查下一个存储设备。
MBR与PBR
前面提到BIOS加载引导程序时会依次检验存储设备前512Bytes的内容**,如果以0x55 0xAA结尾,则这个512Bytes的内容就是MBR**,当BIOS找到MBR之后,就会将计算机的控制权交给MBR,并运行其中的程序。
MBR全称为Master Boot Record,译为**主引导记录,**是存在磁盘的第一个逻辑扇区LBA中(512Byte)的内容,这512Bytes根据功能又分为三个部分:
- **1-446Byte:**用于加载操作系统的机器码;
- 447-510Byte:MBR磁盘分区表(后讲);
- **511-512Byte:**即0x55和0xAA,魔数,用来表示其是MBR;
以一个四分区的磁盘为例,它的分区情况如下:
上图MBR后的1234就是所谓的磁盘分区表(DPT,Disk Partion Table),分区表又分为MBR分区表和GPT分区表,由于我们使用的是MBR引导,因此上图是MBR分区表,至于GPT分区表会在下个章节进行讲解。
分区表是用来将硬盘分区的,比如将笔记本上的一块1T硬盘分成了c、d、e、f,就是通过这个磁盘分区表来定义的。由于每个分区表中的分区只能占16Byte,因此最多只能构成4个分区(现在的扩展分区不止了,可百度)。分区表的中到底定义了什么内容呢?
稍微总结一下:
(1) 第1个字节:0x80,表示该主分区是激活分区,也就是操作系统所在的分区;0x00表示非活动分区。四个主分区里面只能有一个是激活分区(MBR的限制,现在的GPT则不是,后讲)
(2) 第2-4个字节:通过开始磁头、扇区、柱面来确定主分区第一个扇区的位置。
(3) 第5个字节:定义主分区类型。
(4) 第6-8个字节:通过结束磁头、扇区、柱面确定主分区最后一个扇区的物理位置。
(5) 第9-12字节:该分区第一个扇区的逻辑地址。
(6) 第13-16字节:该分区的扇区总数。
因此,当进入MBR代码后,通过查看MBR分区表中记录的活动分区是哪一个,就跳到对应的分区执行之后的操作系统加载。
等等,跳到对应的分区是什么意思?
上图是四个分区,假设我们的操作系统在分区1,那么MBR引导代码就会读取分区1最前面的PBR(Partion Boot Record,分区引导记录,也称为VBR——Volume boot record,即卷引导记录),PBR同样包含一段可运行的代码以及对应的操作系统引导程序所在的位置(上图红色部分)。
操作系统引导程序是指最终去加载操作系统的一段代码,不同的操作系统所使用的引导程序不同,在WindowsXP之前是NTLDR,WIndows7之后就采用bootmgr,Linux是采用Grub(grldr)。
因此对于Windows系统加载来说,系统启动流程如下:
总结一下,基于BIOS和MBR的方式操作系统引导流程为:BIOS -> MBR -> DPT -> PBR-> 寻找根目录下 NTLDR(XP)/bootmgr(WIN7/Vista)等可用于引导的程序。
GRUB
GRUB即是前文提到的“操作系统引导程序”的一种。其全称为Grand Unified Boot Loader,主要是用于类Unix操作系统的加载。它允许用户可以在计算机内同时拥有多个操作系统,并在计算机启动时选择希望运行的操作系统。
不过与bootmgr之类的系统不同的是,如果是采用GRUB引导程序,那么GRUB是安装在MBR及其之后的一部分非分区空间的,GRUB会去读取文件系统,找到其安装目录/boot/grub,并读取其中的grub.cfg,加载需要的模块,最后启动操作系统。下面是我们COMOS的grub.cfg:
//设置HelloOS是启动系统的第一个选项 menuentry 'COSMOS' { #加载part_msdos、ext2模块 #这是grub的语法 insmod part_msdos insmod ext2 set root='hd0' #只有一个硬盘,因此设置根目录为hd0(就是hd.vdi) multiboot2 /boot/COSMOSOS.eki #加载boot目录下的.eki内核文件 boot #加载启动内核文件 } #设置过时样式为菜单 set timeout_style=menu #设置过时时间 if [ "${timeout}" = 0 ]; then set timeout=10 #等待10秒钟自动启动 fi
加载模块命令insmod主要加载了两个模块:part_msdos与ext2。并且我们将根目录设置为hd0,即第一块硬盘,而指明操作系统内核文件在/boot/COSMOS.eki,最后执行boot命令开始加载操作系统。
part_msdos与ext2分别是分区模块和文件系统模块,加载这两个模块主要是为了让引导程序知道文件该去哪找,如果不加载这两个模块,它根本不懂/boot/COSMOS.eki是什么意思!
如果电脑中装了多个系统,那么只需要在上述grub.cfg文件中加入多个选择即可:
menuentry 'OS1' { ...... } menuentry 'OS2' { ...... }
下图是只有一个选项的GRUB启动界面:
UEFI启动流程
在介绍UEFI之前,先讲一下跟前文的GPT分区表。
GPT
GPT,全名Globally Unique Identifier Partion Table,即全局唯一标识分区表,又称为GUID分区表。它是UEFI规范的一部分。在之前介绍MBR时可知,MBR最多只能拥有4个分区,并且只能处理最大2T(2^(16+16)*512=2048GB=2TB)的硬盘,超过2T的硬盘就需要使用扩展分区。而GPT最大硬盘容量可达EB,且分区数量几乎无限制(Windows限定为128),来看看它的结构:
保护MBR即为PMBR,位于磁盘0号扇区,其主要作用是兼容MBR:GPT系统会自动跳过这个扇区,而只支持MBR的系统则会根据PMBR的内容,以Legacy的方式进行启动。
GPT头即GPT HDR,位于磁盘1号扇区,该扇区在创建GPT磁盘时生成,主要定义分区表起始、结束位置、分区表大小、个数等信息,一句话,就是定义分区的个数和位置。
分区表即Partion Table,当前Windows预留了32个扇区来设置分区表,一共可以容纳128个分区表,每个分区表大小为128Byte,每个分区表定义该分区的其实、结束位置。
分区区域:即根据分区表划分的存储区域,也是用户直接操作的地方。
分区表备份与GPT头备份,主要是用作损坏恢复,当首部GPTHDRR破坏时,可以根据尾部进行恢复,提高可靠性。
UEFI
UEFI,统一可扩展固件接口(英语:Unified Extensible Firmware Interface,缩写UEFI)是由intel牵头制定一种个人电脑系统规格,用来定义操作系统与系统固件之间的软件界面,作为BIOS的替代方案。
UEFI的启动过程相比BIOS更为简单:
上电;
UEFI固件从ROM中被加载,初始化硬件;
固件读取NVRAM(一个存储设备)中的引导管理器(配置信息),确定下一步需要加载的UEFI应用;
UEFI应用继续加载其他UEFI应用,最终通过读取.efi文件来加载操作系统;
上面所说efi文件是UEFI用来加载操作系统的UEFI应用,常见的.efi文件有bootmgfw.efi(Windows默认引导文件,引导文件位于ESP分区/EFI/Miscosoft/Boot/bootmgfw.efi)、grubx64.efi(Linux默认引导文件,使用grub引导)、bootx64.efi(默认路径为/EFI/Boot/bootx64.efi,当所有启动入口都不可用,才用它来引导系统)
注:还有两个名词,efibootmgr:linux中的EFI启动管理器,管理NVRAM启动入口以及顺序;NVRAM:掉电不丢失随机访问内存,是BIOS ROM中的一段区域,其中存储着EFI需要的变量。
与BIOS不同的是,UEFI启动的时候再也不需要去磁盘首部512B去加载MBR代码了,且开机更为迅速。
但其实,借助知乎大牛老狼的话来说:“
实际上PC的启动固件的引导流程从IBM PC机诞⽣第⼀天起,就没有本质改变过。如果我们透过SEC、PEI、DXE和BDS等等复杂的术语看幕后隐藏的本质,就会发现⽆论传统BIOS还是UEFI,阳光之下没有什么新鲜的东⻄,启动本身⽆外乎三个步骤: 1.Rom Stage:在这个阶段没有内存,需要在ROM上运⾏代码。这时因为没有内存,没有C语⾔运⾏需要的栈空间,开始往往是汇编语⾔,直接在ROM空间上运⾏。在找到个临时空间(Cache空间⽤作RAM,Cache As Ram, CAR)后,C语⾔终于可以粉墨登场了,后期⽤C语⾔初始化内存和为这个⽬的需要做的⼀切服务。 2. Ram Stage: 在经过 ROM阶段的困难情况后,我们终于有了可以⼤展拳脚的内存,很多额外需要⼤内存的东⻄可以开始运⾏了。在这时我们开始进⾏初始化芯⽚组、CPU、主板模块等等核⼼过程。 3. Find something to boot Stage: 终于要进⼊正题了,需要启动,我们找到启动设备。就要枚举设备,发现启动设备,并把启动设备之前需要依赖的节点统统打通。然后开始移交⼯作,Windows或者Linux的时代开始。 这就是传统BIOS和UEFI的启动过程,在剥去了术语后,主⼲的三个步骤从来没有变化过。”
的确,如果说BIOS是各个硬件厂商闭门造车阻止创新,那么UEFI就提供给了大家开放、创新的平台,使得UEFI能够更好的完成它的使命——初始化硬件和提供硬件的软件抽象(即接口),剩下的,就交给操作系统吧。
补充-文件系统
一直对硬盘的文件系统有点疑惑——为什么需要文件系统?
硬盘存储原理
在这之前我们先来看看机械硬盘的物理结构示意图(来自我是一块硬盘):
硬盘由多个层叠的盘面组成,每一个盘面上有一圈又一圈的磁道,而每一个磁道是由一个个的扇区(sector)组成。多个盘面同一垂直位置的磁道被称为一个柱面,就好似一个组成了一个圆柱体的表面。
主轴带动着所有盘面同时转动,而数据一般是一一个扇区为单位进行存储,旁边的读写头就负责读取/保存当前扇区的存储数据。读写头的原理是电磁感应,详细可参考:磁头原理。
当访问磁盘上数据时,磁盘需要做三件事:
找到存储数据的扇区所在柱面,这一步主要是读写头伸缩;
找到柱面上磁道上指定的扇区,这一步依靠主轴旋转;
读写头读取数据到内存;
三个步骤所消耗的时间分别称之为:寻道时间、旋转时间、传输时延。
从上文可以看出,我们是以柱面/磁道/扇区这三者来最终确定存储区域的,这种定位存储区域的方式被称为绝对扇区。
为了便于理解和存储,将这些扇区分为一个又一个的逻辑块LBA,每个逻辑块存储上限为512B,并依次编号0,1,2,3…n,你不用管怎么编号、怎么对应的,反正你就说需要访问4号块,磁盘自己就会呼呼呼的转过去把数据给你。
我们现在把存储在硬盘上的数据称为文件,一个文件其实就是占据了多个硬盘扇区的一个抽象的概念。
现在就有个问题了,我——硬盘——怎么去记录一个文件使用了哪些扇区呢?文件目录又是什么东西?
负责管理文件扇区、组织文件目录的功能正是文件系统需要做的事。
文件系统
前面说到,文件系统简单来说就是方便人们快捷使用硬盘存储所发明的东西,总不能我需要存储或者一些数据还得自己知道到底存在1号扇区还是9号扇区吧?
人生苦短,我不用知道,也不想知道,文件系统知道就可以了,我只需要知道如何使用文件系统新建、删除、访问一个文件就够了。
考虑到硬盘的随机、顺序读取速度以及硬盘空间的管理等问题,现在主流的文件系统采用索引式的方式来管理文件和扇区的映射关系。
索引式?啥玩意儿?看下图来自我是一块硬盘:
当需要访问文件A时,直接访问对应的16号的扇区,在16号扇区中存储着文件A所占据的所有扇区编号!不管你是想读取文件A的全部数据,还是部分数据,硬盘直接查这个“表”,就去对应的地方把数据传给你了。上述的16号扇区就是所谓的索引节点,称为inode。
其实不管是文件还是文件目录,本质是都是一样的东西,目录也有一个inode,举个例子来说:比如需要读取/tmp/test.log文件,查找次序是:根目录inode->根目录磁盘块->tmp的inode->tmp磁盘块->test.log的inode->test.log的磁盘块。示意图如下:
但是这种方式有个潜在的问题,来看看删除一个文件需要做什么:
将tmp从根目录磁盘块删除
将tmp的inode释放,这样其他文件才可用
将tmp磁盘块,这样其他文件才可以用
上面三个步骤要么都完成,要么都不完成,否则硬盘中有些扇区就永远被屏蔽了。为了解决这个问题,将所完成的操作记录在一个单独的文件中,这样即便出现错误,也知道哪一步没做完,继续做就好了,这就是所谓的日志式文件系统。
解决了如何访问文件对应扇区的问题,还有一个问题需要解决,那就是如何管理扇区?哪些是可用的扇区呢?
最简单也是最节省存储空间的做法——使用位图:
上图中,1表示对应的扇区已使用,0表示对应的扇区空闲可使用。
因此,以ext2文件系统为例,整个文件系统以及磁盘分区结构如下:
MBR前文已经说了,是用来运行bootloader从而加载操作系统的;
引导块是MBR格式每个分区的首部区域,用来存储操作系统(没有就空着);
而后每个分区又分为了多个块组,一个块组由多个块(block)构成,每一块中有相应的属性信息、inode位图、存储数据块,用作什么之前已经解释过了。
不过还需要补充一点,块是什么?
块,block,是Unix操作系统上的概念,Unix操作系统与磁盘打交道的最小单位是块(block),主要原因在于扇区的size太小,只有512B,大部分文件大小都远大于这个数,不便于管理。在Windows中称其为簇。
块/簇可以包含2、4、8、16…个扇区,具体多少由操作系统来确定,现在所使用的的一般是4K,即8个扇区。
注意,假设块的大小为4K,**而一个块只能存储一个文件,**这就会出现一个小文件的空间浪费。比如一个文件大小只有4700+K,但是占用空间却是8K,因为它占据了两个块。