一文解析 Linux 操作系统引导启动全流程

理解 Linux 操作系统开机引导和启动过程,对于操作系统配置,以及解决相关启动问题是至关重要的。

更多相关系列博文请参考:

一文解析 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
2
3
4
initramfs-3.10.0-1127.el7.x86_64.img
System.map-3.10.0-1127.el7.x86_64
vmlinuz-0-rescue-b7ee23f3e71aae428045b321afa67185
vmlinuz-3.10.0-1127.el7.x86_64

选定的内核被加载到内存中,首先必须从压缩格式解压自身,接下来内核将会接管控制并完成 >>> 探测硬件 –> 加载驱动 –> 挂载根文件系统 –> 切换至根文件系统(rootfs)–> 运行 /sbin/init 完成系统初始化。

流程没有问题,但由于 Kernel 为了精简,只保留了最基本的模块,并没有各种硬件或文件系统的驱动程序,就无法识别 rootfs 所在的设备,故产生了 initrd(Initial RAM Disk)这个虚拟内存磁盘镜像文件。

事实上,上面 GRUB 在加载内核同时(Stage2),也把 initrd 加载到内存中并运行,产生一个临时的虚拟根文件系统(rootfs)来替代真实的根文件系统。

1
2
3
4
5
6
# 挂载 initramfs.img 方法
$ mkdir /home/centos/temp
$ cp /boot/initramfs-3.10.0-1127.el7.x86_64.img /home/centos/temp/initrd.gz -a
$ cd /home/centos/temp
$ gunzip ./initrd.gz
$ cpio -ivmd < ./initrd

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1、获得网络环境 
2、挂载设备
3、开机启动画面Plymouth(取替了过往的 RHGB)
4、判断是否启用SELinux
5、显示于开机过程中的欢迎画面
6、初始化硬件
7、用户自定义模块的加载
8、配置内核的参数
9、设置主机名
10、同步存储器
11、设备映射器及相关的初始化
12、初始化软件磁盘阵列(RAID)
13、初始化 LVM 的文件系统功能
14、检验磁盘文件系统(fsck)
15、设置磁盘配额(quota)
16、重新以可读写模式挂载系统磁盘
17、更新quota(非必要)
18、启动系统虚拟随机数生成器
19、配置机器(非必要)
20、清除开机过程当中的临时文件
21、创建ICE目录
22、启动交换分区(swap)
23、将开机信息写入/var/log/dmesg文件中

执行系统初始化后,会执行 /etc/inittab 来设定系统运行的默认级别

可见,Linux 系统中共有 [0-6] 七个运行级别,分别对应不同的登录模式。

关于 Systemd/Upstart 工具启动守护进程进行系统初始化,可以参见后文【Reff Reading】中相关描述。


RunLevel

Runlevel 运行级别,不同的运行级别下,系统会启动的不一样服务。

设定玩系统默认运行级别以后,接着调用 /etc/rc.d/rc 脚本,这个脚本会根据默认运行级别参数,分别执行 /etc/rc.d/rcN.d 目录下的不同脚本

1
2
3
4
5
6
7
Run level 0 – /etc/rc.d/rc0.d/
Run level 1 – /etc/rc.d/rc1.d/
Run level 2 – /etc/rc.d/rc2.d/
Run level 3 – /etc/rc.d/rc3.d/
Run level 4 – /etc/rc.d/rc4.d/
Run level 5 – /etc/rc.d/rc5.d/
Run level 6 – /etc/rc.d/rc6.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
2
3
$ sudo /etc/init.d/network restart
$ sudo /etc/init.d/network stop
$ sudo /etc/init.d/network start

最后会执行 /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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cat /etc/systemd/system/default.target
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.

[Unit]
Description=Graphical Interface
Documentation=man:systemd.special(7)
Requires=multi-user.target
Wants=display-manager.service
Conflicts=rescue.service rescue.target
After=multi-user.target rescue.service rescue.target display-manager.service
AllowIsolate=yes

After 中指定的 target 需要在 default.target 之前运行。

👇👇👇 关于状态检查点 👇👇👇

