Linux 之旅 19:启动流程、模块管理与loader
图源:pexels
Linux启动流程分析
启动流程一览
通常操作系统的启动过程可以分为以下几个步骤:
- 加载BIOS以获取硬件信息并进行自检,这样就可以获取到第一个可启动设备。
- 读取并执行第一个可启动设备的MBR内的启动引导程序(
grub2
、spfdisk
等)。 - 根据启动引导程序的设置加载Kernel,Kernel进行硬件检测并加载需要的驱动和模块。
- Kernel启动Systemd进程,并且用
default.target
模式准备操作系统环境:systemd
执行sysinit.target
初始化系统systemd
执行basic.target
准备基础的操作系统环境systemd
启动multi-user.target
下的本机与服务器服务systemd
执行multi-user.target
下的/etc/rc.d/rc.local
文件systemd
执行multi-user.target
下的getty.target
及登录服务systemd
执行graphical
所需的服务
BIOS、boot loader与Kernel加载
BIOS、启动自我检测与MBR/GPT
操作系统启动后第一件事情就是加载BIOS(Basic Input Output System),并通过BIOS加载CMOS信息,由CMOS内设置的信息读取主机的各种硬件信息。
关于BIOS和CMOS的详细信息可以阅读BIOS和CMOS是什么?教大家认识BIOS和CMOS!。
从CMOS中可以获取到第一个可用的启动设备(就是我们出重装系统的时候会在BIOS中修改的那个启动顺序列表),然后会从第一个启动设备的MBR分区中获取 boot loader(启动引导程序),如果是多系统,还可能由MBR中的boot loader加载其它分区中的boot loader,再启动具体的操作系统。
boot loader
如果一个磁盘上只装了一个操作系统,其boot loader就在磁盘的MBR中,也就是整个磁盘的第一块扇区中。如果是多系统,每个系统的boot loader会保存在系统所在的分区的第一块扇区(启动扇区,boot sector)中,而MBR中的boot loader仅起一个转交启动工作的用途,会在用户选择启动哪个系统之后,将系统启动工作转移给对应系统的boot loader来执行。整个过程可以用下图表示:
图源:鸟哥的私房菜
所以boot loader具有以下功能:
- 提供菜单:用户可以选择启动哪一个选项启动(对于单系统,也可以提供救援模式之类的启动项)
- 加载内核文件:直接加载内核的相关文件到内存
- 转交启动工作给其它loader:如果是多系统,MBR中的boot loader会将具体的启动工作转交给对应的操作系统的boot loader。
不同的操作系统因为使用的文件系统不同,比如Linux使用xfs而Windows使用fat,所以使用的boot loader也会有所不同,而Windows的boot loader行为比较简单,不具备转交启动工作给非Windows系统的boot loader的功能,而且安装Windows时不会提供选项,必定会将boot loader写入MBR中。这样就产生了一个结果,只要安装了Windows,无论其它分区有没有安装别的操作系统,都无法再由boot loader启动,只能启动Windows。所以如果要安装多系统,比如Windows和Linux,最好先安装Windows,再安装Linux。使用Linux的boot loader来提供多个操作系统的加载选项即可。
加载内核检测硬件与initramfs的功能
Linux内核的相关文件放在/boot
中,并且一般会保存在/boot/vmlinuz
:
[icexmoon@xyz ~]$ ls --format=single-column -F /boot
config-3.10.0-1160.el7.x86_64 # 编译内核时的相关配置
efi/
grub/
grub2/ # boot loader grub2的相关文件
initramfs-0-rescue-f5a85e92d51e4d40975fd956fd775f9c.img # 救援时使用的虚拟文件系统
initramfs-3.10.0-1160.el7.x86_64.img # 一般开机会使用的虚拟文件系统
symvers-3.10.0-1160.el7.x86_64.gz
System.map-3.10.0-1160.el7.x86_64
testing.img
vmlinuz-0-rescue-f5a85e92d51e4d40975fd956fd775f9c* # 救援用的内核文件
vmlinuz-3.10.0-1160.el7.x86_64* # 内核文件
内核文件被加载以后,仅能直接使用内核中的相关功能,内核中没有的功能就要以“内核模块”的方式加载,这些模块保存在/lib/modules
目录中。
要直接给对内核中的功能增加或修改相当麻烦,需要重新编译内核,所以一般来说,能做成“内核模块”的功能都会做成模块,这样无论是增加还是修改就会简单的多,只要内核挂载相应的模块即可,无需重新编译内核。但是这样会有一个新的问题,即内核模块所在的目录/lib/modules
是和根目录/
在一个分区的,而Linux加载内核后仅会挂载内核所在的这个分区,更要命的是,xfs
之类的当前主要使用的文件系统驱动是以模块的方式提供的,并非直接编译在内核中,而内核需要先加载使用xfs
文件系统的根目录分区才能读取到模块,这就变成了一个鸡生蛋蛋生鸡的问题。
万幸的是Linux提供了一个叫做虚拟文件系统(Initial RAM Disk 或 Initial RAM Filesystem)的东西,这是一个保存在/boot
中的压缩文件,其中包含了一些内核必须的模块,比如文件系统或者磁盘驱动等。内核会先解压这个虚拟文件系统到内存中,并用它作为根目录,从而加载必须的模块,然后就可以加载“真正的”根目录所在的文件系统了,加载后会取代虚拟文件系统。
下面实际解压系统中的虚拟文件系统查看其组成,因为CentOS 7的虚拟文件系统压缩包格式比较特殊,所以这里参考centos7 initramfs解包 打包编写了一个解包的脚本:
initramfs_file=$(ls -a /boot/initramfs-$(uname -r).img)
tmp_home='/tmp/initramfs'
if [ ! -d $tmp_home ]
then
mkdir $tmp_home
fi
initramfs_tmp="${tmp_home}/initramfs_file"
if [ ! -f $initramfs_tmp ]
then
cp $initramfs_file ${initramfs_tmp}
fi
/usr/lib/dracut/skipcpio $initramfs_tmp | zcat | cpio -id
运行这个脚本解包即可:
[root@xyz initramfs]# sh unzip_initramfs.sh
128811 块
[root@xyz initramfs]# ll
总用量 31352
lrwxrwxrwx. 1 root root 7 9月 7 22:00 bin -> usr/bin
drwxr-xr-x. 2 root root 45 9月 7 22:00 dev
drwxr-xr-x. 12 root root 4096 9月 7 22:00 etc
lrwxrwxrwx. 1 root root 23 9月 7 22:00 init -> usr/lib/systemd/systemd
-rw-------. 1 root root 32089078 9月 7 22:00 initramfs_file
lrwxrwxrwx. 1 root root 7 9月 7 22:00 lib -> usr/lib
lrwxrwxrwx. 1 root root 9 9月 7 22:00 lib64 -> usr/lib64
drwxr-xr-x. 2 root root 6 9月 7 22:00 proc
drwxr-xr-x. 2 root root 6 9月 7 22:00 root
drwxr-xr-x. 2 root root 6 9月 7 22:00 run
lrwxrwxrwx. 1 root root 8 9月 7 22:00 sbin -> usr/sbin
-rwxr-xr-x. 1 root root 3117 9月 7 22:00 shutdown
drwxr-xr-x. 2 root root 6 9月 7 22:00 sys
drwxr-xr-x. 2 root root 6 9月 7 22:00 sysroot
drwxr-xr-x. 2 root root 6 9月 7 22:00 tmp
-rw-r--r--. 1 root root 302 9月 7 21:59 unzip_initramfs.sh
drwxr-xr-x. 7 root root 66 9月 7 22:00 usr
drwxr-xr-x. 2 root root 29 9月 7 22:00 var
systemd
systemd是系统内核启动后开启的第一个服务进程,其功能是准备一个基础的操作系统运行环境,包括主机名、网络设置、语言设置、文件系统格式等,所有的这些都是通过一个默认的启动服务集合default.target
实现的。
target与runlevel
之前我们说过,现在的服务管理机制systemd与以前的system V是有所不同的,以前用于区分系统模式的是runlevel,而现在则使用的是target,为了兼容性方面的考虑,systemd将部分以前的runlevel映射到了某些target上:
[icexmoon@xyz ~]$ ll -d /usr/lib/systemd/system/runlevel*.target | cut -c 41-
/usr/lib/systemd/system/runlevel0.target -> poweroff.target
/usr/lib/systemd/system/runlevel1.target -> rescue.target
/usr/lib/systemd/system/runlevel2.target -> multi-user.target
/usr/lib/systemd/system/runlevel3.target -> multi-user.target
/usr/lib/systemd/system/runlevel4.target -> multi-user.target
/usr/lib/systemd/system/runlevel5.target -> graphical.target
/usr/lib/systemd/system/runlevel6.target -> reboot.target
因此,以前用于操作系统模式转换的命令init x
是依然可以使用的,其和systemctl
命令的对应关系是:
SystemV | systemd |
---|---|
init 0 | systemctl poweroff |
init 1 | systemctl rescue |
init [234] | systemctl isolate multi-user.target |
init 5 | systemctl isolate graphical.target |
init 6 | systemctl reboot |
systemd的处理流程
systemd
会通过启动default.target
的方式准备一个基本的操作系统环境,从Linux 之旅 17:系统服务(daemons)中我们已经知道,default.target
可以有两个选项:graphical.target
和multi-user.target
,实际上default.target
就是一个到这两者之一的一个alias
。
比较常用的是graphical.target
,并且其本身包含multi-user.target
,所以这里假设当前Linux主机使用的default.target
是graphical.target
。
我们来看graphical.target
具体会干些什么:
[root@xyz ~]# cat /usr/lib/systemd/system/graphical.target | grep -v '^#'
[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
比较重要的设置是Requires
和Wants
,前者是说启动graphical.target
之前要先启动哪些服务,后者是说启动graphical.target
之后要启动哪些服务。
从这里可以看出,相关服务的启动顺序是multi-user.target
->graphical.target
->display-manager.service
。当然,前两个是target
,是服务集,最后一个是单纯的服务。
现在我们看multi-user.target
会干些什么:
[root@xyz ~]# cat /usr/lib/systemd/system/multi-user.target | grep -v '^#'
[Unit]
Description=Multi-User System
Documentation=man:systemd.special(7)
Requires=basic.target
Conflicts=rescue.service rescue.target
After=basic.target rescue.service rescue.target
AllowIsolate=yes
可以看到,要启动multi-user.target
就要先启动basic.target
。
最后再看一下默认安装的需要被multi-user.target
加载的服务有哪些:
[root@xyz ~]# ls /usr/lib/systemd/system/multi-user.target.wants/
dbus.service plymouth-quit.service systemd-ask-password-wall.path systemd-update-utmp-runlevel.service
getty.target plymouth-quit-wait.service systemd-logind.service systemd-user-sessions.service
用户自定义的需要被multi-user.target
加载的服务有:
[root@xyz ~]# ls /etc/systemd/system/multi-user.target.wants/
abrt-ccpp.service