一文解析 Linux 操作系统引导启动全流程
理解 Linux 操作系统开机引导和启动过程,对于操作系统配置,以及解决相关启动问题是至关重要的。
更多相关系列博文请参考:
Linux OS 引导启动流程
首先,通过 Linux 引导启动图解来了解系统引导启动的整个流程,可以基本分为以下几个步骤:
POST >>> BIOS >>> MBR >>> GRUB >>> Kernel >>> Init >>> RunLevel
下面逐步剖析说明系统引导过程(CentOS 6.9):
| ================================================== Split Line =============================================== |
BIOS
Linux 主机开机上电后,首先会加载位于主板 EPPROM 芯片中的 BIOS(Basic Input/Output System)系统固件程序(用于初始化硬件组件),同时会从 CMOS 芯片中读取用户自定义和系统自动记录的 BIOS 配置(BIOS 无法存储配置,CMOS 电池掉电会重置 BIOS 设置)。
然后会进行 POST(Power On Self Test)加电自检过程(也称为 BIOS 上电自检) >>> 就是根据主板 BIOS 中的设置对主机各种硬件设备(CPU、Memory、Mainboard、Disk、CMOS Chip)进行检测,以确认主机硬件基本功能是否正常?!!
此时,可能会出现 两种异常情况:
- 如果出现致命故障:停机,并且由于初始化过程还没完成,所以不会出现任何提示信号;
- 如果出现一般故障:会发出声音等提示信号,等待故障清除;
若未出现故障(主机硬件基本功能正常),则 POST 加电自检通过。
BIOS 上电自检通过后,会按照 BIOS 中预设的启动顺序(Boot Sequence)逐一查找可引导启动的磁盘设备(HDisk/CD-ROM/Removable/Floppy),也就是根据启动顺序去依次查找磁盘设备头是否存在有效 MBR(Master Boot Record)主引导记录?!!若第一个磁盘不存在 MBR,则会继续查找第二个磁盘……
找到可引导启动的设备后,BIOS 会将系统控制权移交给可引导设备。
此时,会从可引导设备的第一个扇区(引导扇区)中读取 MBR 主引导记录信息:
MBR
MBR(Master Boot Record),存储于可引导启动磁盘的头部( 0 磁道 0 扇区),占用 512 字节。MBR 主要是用来告诉从可启动设备的哪个分区来加载 Boot loader 引导加载程序。
512 字节 MBR 包括如下内容:
- 446 bytes:用于存储 BootLoader 程序(GRUB1/GRUB2/LILO…),包含操作系统名称,操作系统内核位置等信息,主要功能是加载内核到内存中运行;
- 64 bytes:用于存储 Partition Table(分区表)信息,每个主分区占用 16 字节(这就是为啥一块硬盘只能有 4 个主分区啦^_^);
- 2 bytes:分区表有效性标记
不同于 MBR 分区格式,对于较大磁盘设备的 GPT 分区格式,引导加载程序的位置会略有变化。
将其加载到内存中,运行引导装载器/程序 GRUB(Boot Loader 目前常用的是 GRUB)。
GRUB1/GRUB2
GRUB(Grand Unified BootLoader)是多系统启动程序,它是目前流行的大部分 Linux 发行版本的主要引导加载程序,用于计算机寻找操作系统内核并加载其到内存的智能程序。
关于 GRUB1 && GRUB2 更多的描述,可以参见后文【Reff Reading】中相关描述。
可见,GRUB 的主要目的就是 >>> 查找和加载 Kernel 到内存。
GRUB 执行过程分为三个阶段(Stages),以 GRUB1 为例:
[1] >>> Stage1
Stage 1 代码存在于 MBR 分区。
BIOS 加电自检通过后,会从可引导设备的引导扇区读取 MBR 主引导记录,并将其载入内存,并开始执行 GRUB/BootLoader 主程序。
由于引导代码(即阶段 1 代码)实际所占用的空间大小仅为 446 字节(也被称为引导镜像 [boot.img]),非常的小,它不可能非常智能。Stage1 的主要工作就是查找并加载第二段 BootLoader 程序(Stage2)。
但此时文件系统还未挂载,MBR 中只记录了分区表(只能通过 MBR 中的分区表来识别分区),不能理解文件系统结构,于是就有了 stage1_5。因此 Stage1 还需要定位并加载 Stage1_5 的程序。
Stage1_5 程序代码必须位于 MBR 记录之后。
| ================================================== Split Line =============================================== |
[2] >>> Stage1_5
Stage1_5 程序必须位于 MBR 引导记录与设备第一个分区之间的位置。
MBR(扇区 0)和 第一个分区(开始位置在扇区 63)之间遗留下 62 个 512 字节的扇区(共 31744 字节),此区域有足够大小的空间用来存储 Stage1_5 程序(代码镜像 [core.img])
因为有更大的存储空间用于 Stage1_5,且该空间足够容纳一些通用的文件系统驱动程序(EXT/FAT/NTFS…)。你可以理解为 Stage1_5 通过加载自身携带的文件系统驱动来实现 MBR 分区表中分区文件系统的识别,让 Stage1 中的 BootLoader 能识别 Stage2 所在分区(主分区)上的文件系统。
Stage1_5 支持的文件系统:
| ================================================== Split Line =============================================== |
[3] >>> Stage2
由于内核的相关文件位于 /boot
目录下,Stage2 程序必须位于 boot 目录所在的磁盘分区(/boot/grub)。
Stage2 BootLoader 程序会根据 /boot/grub/grub.conf
文件查找 Kernel 的信息,然后开始加载 Kernel 程序到内存中。
对于 GRUB2 的 Stage2 还会从 /boot/grub2/i386-pc
目录下加载一些内核运行时模块。
来看一下 GRUB1 的配置文件:
其中,root (hd0,0)
这个 root 并不是真正的根,而是 / 所在的位置。可以理解成 /boot 是处在 (hd0,0)/boot
,而这里的(hd0,0) 指的是第一个磁盘的第一个分区。
当 Kernel 程序被检测并在加载到内存中,GRUB 就将控制权交接给 了 Kernel 程序。
Kernel
Kernel 内核文件很小,只保留了最基本的模块,以一种自解压的压缩格式存储以节省空间,它与一个初始化的虚拟内存磁盘映像和存储设备映射表都存储于 /boot
目录之下:
1 | initramfs-3.10.0-1127.el7.x86_64.img |
选定的内核被加载到内存中,首先必须从压缩格式解压自身,接下来内核将会接管控制并完成 >>> 探测硬件 –> 加载驱动 –> 挂载根文件系统 –> 切换至根文件系统(rootfs)–> 运行 /sbin/init 完成系统初始化。
流程没有问题,但由于 Kernel 为了精简,只保留了最基本的模块,并没有各种硬件或文件系统的驱动程序,就无法识别 rootfs 所在的设备,故产生了 initrd(Initial RAM Disk)这个虚拟内存磁盘镜像文件。
事实上,上面 GRUB 在加载内核同时(Stage2),也把 initrd 加载到内存中并运行,产生一个临时的虚拟根文件系统(rootfs)来替代真实的根文件系统。
1 | 挂载 initramfs.img 方法 |
initrd 文件展开后的目录如下:
Linux 根目录下文件:
可以看到,initrd 文件其实是一个虚拟的根文件系统,里面包含 bin、lib、lib64、sys、var、etc、sysroot、 dev、proc、tmp 等根目录,其中装载了必要的驱动模块。将内核与真正的根建立联系,内核通过它加载根文件系统的驱动程序,然后以读写方式挂载根文件系统。
当 Kernel 加载启动时,可以从 initrd 文件中装载驱动模块,直到挂载真正的 rootfs,然后将 initrd 从内存中移除。当根文件系统被挂载后,开始装载第一个进程(/sbin/init)<<< pid=1,之后就将控制权交接给了 System V init 程序。
| ================================================== Split Line =============================================== |
↓↓↓↓↓↓ SystemV && Upstart && Systemd ↓↓↓↓↓↓
Linux 系统中支持三种 Init 方式:
- System V initialization:Linux 最古老、最广为流传的(串行执行 shell 脚本启动服务)初始化系统
- Upstart:是一个使用基于事件(Event)的 System-V Init 初始化系统的替代品;最初为 Ubuntu 发行版开发,以适合所有 Linux 发行版为目标的
- Systemd:是新一代的(并行启动服务进程)初始化系统,已成为目前大多数 Linux 发行版(CentOS 7/Ubuntu)中流行且广泛适应的标准初始化系统,大有取代 Upstart 之势;
Systemd 运行的第一个 init 进程是(/lib/systemd/systemd 或 /usr/lib/systemd/systemd);而 Upstart 运行第一个 init 进程是 Upstart init(/sbin/init splash)。
以 System V initialization 为例:
Init
挂载完成根文件系统之后,执行第一个用户进程 init >>> /sbin/init(所有进程的父进程),然后开始进行 OS 初始化。
/etc 目录下的 init 相关的脚本:
init 首先运行 /etc/init/rcS.conf
脚本:
可见,在 rcS.conf 脚本中首先调用了 /etc/rc.d/rc.sysinit
进行系统的初始化设置,来看看:
事实上,init 执行 /etc/rc.d/rc.sysinit 的初始化将会做很多设置:
1 | 1、获得网络环境 |
执行系统初始化后,会执行 /etc/inittab 来设定系统运行的默认级别:
可见,Linux 系统中共有 [0-6] 七个运行级别,分别对应不同的登录模式。
关于 Systemd/Upstart 工具启动守护进程进行系统初始化,可以参见后文【Reff Reading】中相关描述。
RunLevel
Runlevel 运行级别,不同的运行级别下,系统会启动的不一样服务。
设定玩系统默认运行级别以后,接着调用 /etc/rc.d/rc 脚本,这个脚本会根据默认运行级别参数,分别执行 /etc/rc.d/rcN.d 目录下的不同脚本:
1 | Run level 0 – /etc/rc.d/rc0.d/ |
目录名中的 rc
表示 “run command”;d
表示 directory。运行程序目录下的脚本只有 K 和 S 开头的文件:K 开头的文件为开机需要执行关闭的服务,S 开头的文件为开机需要执行开启的服务:
需要注意的是,为了方别系统管理更新,七个 /etc/rc.d/rcN.d
目录里列出的程序,都设为链接文件,全部指向另外一个目录 /etc/rc.d/init.d
(d 表示目录,与程序 init 区分)。通过这一特性,如果你要手动关闭或重启某个进程,直接到目录 /etc/init.d 中寻找启动脚本(以网络服务为例):
1 | sudo /etc/init.d/network restart |
最后会执行 /etc/rc.d/rc.local
这个脚本,可以根据自己的需求将一些执行命令或者脚本写到其中,当开机时就可以加载。
最后执行完 /etc/rc.d/rc.local 脚本后,系统启动完成~~~
Login In
系统引导启动、初始化完成后,Init 给出用户登录提示符(login)或者图形化登录界面,等待用户登入。
用户输入用户和密码登陆后,系统会为用户分配一个用户 ID(uid)和组 ID(gid),这两个 ID 是用户的身份标识,用于检测用户运行程序时的身份验证。
用户登录成功后,整个系统启动流程运行完毕!!!
Reff Reading
推荐阅读部分,通过这一章节的阅读可以帮助你更好的理解 Linux 系统引导启动的全流程:
| ================================================== Split Line =============================================== |
关于 GRUB1/GRUB2
GRUB(Grand Unified BootLoader)是多系统启动程序,它是目前流行的大部分 Linux 发行版本的主要引导加载程序,用于计算机寻找操作系统内核并加载其到内存的智能程序。
GRUB2 跟 GRUB1 类似,支持从 Linux 内核选择之一引导启动。Red Hat 包管理器(DNF)支持保留多个内核版本,以防最新版本内核发生问题而无法启动时,可以恢复老版本的内核。
默认情况下,GRUB 提供了一个已安装内核的预引导菜单(GUN GRUB 界面),其中包括问题诊断菜单(recuse)以及恢复菜单(如果已经配置恢复镜像)。GRUB1 能够通过文件 /boot/grub/grub.conf
进行配置。GRUB2 通过 /boot/grub2/grub.cfg
进行配置。
GRUB1 现在已经逐步被弃用,在大多数现代发行版上它已经被 GRUB2 所替换,GRUB2 是在 GRUB1 的基础上重写完成。基于 Red Hat 的发行版大约是在 Fedora 15 和 CentOS/RHEL 7 时升级到 GRUB2 的。GRUB2 提供了与 GRUB1 同样的引导功能,但是 GRUB2 也是一个 类似主框架(mainframe)系统上的基于命令行的前置操作系统(Pre-OS)环境,使得在预引导阶段配置更为方便和易操作(grub >)。
两个版本的 GRUB 的基本工作方式一致,其主要阶段也保持相同,都可分为 3 个阶段。
Systemd 启动流程分析
Systemd 是新一代的初始化系统(并行启动服务进程),已成为目前大多数 Linux 发行版(CentOS 7/Ubuntu)中流行且广泛适应的标准初始化系统,大有取代 Upstart 之势。
Systemd 运行的第一个 Init 进程是(/lib/systemd/systemd 或 /usr/lib/systemd/systemd)<<< pid=1,是所有进程的父进程。
Systemd Init 进程会根据其配置文件(/etc/systemd/system/default.target
),决定 Linux 系统应该启动达到哪个目标态(Target)?!!并且递归的处理它的依赖关系。
默认目标态一般为:graphical.target(RunLevel 5)
或者 multi-user.target(RunLevel 3)
,下图展示关键服务配置的启动依赖:
下面逐步剖析说明 systemd 启动的 4 个关键步骤:
| ================================================== Split Line =============================================== |
[Step 1] >>> systemd 执行默认 default.target 配置,决定 Linux 系统应该启动达到哪个目标态(Target)
default.target
是一个指向真实的 target 文件的符号链接:
- 对于桌面系统,其链接到
graphical.target
,该文件相当于旧式 SystemV Init 方式的runlevel 5
; - 对于一个服务器操作系统来说,default.target 更多是默认链接到
multi-user.target
, 相当于 SystemV Init 系统的runlevel 3
; - emergency.target 相当于单用户模式。
systemd 启动的目标态(target)和老版 SystemV Init 启动运行级别(runlevel)的对比:
其中的,systemd 目标态别名 是为了 systemd 向前兼容 systemV Init 而提供。这个目标态别名允许系统管理员(包括我自己)用 systemV 命令(例如 init 3)改变运行级别。当然,该 systemV 命令是被转发到 systemd 进行解释和执行的。
| ================================================== Split Line =============================================== |
[Step 2] >>> systemd 执行启动 default.target 所依赖的目标 basic.target 和 sysinit.target 初始化系统
你可以通过如下命令来查看依赖:
1 | cat /etc/systemd/system/default.target |
After 中指定的 target 需要在 default.target 之前运行。
👇👇👇 关于状态检查点 👇👇👇
sysinit.target
和 basic.target
目标态可以被视作启动过程中的状态检查点。
尽管 systemd 的设计初衷是并行启动系统服务,但是部分服务或目标态是其它服务或目标态的启动的前提。因此,系统将暂停于 检查点 直到其所要求的服务和目标态都满足为止。
For sysinit.target 状态
sysinit.target 状态的到达,是以其所依赖的所有资源模块都正常启动为前提的,如:文件系统挂载、交换文件设置、设备管理器的启动、随机数生成器种子设置、低级别系统服务初始化、加解密服务启动等都必须完成,但是 在 sysinit.target 中这些服务与模块是可以并行启动的。
For basic.target 状态
systemd 接下来启动 basic.target,启动其所要求的所有单元。 basic.target 通过启动下一目标态所需的单元而提供了更多的功能,这包括各种可执行文件的目录路径、通信 sockets,以及定时器等。在 basic.target 中这些服务与模块也是可以并行启动的。
| ================================================== Split Line =============================================== |
[Step 3] >>> systemd 启动最终 graphical.target 下的本机与服务器服务
由于当前 default.target 指向 graphical.target 目标态,那么这一步就启动对应的目标态下服务。它的服务存在于 /etc/systemd/system/graphical.target.wants
目录中:
1 | ll /etc/systemd/system/graphical.target.wants/ |
| ================================================== Split Line =============================================== |
[Step 4] >>> systemd 最后执行 graphical.target 下的 /etc/rc.d/rc.local
systemd 是可以兼容 systemv init 中的 rc.local 配置的,通过 rc-local.service 来实现兼容的。
你可以通过查看其配置文件来观察如何启动:
1 | systemctl cat rc-local.service |
systemd 在启动的很早时候就会判断 /etc/rc.local 是否存在并且是可执行的?!!
如果满足条件 >>> 那么 systemd 会调用 /usr/lib/systemd/system-generators/ 下面的小程序来把 rc-local.service 服务加入到 default.target 中来。这样在后面的执行时就会触发 rc.local 的运行。
| ================================================== Split Line =============================================== |
↓↓↓↓↓↓ 兼容 System V init 启动 ↓↓↓↓↓↓
systemd 也会查看老式的 systemV init 目录(/etc/init.d/)中是否存在相关启动文件,若存在,则 systemd 根据这些配置文件的内容启动对应的服务。在 Fedora 系统中,过时的网络服务就是通过该方式启动的一个实例:
1 | ls /etc/init |
Upstart 启动流程分析
Upstart 是一个使用基于事件(Event)的 System-V Init 初始化系统的替代品,最初为 Ubuntu 发行版开发,以适合所有 Linux 发行版为目标。
而 Upstart 运行的第一个 init 进程是 Upstart init(/sbin/init splash)<<< pid=1,是所有进程的父进程。
对于 Upstart 而言,/etc/init(/etc/event.d)配置目录是关键,里面全是作业配置文件(控制作业的启动/停止):
1 | ls /etc/init |
Ubuntu Upstart 为了兼容原 System V Init 方式的服务,其启动大概流程如下图所示:
| ================================================== Split Line =============================================== |
下面逐步剖析说明 Upstart 启动的关键步骤:
[1] >>> 设备上电后,由 Grub 加载内核 Kernel,内核在完成初始化后会执行第一个进程 init 进程 <<< Upstart init;
[2] >>> Upstart init 在进行自身的一些初始化之后,会发出 startup 事件;
[3] >>> startup 事件会触发 mountall 等作业(/etc/init 下的作业配置文件,其中包含 start on startup 语句);
[4] >>> mountall 作业中会发射(emits):virtual-filesystems & local-filesystems & remote-filesystems & all-swaps & filesystem & mounting & mounted 事件;
[5] >>> 其中,mounted 事件会触发 container-detect 作业,从而发出 container 事件去触发 network-interface-container 作业,其又发出 net-device-added 事件去触发 network-interface 作业,进而发出 static-network-up 事件,static-network-up 事件和 filesystem 事件一起去触发 rc-sysinit 作业;rc-sysinit 最后会调用 telinit “$[DEFAULT_RUNLEVEL]” 去改变运行等级,从而发出 runlevel 事件触发 rc 作业;
[6] >>> 在 /etc/init/rc.conf 中会调用 /etc/init.d/rc $RUNLEVEL,到这里就和 System V Init(/etc/init/rcS.conf)类似了~~~
[7] >>> 会去 /etc/rcN.d 目录下执行所有脚本,以启动相应运行等级的系统服务。
install_url
to use ShareThis. Please set it in _config.yml
.