本节以CentOS 8启动为例,说明计算机上电后Linux的启动过程。了解Linux的启动过程有两个直接的好处:Linux运行时出错时,可以很方便地定位是在哪个阶段出的问题;如果需要在Linux启动运行过程中加入自己的操作,如增加开机自启动等,可以很清楚地知道应该在哪个阶段加入。
CentOS 8安装在x86架构CPU的计算机上,如无特殊说明,本书所涉及的计算机都是x86架构的计算机。
如图1-4所示,计算机上电后的启动顺序包括4个步骤:基本输入输出系统(Base Input &Output System,BIOS)自检、系统引导、内核运行和初始化程序运行。
图1-4 x86架构下Linux系统启动过程图
BIOS有两种系统引导方式,第一种是传统方式;第二种是基于统一的可扩展固件接口(Unified Extensible Firmware Interface,UEFI)方式。UEFI相对较新,且需要主板支持。本书所使用的VMware Workstation 9创建的虚拟机BIOS中只支持传统方式,因此,本书以传统方式为例进行讲解。
BIOS是固化在计算机主板芯片上的程序。BIOS自检主要完成计算机硬件的检测和初始化,并且提供硬件配置界面,如修改BIOS设置,更改开机的引导顺序、查看硬件信息、设置系统时钟或者设置各种外设接口等。按〈Delete〉键或者根据开机的屏幕提示信息(VMware中是按〈F2〉键),可以进入BIOS设置界面。因为BIOS属于计算机自带程序,因此把BIOS自检归类到计算机启动过程,而不归类到Linux启动过程。
BIOS存储在ROM之中,该ROM是可以直接被CPU寻址的(如ARM中Norflash),因此,BIOS程序不需要加载到内存,而是直接在ROM中执行。有关BIOS的执行过程,可以参考下面的两篇文章。
1)https://blog.csdn.net/dahuichen/article/details/53183836。
2)http://www.360doc.com/content/06/0810/13/561_177979.shtml。
Linux启动过程如图1-4中所示,共分三个阶段: 系统引导 、 内核运行 和 初始化程序运行 ,具体描述如下。
由于CPU架构或者Linux发行版不同,因此,系统引导和初始化程序运行阶段所采用的程序和版本可能会不同,内核运行阶段所采用的Linux内核版本也可能会不同。例如CentOS 8在系统引导阶段采用的程序是GRUB2,而之前的CentOS版本采用的则可能是GRUB1;CentOS 8在内核运行阶段采用的Linux内核版本是4.18.0,而之前的CentOS版本则采用更低版本的Linux内核;CentOS 8在初始化程序运行阶段采用的初始化程序是systemd,而之前的CentOS版本则可能采用init程序。
如图1-5所示,系统引导分为3个阶段:初始准备阶段,主要是读入引导程序,为引导程序的运行做准备;内核启动前的配置和管理阶段,例如提供双系统或多内核的选择菜单、配置内核启动参数和修复引导程序等;加载内核阶段,包括准备加载的环境,找到内核,最后加载内核,将控制权交给内核。
图1-5 系统引导阶段过程
(1)初始准备阶段
BIOS自检后,会根据引导顺序的设置,找到第一个可以引导的设备。如果这个引导设备是硬盘/U盘(统称为启动盘)的话,BIOS会将启动盘的第一个扇区(主引导扇区)加载到内存的0x07c00处,跳转到该地址执行。主引导扇区的大小只有512字节,结构如图1-6所示。主引导扇区的前446字节为主引导记录(Master Boot Record,MBR),即引导程序的代码,接下来的64字节为分区表,最后两个字节是结束标识,具体值为0xaa55。
图1-6 主引导扇区结构图
MBR的大小只有446字节,不可能实现复杂的功能。它的主要任务是,读取GRUB2核心程序的第一个扇区到内存,并执行该扇区的代码。
MBR主要来源于/boot/grub2/i386-pc/目录下的boot.img文件。GRUB2安装时,会读取boot.img的内容,并将其写入MBR。
GRUB2核心程序的内容,主要来源于/boot/grub2/i386-pc/目录下的core.img文件。
MBR体积有限,无法解析文件系统,因此,它不能通过文件系统接口读取GRUB2核心程序,只能按扇区去读取内容。那么GRUB2核心程序又存储在哪个扇区呢?
一般来说,在主引导扇区和硬盘的第一个分区之间,还有一块小小的空闲区域(MBR gap,又称保留扇区)。如图1-7所示,MBR gap有62个扇区,共计31744字节。GRUB2核心程序(core.img)就从MBR gap的第一个扇区开始依次存储,因为core.img的大小为30385字节,小于MBR gap的31744字节,因此空间是足够的。
图1-7 主引导扇区结构图
CentOS 8安装GRUB2时,会将MBR gap第一个扇区的位置硬编码到MBR中。MBR会从该位置开始读取一个扇区的内容(主要来源于core.img的前512字节)到内存并执行,此扇区代码会将MBR gap中core.img的剩余部分读入内存,由于文件系统尚不可用,因此它使用Block List格式对core.img的存储位置进行编码。
和MBR相比,core.img功能强大,它包含了文件系统(如Ext4)的驱动,因此core.img可以按照文件系统的接口来访问指定路径下的文件,而不用关心文件的存储是如何实现的。core.img访问的文件中,最典型的有两类:一类是内核文件,Linux内核文件vmlinuz-4.18.0-193.el8.x86_64存储在/dev/sda1上,文件系统是Ext4,挂载点是/boot,因此,core.img就可以用路径boot/vmlinuz-4.18.0-193.el8.x86_64来访问内核文件,而不用管内核文件到底有多大,存储在哪些扇区上,这样既简化了访问机制,同时内核文件的存储也不用存储到指定位置的扇区;另一类是GRUB模块文件,它们是GRUB2功能的扩展,每个模块代表一个扩展功能。因为core.img可以访问文件系统,因此,每扩展一个core.img的功能,就可以将其写成一个模块文件,存储在/boot/grub2/i386-pc/下,当要使用该功能时,GRUB2直接到该路径下找到对应的模块文件,加载进来即可,这样,core.img的功能就不再受MBR gap的限制了,而是可以通过放置在/boot/grub2/i386-pc/下的模块文件来实现持续扩展。
(2)内核启动前的配置与管理阶段
core.img执行后,会出现GRUB2启动菜单,如图1-8所示,第一项表示使用内核vmlinuz-4.18.0-193.el8.x86_64来启动Linux;第二项表示使用vmlinuz-0-rescue-c728625b6fee4703af663d7a424019c9来启动Linux。可以使用上下方向键进行选择,选中其中一项,按〈Enter〉键,core.img就会准备好内核加载环境,然后将启动项中指定的内核加载到内存,并移交控制权给内核,引导程序的使命就完成了。
GRUB2的启动菜单项是可以编辑的,可以增加/删除启动菜单项,也可以修改已有的启动菜单项。
图1-8 GRUB2启动菜单
在加载内核之前,还可以使用GRUB2做一系列的操作,如编辑启动项、运行GRUB2命令等,具体操作说明如下。
1)编辑启动项,在GRUB2启动菜单选择一个启动项后,按〈E〉键,可以编辑该启动项的内容,如图1-9所示,可以配置启动的Linux内核文件名和内核参数等,按〈Esc〉键可以退回到GRUB2启动菜单。
图1-9 GRUB2启动项编辑界面
2)运行GRUB2命令,在GRUB2启动菜单界面,按〈C〉键,可以进入GRUB2的命令模式,如图1-10所示。
图1-10 GRUB2的命令模式
在grub>命令提示符后,输入help,按〈Enter〉键,可以列出GRUB2当前所支持的命令,如图1-11所示。
图1-11所示命令都是由/boot/grub2/i386-pc/下的模块文件实现的。GRUB2命令提供了很多实用操作,例如很多情况会导致GRUB2不能正常引导(如/boot分区损坏或者配置文件被删除等),此时可以使用GRUB2命令进行手动修复和引导;如果要实现多操作系统的引导(如Linux+Windows),也可以使用GRUB2命令来完成;此外,还可以在不进入操作系统的情况下,使用GRUB2查看指定分区下文件的内容等。
图1-11 GRUB2 命令列表
(3)内核加载阶段
在GRUB2启动菜单中选择启动项后(正常情况下都是选择第一项),按〈Enter〉键,就进入系统引导的第三阶段:内核加载阶段。GRUB2会准备好内核运行的环境,并将指定的内核文件加载到物理内存0x100000开始的位置,一切就绪后,就将控制权转交给内核,跳转到内核的指令开始执行。整个过程都是自动的,不需要人为干预。
“内核运行”是Linux启动过程中的第二个阶段,该阶段主要完成两个任务:操作系统核心的运行;用户态程序运行环境的准备。具体过程如图1-12所示,分为三个阶段。
图1-12 Linux内核运行过程
(1)内核自解压
CentOS 8内核文件路径是/boot/vmlinuz-4.18.0-80.el8.x86_64,这是一个自带解压模块的压缩内核。因此,内核运行要做的第一件事情就是利用自带的解压缩模块将内核文件的剩余内容解压缩到内存。
(2)内核初始化
内核自解压后要再次检测硬件并做初始化,然后完成操作系统各功能模块的初始化工作。
(3)准备过渡系统的root文件系统
任何一个正常工作的Linux操作系统都需要有root文件系统,因为所有的应用程序、内核模块以及其他文件都在root文件系统中,如果root文件系统不能正常访问,文件就不能访问,所有的应用就不能启动,CentOS 8同样如此,也要准备一个root文件系统。不同的是,CentOS 8会先使用一个临时的root文件系统(简称过渡root文件系统)作为过渡系统,在过渡系统中加载本机的硬件驱动内核模块,使得最终的root文件系统能够被顺利挂载。
过渡系统的root文件系统就来源于/boot/initramfs-4.18.0-80.el8.x86_64.img镜像文件(简称initramfs镜像),是一个27MB的文件。本阶段的任务就是要展开initramfs镜像作为过渡root文件系统,有关过渡root文件系统后续还有详细的解释。过渡root文件系统准备好后,Linux内核就可以访问其中的应用程序和文件了,也就可以运行用户态程序了,这就为Linux启动的第三阶段“初始化程序运行”做好了准备。
CentOS 8为什么要使用过渡系统,而不是直接挂载最终的root文件系统呢?
因为root文件系统的挂载至少取决于两个因素:第一个因素是存储设备,有的root文件系统使用SATA硬盘,有的使用SCSI硬盘,有的则使用网络存储,还有的使用逻辑卷,不同的存储设备需要不同的内核驱动来支持;第二个因素是文件系统的类型,有的采用Ext系列,有的采用XFS等,不同的文件系统也需要不同的内核驱动来支持。
CentOS 8作为一个发行版,其内核是事先编译好的,也就是说不管在哪台机器上安装,其内核文件都是一样的,这就决定了内核配置也是一样的。CentOS 8要支持在所有计算机上直接挂载所有的root文件系统,就要将挂载相关的所有驱动全部编译进内核,这样会导致内核文件的体积很大,占用资源,而具体到每个用户,它们又只用到了其中很少的一部分驱动,造成了浪费。
基于以上原因,CentOS 8没有将所有的相关驱动编译进内核,而是选择性的将最常用的驱动编译进内核,其他的驱动则编译成内核模块,存储在root文件系统。CentOS 8的这种做法,解决了内核体积大,资源占用多且浪费的问题。但是,如果某台计算机需要加载内核模块才能挂载root文件系统,就会遇到问题,因为要挂载root文件系统,必须要先加载内核模块,而这些内核模块又位于root文件系统中,需要先挂载root文件系统才能访问,这样就陷入了一个死循环。
为此CentOS 8引入了一个过渡系统,过渡系统的root文件系统(简称过渡root文件系统)类型是tmpfs,这是一个基于内存的临时文件系统,内核可以直接访问,不需要内核模块的支持,也就是说这个过渡root文件系统在每台安装了CentOS 8的计算机上都可以直接挂载。
过渡root文件系统的内容来源于initramfs镜像文件,在系统引导阶段由GRUB2加载到内存,然后内核将initramfs镜像文件直接解压到内核所创建的tmpfs中,而tmpfs在创建时就挂载在过渡系统的/ 目录下,此时,过渡系统的/ 目录下就有内容了,即initramfs镜像文件的内容。initramfs镜像文件包含了本机硬件相关的内核模块,在安装CentOS 8的过程中,这些内核模块会打包到initramfs镜像文件中,因此initramfs镜像文件是定制的,不同配置的机器上,initramfs镜像文件可能不同。这样,过渡系统就可以将这些内核模块加载到内核之中,有了这些内核模块驱动的支持,最终的root文件系统就可以访问和挂载了,这样就解决了CentOS 8发行版在不同机器上安装的问题。
另外,tmpfs是一个基于内存的临时文件系统,与其他内存文件系统相比,它有两个特点:tmpfs效率更高,tmpfs不是一个单独的块设备,这意味着对tmpfs的操作不需要用传统块设备的那些路径和层级,开销更小;tmpfs更灵活,它可以动态调整大小,还可以交换分区的空间。
Linux内核运行阶段会输出信息到屏幕,如图1-13所示。
图1-13 Linux内核运行信息
使用dmesg命令可以打印内核运行阶段所输出的信息,如果内核运行阶段没来得及看屏幕上的输出,可以等Linux启动后输入dmesg就可以将之前的信息再输出一遍。
注意: 内核运行阶段只会准备过渡root文件系统,准备好后,存储在过渡root文件系统上的内核模块文件是可以访问的,但此阶段并不加载这些内核模块,加载的动作是在Linux启动的第三个阶段完成的,后续会有详细说明。
在内核运行阶段,和用户最相关的就是内核配置:内核中哪些驱动需要,哪些不需要,哪些需要和内核编译在一起,哪些可以编译成模块动态加载,这些都是特别需要注意的地方。这些配置直接决定了该Linux系统能否在此计算机上正常运行,能否驱动该计算机上的硬件正常工作,也直接决定了Linux内核所占用的资源大小。如果后续从事嵌入式开发、系统性能调优相关的工作,就会经常和内核配置打交道。
初始化程序是Linux内核运行后启动的第一个用户态程序,进程号(PID)是1。初始化程序的功能主要有两个,具体说明如下。
Linux下的服务通常指一个守护进程,在某个端口监听、接收并处理客户端从网络发送过来的请求,常见的服务如firewalld、crond和sshd服务等。
本书此处所指的服务,范围更大,它指由初始化程序启动和管理的程序,在这些程序中,有的完成任务后就退出了,有的则会常驻系统,监听请求。
不同的Linux发行版,甚至同一Linux发行版中不同版本的初始化程序都可能不同。例如CentOS 5采用的是init程序,CentOS 6采用的是upstart,CentOS 7/CentOS 8采用的是systemd。因此,CentOS 8的初始化程序运行就是指systemd的运行。systemd比较新,和传统的初始化程序(如SysV风格的init程序)相比,systemd最大的特点是它实现了 Linux服务的并行启动 ,大大缩短了Linux系统的启动时间。
可以在CentOS 8上使用命令“man bootup”查看systemd初始化时启动服务的顺序以及相关信息。
在功能上,systemd除了管理服务外,还包含一系列的实用工具,用于记录日志、系统设置、简单网络管理、网络时间同步和名字解析等。在使用方式上,systemd还兼容传统的SysV的init程序使用方式。由于systemd的诸多特性,已有越来越多的发行版(Red Hat/CentOS/Ubuntu等)使用它来替代init,作为新的初始化程序。
CentOS 8中初始化程序(systemd)的运行过程分为两个阶段,如图1-14所示。具体说明如下。
图1-14 CentOS 8初始化程序运行过程
(1)过渡系统初始化
此阶段位于“准备过渡系统的root文件系统”之后,systemd程序存储在过渡root文件系统之中,当过渡root文件系统可以访问后,systemd程序也就可以访问了。然后,内核就启动systemd程序,systemd是第一个运行的程序,因此它的进程号PID为1。systemd启动后,会根据配置启动相关服务,如日志服务和systemd-udevd服务等。由于过渡系统初始化的主要目标是挂载最终的root文件系统,因此,要将过渡root文件系统中相关的内核驱动加载到内核中,例如本例中CentOS 8最终的root文件系统类型是XFS,因此,在过渡系统的初始化中就要加载XFS的内核驱动模块。具体做法如下代码所示,systemd-fsck先检查最终的root文件系统类型,得知是XFS,从而加载XFS内核驱动模块。
在第一阶段和第二阶段都有“自动加载内核模块”的操作,通常情况下,内核模块的加载是需要手动运行insmod或者modprobe来手动添加的,为了方便起见,也可以编写自定义的systemd服务,然后将这些命令写入自定义服务中,由systemd在Linux系统启动时运行这些服务,从而实现加载内核模块的操作。
上述两种方式,都需要人为的干预或配置,而“自动加载内核模块”则是除以上两种方式外的另外一种方式,它无须人为干预,由sytemd的服务自动判断,自动加载内核模块。例如XFS内核模块就是在挂载root文件系统时自动加载的,虚拟机网卡e1000的内核驱动模块就是由systemd-udevd服务在接收到网卡检测信息后自动加载的,这些内核模块都不需要用户做任何工作就能自动加载。
(2)最终系统的初始化
相关驱动都准备好后,就进入第二个阶段“最终系统的初始化”。首先挂载最终的root文件系统,如下代码所示,最终的root文件系统挂载在过渡root文件系统的/sysroot路径下。
接下来systemd会重新加载配置(initrd-parse-etc.service),如下代码所示。
systemd还会做一些准备和处理工作(如启动initrd-cleanup.service服务),当systemd的初始化工作达到一个同步点(initrd-switch-root.target目标)后,日志会显示:“systemd[1]: Reached target Switch Root.”,systemd会启动initrd-switch-root.service服务,该服务会调用/usr/bin/systemctl--no-block switch-root/sysroot将/sysroot切换为最终系统的根(/)目录。
initrd-switch-root.service除了切换根目录外,还会运行新的初始化程序systemd(该程序位于最终root文件系统中)且进程号为1。
因为CentOS 8中老的初始化程序(位于过渡系统)和新的初始化程序(位于最终系统的root文件系统中)都是systemd且路径相同。因此,initrd-switch-root.service会将老的systemd进程状态传递给新运行的systemd进程,这样,新的systemd进程就获得了过渡系统中所启动的服务的状态信息,便于后续对这些服务进行管理。
新systemd会找到/etc/systemd/system/default.target以此决定CentOS 8 运行级 ,default.target是一个软链接(类似Windows的快捷方式)文件,链接的文件不同,运行级也不同。以本书为例,default.target链接到了/lib/systemd/system/multi-user.target文件,这样systemd就会将CentOS 8初始化成“多用户字符界面”,对应传统的SysVinit的runlevel3运行级。systemd会根据运行级做不同的初始化工作,例如本例multi-user.target的初始化工作包括:重新加载SELinux的策略、重启日志服务、挂载/dev/sda1分区到/boot、自动加载内核模块、启动NetworkManager服务、启动sshd服务等,如图1-14所示。
不管是在哪个阶段,systemd的初始化工作都是可以并行的。
在基本的初始化服务完成后,systemd会启动getty@tty1.service服务,该服务会运行agetty程序,agetty程序的界面如图1-15所示,即登录界面。
图1-15 CentOS 8登录界面
使用命令systemctl -a列出sytemd的服务及状态,包括要加载但没有找到的服务、已经加载正在运行的服务、已经加载但已经退出的服务等。