五.内核加载和内核初始化阶段
提前说明,下文所述均为sysV init系统启动风格,systemd的启动管理方式大不相同,所以不要将systemd管理的启动方式与此做比较。
到目前为止,内核已经被加载到内存掌握了控制权,且收到了boot loader最后传递的内核启动参数以及init ramdisk的路径。
所有的内核都是以bzImage方式压缩过的,压缩后CentOS 6的内核大小大约为4M,CentOS 7的内核大小大约为5M。内核要能正常运作下去,它需要进行解压释放。
解压释放之后,将创建pid为0的idle进程,该进程非常重要,后续内核所有的进程都是通过fork它创建的,且很多cpu降温工具就是强制执行idle进程来实现的。
然后创建pid=1和pid=2的内核进程。pid=1的进程也就是init进程,pid=2的进程是kthread内核线程,它的作用是在真正调用init程序之前完成内核环境初始化和设置工作,例如根据grub传递的内核启动参数找到init ramdisk并加载。
所谓的救援模式就是刚加载完内核,init进程接收到控制权的那一阶段,因为没有进行任何操作系统初始化过程,所以可以修复和操作系统相关的很多问题。另外,安装镜像中也有内核,可以通过安装镜像进入救援模式,这种进入救援模式的方式几乎可修复任何操作系统启动相关的问题,即使是/boot目录下内核镜像缺失都可以重装。(还有一种单用户模式,它是运行级别为1的环境,所以已经初始化完运行级别,见后文)
加载init ramdisk
在前面,已经创建了pid=1的init进程和pid=2的kthread进程,但注意,它们都是内核线程,全称是kernel_init和kernel_kthread,而真正能被ps捕获到的pid=1的init进程是由kernel_init调用init程序后形成的。
要加载/sbin/init程序,首先要找到根分区,根分区是有文件系统的,所以内核需要先识别文件系统并加载文件系统的驱动,但文件系统的驱动又是放在根分区的,这就出现了先有鸡还是先有蛋的矛盾。
解决的方法之一是像grub2识别boot分区的文件系统一样,将根文件系统驱动模块嵌入到内核中,但文件系统的种类太多,而且会升级,这样就导致内核不断的嵌入新的文件系统驱动模块,内核不断增大,这显然是不合适的。
解决方法之二则像传统grub借助中间过渡引导段stage1_5一样,将根文件系统的驱动模块放入一个中间过渡文件,在加载根文件系统之前先加载这个过渡文件,再由过渡文件跳转到根文件系统。
方法二正是现在采用的,其采用的中间过渡文件称为init ramdisk,它是在安装完操作系统时生成的,这样它会收集到当前操作系统的根文件系统是什么类型的文件系统,也就能只嵌入一个对应的文件系统驱动模块使其变得足够小。如下图,它是安装操作系统时安装完所有软件包后执行的一个收集过程。
在CentOS 5上采用的init ramdisk称为initrd,而CentOS 6和CentOS 7采用的则是initramfs,它们的目的是一样的,但在实现上却大有不同。但它们都存放在/boot目录下。
代码语言:javascript复制[root@xuexi ~]# ll -h /boot/init*-rw-------. 1 root root 19M Feb 25 11:53 /boot/initramfs-2.6.32-504.el6.x86_64.img
可以看到,它们的大小有十多兆,由此也可知道init ramdisk的作用肯定不仅仅只是找到根文件系统,它还会做其他工作。具体还做什么工作,请继续阅读下文。
initrd
initrd其实是一个镜像文件系统,是在内存中划分一片区域模拟磁盘分区,在该文件中包含了找到根文件系统的脚本和驱动。
既然是文件系统,那么内核也必须要带有对应文件系统的驱动,另外文件系统要使用就必须有根”/“,这个根是内存中的”虚根”。由于内核加载到这里已经初始化一些运行环境了,所以内核的运行状态等参数也要保存下来,保存的位置就是内存中虚根下的/proc和/sys,此外还有收集到的硬件设备信息以及设备的运行环境也要保存下来,保存的位置是/dev。到此为止,pid=2的内核线程kernel_kthread就完成了基本工作,开始转到kernel_init进程上了。
再之后就是kernel_init挂载真正的根文件系统并从虚根切换到实根,最后kernel_init将调用init程序,也就是真正的能被我们看见的pid=1的init进程,然后将控制权交给init,所以从现在开始,将切换到用户空间,后续剩余的事情都将由用户空间的程序完成。
以下是CentOS 5.8中initrd文件的解压过程和解包后的目录结构。
[root@localhost ~]# cp /boot/initrd-2.6.18-308.el5.img
/tmp/initrd.gz
[root@localhost tmp]# gunzip initrd.gz
[root@localhost tmp]# cpio -id < initrd
[root@localhost tmp]# ls
bin dev etc init initrd lib proc sbin sys sysroot
initramfs
initramfs比initrd先进了一些,initrd必须是一个文件系统,是在内存中模拟出磁盘分区的,所以内核必须要带有它的文件系统驱动,而initramfs则仅仅只是一个镜像压缩文件而非文件系统,所以它不需要带文件系统驱动,在加载时,内核会将其解压的内容装入到一个tmpfs 中。
initramfs和initrd最大的区别在于init进程的区别对待。initramfs为了尽早进入用户空间,它将init程序集成到了initramfs镜像文件中,这样就可以在initramfs装入tmpfs时直接运行init进程,而不用去找根文件系统下的/sbin/init,由此挂载根文件系统的工作将由init来完成,而不再是内核线程kernel_init完成。最后从虚根切换到实根。
那根分区下的/sbin/init是干嘛的呢?可以认为是init ramdisk中init的一个备份,如果ramdisk中找不到init就会去找/sbin/init。另外,在正常运行的操作系统环境下,/sbin/init还经常用来完成其他工作,如发送信号。
其实initramfs完成了很多工作,解开它的镜像文件就能发现它的目录结构和真实环境下的目录结构类似。以下是CentOS 7上initramfs-3.10.0-327.el7.x86_64解包过程和解包后的目录结构。
[root@xuexi ~]# cp /boot/initramfs-3.10.0-327.el7.x86_64.img /tmp/initramfs.gz
[root@xuexi ~]# cd /tmp; gunzip /tmp/initramfs.gz
[root@xuexi tmp]# cpio -id < initramfs
[root@xuexi tmp]# ls -l
total 8
lrwxrwxrwx 1 root root 7
Jun
29
23:28 bin -> usr/bin
drwxr-xr-x 2 root root 42
Jun
29
23:28 dev
drwxr-xr-x 11 root root 4096
Jun
29
23:28 etc
lrwxrwxrwx 1 root root 23
Jun
29
23:28 init -> usr/lib/systemd/systemd
lrwxrwxrwx 1 root root 7
Jun
29
23:28 lib -> usr/lib
lrwxrwxrwx 1 root root 9
Jun
29
23:28 lib64 -> usr/lib64
drwxr-xr-x 2 root root 6
Jun
29
23:28 proc
drwxr-xr-x 2 root root 6
Jun
29
23:28 root
drwxr-xr-x 2 root root 6
Jun
29
23:28 run
lrwxrwxrwx 1 root root 8
Jun
29
23:28 sbin -> usr/sbin
-rwxr-xr-x 1 root root 3041
Jun
29
23:28 shutdown
drwxr-xr-x 2 root root 6
Jun
29
23:28 sys
drwxr-xr-x 2 root root 6
Jun
29
23:28 sysroot
drwxr-xr-x 2 root root 6
Jun
29
23:28 tmp
drwxr-xr-x 7 root root 61
Jun
29
23:28 usr
drwxr-xr-x 2 root root 27
Jun
29
23:28 var
另外,还可以在其sbin目录下发现init程序。
代码语言:javascript复制[root@xuexi tmp]# ll sbin/initlrwxrwxrwx 1 root root 22 Jun 29 23:28 sbin/init -> ../lib/systemd/systemd
六. 操作系统初始化
下文解释的是sysV风格的系统环境,与systemd初始化大不相同。
当init进程掌握控制权后,意味着已经进入了用户空间,后续的事情也将以用户空间为主导来完成。
init的名称是initialize的缩写,是初始化的意思,所以它的作用也就是初始化的作用。在内核加载阶段,也有初始化动作,初始化的环境是内核的环境,是由kernel_init、kernel_thread等内核线程完成的。而init掌握控制权后,已经可以和用户空间交互,意味着真正的开始进入操作系统,所以它初始化的是操作系统的环境。
操作系统初始化涉及了不少过程,大致如下:读取运行级别;初始化系统类的环境;根据运行级别初始化用户类的环境;执行rc.local文件完成用户自定义开机要执行的命令;加载终端;
运行级别
在sysV风格的系统下,使用了运行级别的概念,不同运行级别初始化不同的系统类环境,你可以认为windows的安全模式也是使用运行级别的一种产物。
在Linux系统中定义了7个运行级别,使用0-6的数字表示。
0:halt,即关机
1:单用户模式
2:不带NFS的多用户模式
3:完整多用户模式
4:保留未使用的级别
5:X11,即图形界面模式
6:reboot,即重启
实际上,执行关机或重启命令的本质就是向init进程传递0或6这两个运行级别。