本节索引
在对系统启动流程进行分析的时候,我想你一定是对系统有了一定的了解。系统的启动目前来讲大都为串行接力的方式来启动。而所谓的并行方式的启动方式也是某一个阶段的并行。所以我按照系统启动的顺序来把文章连缀起来。
* BIOS阶段
* BootLoader阶段
* 内核阶段
* 用户层阶段
BIOS阶段
加载BIOS
当按下开机键后,系统会自动自动加载BIOS,加载的详细过程不再详述,感兴趣的读者可学习微机原理和或对汇编代码分析
BIOS从CMOS芯片中读取硬件配置信息
开机所需要的BIOS设置和用户自定义的设置都保存在CMOS芯片中。
POST自检
POST(power on system test)
BIOS的代码中包含有诊断功能,用以保证重要硬件被正确初始化,比如内存,CPU,以及一些主板上的各种芯片组。如果硬件损坏,用户可通过主板的debug显示或者蜂鸣器的声音来诊断哪些硬件有故障。
加载Bootloader
当POST自检无误后,BIOS会执行一个 INT 13的中断例程来进入加载Bootloader阶段。
INT13 中断会从第一启动项的0磁道1扇区读取BootLoader,把系统启动的任务移交给BootLoader。
Bootloader阶段
Bootloader具体到Linux系统就是grub了。早期使用的LILO以不再使用。
BootLoader的主要作用就是识别和加载操作系统的内核文件,并移交至内存中运行,进而启动操作系统。
磁盘的第一块扇区被称为主引导记录(MBR—Master boot Record ),该扇区的前446字节存储grub第一阶段的代码。把一个庞大的操作系统内核交给446个字节来引导显然是不现实的,所以可以简单理解为第一阶段的grub的任务就是为了加载第二阶段的grub。
第二阶段的grub把对资源的控制权转交给内核镜像。
BootLoader主要功能
- 提供菜单功能,让用户选择操作系统。
- 加载内核文件 。
- 移交给其他的BootLoader(该项有些bootloader不支持,比如Windows的loader)。
- 命令行编辑 当系统BootLoader 加载内核出现故障,用户可在命令行模式下修改启动参数。
以grub为例分析BootLoader阶段多做的那些事:
- BIOS 进行POST自检并无误后,执行INT 13中断,读取对应MBR中前446字节的grub stag1代码并执行。
- 加载boot分区文件系统驱动。Grubstag1阶段存储了grub的预启动信息和grub stag1_5阶段代码地址信息,grub stag1_5 阶段的代码位于磁盘的主引导记录之后的27个扇区中(修复grub时的数据,非标准),该段空间主要存储挂载/boot分区文件系统所需要的驱动程序。一般在系统的/boot/grub目录下会有stag1_5阶段的备份文件。
- 挂载/boot目录,进入grubstag2阶段,读取grub.conf配置文件,根据配置信息,启动或者由用户选择内核启动或者移交给其他其他BootLoader,并提供命令行功能用于手动加载内核。把所有的控制权移交给内核。
注:此阶段的不含有rootfs的概念,寻找内核文件都是以boot为根 |
Grub.conf文件如下:
default=0 timeout=5 title CentOS 6 (2.6.32-696.el6.x86_64) root (hd0,0) kernel /vmlinuz-2.6.32-696.el6.x86_64 ro root=/dev/mapper/vg_centos6-lv_root #以boot为根的内核路径 initrd /initramfs-2.6.32-696.el6.x86_64.img |
内核阶段
Linux内核的设计风格是单内核,单内核设计要支持市面大部分硬件设备的前提下就要把很多的驱动程序编译在内核文件中,会使得内核文件体积异常庞大。但是Linux系统内核在设计上吸收了微内核的一些优点,把大部分的驱动程序,文件系统驱动程序还有一些外围的驱动程序封装成一个个单独的模块,在使用过程中只需动态的加载所需要的模块就好了。
内核模块文件在/lib/modules/`uname -r` 这个目录下,这时候问题就来了,如果内核文件要加载模块驱动,就要先从根分区查找模块位置,而要找到根分区,就要有根文件系统的驱动模块,而模块又在根文件系统下又回到了先有鸡先有蛋的问题。
为了解决这个问题,一种方法是可以把根文件系统的文件系统驱动程序直接编译在内核文件中,但是市场上的Linux发行版本要支持很多文件系统类型,把所有的文件系统驱动程序编译进内核又会让内核文件体积变得非常庞大。为了解决这一矛盾,就有了initrd这个文件,当然这个是最早期的,在centos6版本称为initramfs也称为虚拟文件系统,该文件是在系统安装的最后阶段生成的,只用来加载根文件系统的驱动程序,在内核无法驱动根文件系统的时候就要加载虚拟文件系统,然后在根文件系统下找到对应的驱动模块后在进行根切换。
系统启动内核阶段的步骤
- 加载内核至内存后解压运行,尝试挂载根文件系统,如果挂载成功,动态加载个驱动模块,对周围硬件设备进行探测并进行初始化。
- 如果无法挂载根文件系统,加载initrd虚拟文件系统,然后挂载真正的根文件系统,并切换根,再执行第一步。
至此,Linux内核已经建立起来了。
用户层阶段
内核被加载成功之后,这个系统已经正常运行,这也是最初的Linux,但是作为一个用户是无法直接使用内核的。所以就要初始化启动相关的进程或者服务来供用户来使用。
管理进程的工具多种,以centOS为例,有sysVinit和systemd两种。本文主要分析sysVinit的启动方式。
Systemd是由sysVint进化而来,因此更好的掌握了sysVinit启动方式有助于理解systemd的启动方式
执行init程序
内核在引导完成之后会执行系统的第一个进程init。这时也就正式进入了sysVinit的引导环境。Init之后的所有进程都是由init派生出来,它的PID永远为1
init进程加载inittab配置文件
init进程依据inittab文件来设定运行级别,不同的运行级别可以定义一组不同服务的启动顺序。CentOS5和6版本默认有7个运行级别。详情如下:
# Default runlevel. The runlevels used are: # 0 - halt (Do NOT set initdefault to this) # 1 - Single user mode # 2 - Multiuser, without NFS (The same as 3, if you do not have networking) # 3 - Full multiuser mode # 4 - unused # 5 - X11 # 6 - reboot (Do NOT set initdefault to this) # id:3:initdefault: #默认的运行级别 |
生产环境中默认使用3级别,运行级别的只是用于定义一组不同的启动顺序,用户完全可以自定义,也可以自定义这7种运行级别。
执行rc.sysinit
init在得到运行级别之后并不会立即执行该运行级别的一组服务程序,而执行的第一个程序是/etc/rc.d/rc.sysinit脚本程序。该程序的在centos5的inittab文件中有如下一行:
si:sysinit:/etc/rc.d/rc.sysinit |
这个脚本所做的工作有很多,包括主机名,文件系统,swap,SELinux,udev,内核参数,系统时钟,Raid和LVM等服务的开启。为后续服务启动准备基础环境。
加载rc$runlevel.d下的服务
在rc.sysinit脚本初始化完成之后,会加载默认启动级别下的一组服务。这个时候可以查看对应rc$runlevel.d下的文件,有大量以K和S开头的脚本文件(软连接),跟踪rc.d目录下的rc脚本可以发现这样两个循环结构,仔细研究之后符合以下逻辑。
for i in /etc/rc$runlevel.d/K* ; do $i stop #依次关闭以K开头的服务 done for i in /etc/rc$runlevel.d/S* ; do $i start #依次开启以S开头的服务,即开机自启动 Done |
通过这个结构我们很容易发现服务的启动时顺序执行的,前一个服务脚本没有执行成功的后续脚本就要等待。
通过对rc脚本的分析可以得出对应runlevel下以S开头的脚本是开机启动的,我们可以通过chkconfig命令调整程序是否开启启动或者指定模式下的开机自启动。
跟踪对应模式下的脚本软连接可以,找到服务脚本大多放置在/etc/init.d目录之下。可以使用service 命令来管理这些脚本。
在2345运行级别所对应的rc$runlevel.d目录中都有一个$99local的脚本软连接,该脚本一般是最后一个执行的脚本,该脚本位置在/etc/rc.d/local。所以用户可以把开机后需要执行的操作写在该脚本中。
当然也可以把开机需要的操作定义为服务放置于/etc/init.d目录下,这时需要修改服务脚本的格式。