sysinit.targetbasic.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
2
3
4
5
6
$ ll /etc/systemd/system/graphical.target.wants/
total 0
lrwxrwxrwx. 1 root root 47 Mar 7 01:30 accounts-daemon.service -> /usr/lib/systemd/system/accounts-daemon.service
lrwxrwxrwx. 1 root root 61 Mar 7 01:31 initial-setup-reconfiguration.service -> /usr/lib/systemd/system/initial-setup-reconfiguration.service
lrwxrwxrwx. 1 root root 44 Mar 7 01:31 rtkit-daemon.service -> /usr/lib/systemd/system/rtkit-daemon.service
lrwxrwxrwx. 1 root root 39 Mar 7 01:31 udisks2.service -> /usr/lib/systemd/system/udisks2.service

| ================================================== Split Line =============================================== |

[Step 4] >>> systemd 最后执行 graphical.target 下的 /etc/rc.d/rc.local

systemd 是可以兼容 systemv init 中的 rc.local 配置的,通过 rc-local.service 来实现兼容的。

你可以通过查看其配置文件来观察如何启动:

1
2
3
4
5
6
7
8
9
10
11
$ systemctl cat rc-local.service
[Unit]
Description=/etc/rc.d/rc.local Compatibility
ConditionFileIsExecutable=/etc/rc.d/rc.local
After=network.target

[Service]
Type=forking
ExecStart=/etc/rc.d/rc.local start
TimeoutSec=0
RemainAfterExit=yes

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
2
3
4
5
$ ls /etc/init
init.d/ inittab

$ ls /etc/init.d/
functions netconsole network README

Upstart 启动流程分析

Upstart 是一个使用基于事件(Event)的 System-V Init 初始化系统的替代品,最初为 Ubuntu 发行版开发,以适合所有 Linux 发行版为目标。

Upstart 运行的第一个 init 进程是 Upstart init(/sbin/init splash)<<< pid=1,是所有进程的父进程。

对于 Upstart 而言,/etc/init(/etc/event.d)配置目录是关键,里面全是作业配置文件(控制作业的启动/停止):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ls /etc/init
acpid.conf container-detect.conf hwclock.sh.conf mounted-tmp.conf rcS.conf tty6.conf
alsa-utils.conf control-alt-delete.conf irqbalance.conf mounted-var.conf rc-sysinit.conf udev.conf
anacron.conf cron.conf kmod.conf mountkernfs.sh.conf resolvconf.conf udevmonitor.conf
apparmor.conf cups-browsed.conf lightdm.conf mountnfs-bootclean.sh.conf rfkill-restore.conf udevtrigger.conf
apport.conf cups.conf mountall-bootclean.sh.conf mountnfs.sh.conf rfkill-store.conf ufw.conf
avahi-cups-reload.conf dbus.conf mountall.conf mtab.sh.conf rsyslog.conf upstart-file-bridge.conf
avahi-daemon.conf failsafe.conf mountall-net.conf networking.conf setvtrgb.conf upstart-socket-bridge.conf
bluetooth.conf failsafe-x.conf mountall-reboot.conf network-interface.conf shutdown.conf upstart-udev-bridge.conf
bootmisc.sh.conf flush-early-job-log.conf mountall.sh.conf network-interface-container.conf ssh.conf ureadahead.conf
checkfs.sh.conf friendly-recovery.conf mountall-shell.conf network-interface-security.conf thermald.conf ureadahead-other.conf
checkroot-bootclean.sh.conf gpu-manager.conf mountdevsubfs.sh.conf network-manager.conf tty1.conf usb-modeswitch-upstart.conf
checkroot.sh.conf hostname.conf mounted-debugfs.conf passwd.conf tty2.conf wait-for-state.conf
console.conf hostname.sh.conf mounted-dev.conf procps.conf tty3.conf whoopsie.conf
console-font.conf hwclock.conf mounted-proc.conf procps-instance.conf tty4.conf
console-setup.conf hwclock-save.conf mounted-run.conf rc.conf tty5.conf

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 目录下执行所有脚本,以启动相应运行等级的系统服务。


Author

Waldeinsamkeit

Posted on

2015-01-26

Updated on

2024-03-16

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.