1.5 系统启动流程
1.5.1 系统引导概述
为了更好地了解Linux系统的运行原理,非常有必要了解系统启动的流程。实际上,这也是学习Linux应知应会的内容,在很多Linux系统工程师的职位面试中都会被问及。
来想象一下台式机的启动过程,相信大家都有这样的经验和体会。在按开机电源后,会听到机箱内发出“滴”的一声,接着屏幕上开始打印出一些字符,然后开始显示出图形界面,最后屏幕上会显示需要输入用户名、密码的登录界面。其实,不管是Linux还是Windows,从用户感官上的体验而言,顺序都是大同小异的。本节将详细描述Linux环境下的启动流程,起点是从按下计算机的电源键开始。
首先,计算机会加载BIOS,这是计算机上最接近硬件的软件,各家主板制造商都会开发适合自己主板的BIOS,而BIOS中一项很重要的功能就是对自身的硬件做一次健康检查,只有硬件没有问题,才能运行软件,记住,操作系统也是一种软件。这种通电后开始的自检过程被称为“加电自检”,英文中称为Power On Self Test,简称POST。如果所有的硬件自检通过,一般都会发出一次“滴”的短声提示,说明硬件一切正常。
机器自检通过后,下面就要引导系统了。这个动作是BIOS设定的,BIOS默认会从硬盘上的第0柱面、第0磁道、第一个扇区中读取被称为MBR的东西,即主引导记录。一个扇区的大小是512字节,存放的内容是一段引导程序和分区信息,其中引导程序部分占用446字节,另外64字节是磁盘分区表DPT,最后两字节是MBR的结束位。这512字节的空间内容是由专门的分区程序产生的,比如说Windows下的fdisk.exe,或者Linux下的fdisk命令,所以它不依赖于任何操作系统,而MBR中的引导程序也是可以修改的,所以可以利用这个特性实现多操作系统共存。由于RedHat、CentOS默认会使用Grub作为其引导操作系统的程序,而Grub本身又比较大,所以常见的方式是在MBR中写入Grub的地址,这样系统实际会载入Grub作为操作系统的引导程序。
经过了上面的步骤,第三步就是顺理成章地运行Grub了。Grub最重要的功能就是根据其配置文件加载kernel镜像,并运行内核加载后的第一个程序/sbin/init,这个程序会根据/etc/inittab来进行初始化的工作。其实这里最重要的就是根据文件中设定的值来确定系统将会运行的runlevel,默认的runlevel定义在“id:3:initdefault:”中,其中的数字3说明目前的运行级别定义为3(这里提到了runlevel的概念,将在后面详细讲解)。
第四步,Linux将根据/etc/inittab中定义的系统初始化配置si::sysinit:/etc/rc.d/rc.sysinit执行/etc/rc.sysinit脚本,该脚本将会设置系统变量、网络配置,并启动swap、设定/proc、加载用户自定义模块、加载内核设置等。
第五步是根据第三步读到的runlevel值来启动对应的服务,如果值为3,就会运行/etc/rc3.d/下的所有脚本,如果值为5,就会运行/etc/rc5.d/下的所有脚本。
第六步将运行/etc/rc.local,第七步会生成终端或X Window来等待用户登录。
1.5.2 系统运行级别
前一节多次提到了runlevel这个词,但是runlevel究竟是什么呢?我们说Linux默认有7个运行级,从运行级0到运行级6,每一个运行级所对应的含义如下:
运行级0:关机。
运行级1:单用户模式,系统出现问题时可使用这种模式进入系统维护,典型的使用场景是在忘记root密码时可进入此模式修改root密码。
运行级2:多用户模式,但是没有网络连接。
运行级3:完全多用户模式,这也是Linux服务器最常见的运行级。
运行级4:保留未使用。
运行级5:窗口模式,支持多用户,支持网络。
运行级6:重启。
任何时候Linux只能在一种runlevel下运行。那么不同的runlevel之间到底有什么区别呢?上一节中提到,系统在启动的过程中会根据/etc/inittab中的设定读取runlevel的数值X,并相应地读取和运行/etc/rcX.d(X代表0~6)下所有的脚本。看一下/etc/rc3.d中的内容:
[root@localhost ~]# ll /etc/rc3.d/ total 288 ......(略去内容)...... lrwxrwxrwx 1 root root 15 Oct 7 20:52 K15httpd-> ../init.d/httpd lrwxrwxrwx 1 root root 13 Oct 7 20:55 K20nfs-> ../init.d/nfs ......(略去内容)...... lrwxrwxrwx 1 root root 18 Oct 7 20:50 S08iptables-> ../init.d/iptables lrwxrwxrwx 1 root root 17 Oct 7 20:52 S10network-> ../init.d/network ......(略去内容)......
注意看每行中第9列的内容,分别是以K或S开头、后跟两位数字、再接服务名的文件,其实它们链接的是上层init.d目录中的服务脚本。系统在启动过程中,会首先运行以K开头的脚本,全部运行完毕后再运行以S开头的脚本,在运行所有K开头的脚本时,又会严格按照K后面的数字大小依次来运行,也就是数字小的先运行,数字大的后运行。同样,在运行S开头的脚本时,也是按照这个原则进行的,即先运行数字小的脚本,再运行数字大的脚本。K和S的意思分别是停止(kill)和启动(start),只要定义好不同运行级需要启动和停止的服务,就可以让系统在不同的运行级下启动和关闭不一样的服务。再来对比一下/etc/rc1.d下的关于network项内容:
[root@localhost ~]# ll /etc/rc1.d/ total 288 ......(略去内容)...... lrwxrwxrwx 1 root root 17 Oct 7 20:52 K90network-> ../init.d/network ......(略去内容)......
在运行级为1的时候,network是在开机启动的过程中被关闭的(K90network),而在运行级为3的时候,network则是被开启的(S10network)。
1.5.3 服务启动脚本
上节在介绍Linux运行级时,谈到在Linux启动过程中会使用K或S开头的脚本关闭或启动相关服务,那么这是怎么做到的呢?本节将通过一个脚本帮助大家理解。当然因为这里还没有讲到Shell编程的内容,所以只做非常简单的讲解。
#!/bin/bash #一个bash脚本开始的标记,必须是用“#!/bin/bash”开头,含义是提示系统在运行该脚本时使用 /bin/bash作为执行该文件的解释器 # /etc/rc.d/init.d/atd #说明自己的绝对路径 # Starts the at daemon # # chkconfig: 345 95 5 #345是说在运行级是345的时候,默认开启atd,也就是Start #95是说明当默认设置为on的时候,运行优先级定为95 #5是说明当默认设置为off的时候,停止优先级定为5 # description: Runs commands scheduled by the at command at the time # specified when at was run,and runs batch commands when the load # average is low enough. # processname: atd
# Source function library. . /etc/init.d/functions #使用“.”命令包含文件,可以使用/etc/init.d/functions中定义的函数 # pull in sysconfig settings [-f /etc/sysconfig/atd ] && . /etc/sysconfig/atd test-x /usr/sbin/atd || exit 0 RETVAL=0
# # See how we were called. # prog="atd" start() { # Check if atd is already running if [ !-f /var/lock/subsys/atd ]; then echo-n $"Starting $prog: " daemon /usr/sbin/atd $OPTS && success || failure RETVAL=$? [ $RETVAL-eq 0 ] && touch /var/lock/subsys/atd echo fi return $RETVAL } #定义start函数 stop() { echo-n $"Stopping $prog: " killproc /usr/sbin/atd RETVAL=$? [ $RETVAL-eq 0 ] && rm-f /var/lock/subsys/atd echo return $RETVAL } #定义stop函数
restart() { stop start } #定义restart函数,实际调用时,先执行stop函数后执行start函数 reload() { restart } #定义reload函数,实际调用时,就是执行restart函数 status_at() { status /usr/sbin/atd } #定义status_at函数,实际调用时,是调用/etc/init.d/functions中定义的函数status, 参数为/usr/sbin/atd,也就是查询atd的运行状态 case "$1" in start) start ;; stop) stop ;; reload|restart) restart ;; condrestart) if [-f /var/lock/subsys/atd ]; then restart fi ;; status) status_at ;; *) echo $"Usage: $0 {start|stop|restart|condrestart|status}" exit 1 esac
exit $? exit $RETVAL
上面的脚本实际上是/etc/init.d/atd中的内容,我在脚本中做了一些注释来简单讲解脚本的处理过程。当atd设置为启动时,将会在对应的/etc/rcX.d(X代表0~6)目录下显示:S95atd-> ../init.d/atd,系统根据第一个字母S判定atd需要启动,然后会调用命令/etc/init.d/atd start;当atd设置为关闭时,将会在对应的/etc/rcX.d目录下显示:K05atd-> ../init.d/atd,系统根据第一个字母K判定atd需要关闭,然后调用命令/etc/init.d/atd stop,这样就实现了对atd的启停控制,其他服务也是同样的原理。
1.5.4 Grub介绍
在之前的系统引导概述中,相信大家已经看到Grub这个词了,它的全称为Grand Unified Bootloader,也是GNU赞助的项目之一,事实上Grub可以引导多个操作系统。早先Linux的引导程序是lilo,含义为Linux Loader,这是ext2文件系统中特有的引导程序,现在基本上已经不再使用了。
在之前的系统启动流程中提到,计算机在启动时,BIOS默认会从硬盘上的第0柱面、第0磁道、第一个扇区中读取512字节的数据来引导系统启动,但是Grub这个程序远远大于512字节,这一个扇区又如何能够载下Grub所有的内容呢?为了解决这个问题,实际上Grub的启动是分成两段完成的。第一段以stage1作为主引导程序,它的主要任务是定位和装载第二段引导程序,并转交控制权,即stage2。Grub目录中的内容如下:
[root@localhost grub]# cd /boot/grub/ [root@localhost grub]# ls-l total 257 -rw-r--r-- 1 root root 63 Oct 7 21:02 device.map -rw-r--r-- 1 root root 7584 Oct 7 21:02 e2fs_stage1_5 -rw-r--r-- 1 root root 7456 Oct 7 21:02 fat_stage1_5 -rw-r--r-- 1 root root 6720 Oct 7 21:02 ffs_stage1_5 -rw------- 1 root root 573 Oct 7 21:02 grub.conf -rw-r--r-- 1 root root 6720 Oct 7 21:02 iso9660_stage1_5 -rw-r--r-- 1 root root 8192 Oct 7 21:02 jfs_stage1_5 lrwxrwxrwx 1 root root 11 Oct 7 21:02 menu.lst-> ./grub.conf -rw-r--r-- 1 root root 6880 Oct 7 21:02 minix_stage1_5 -rw-r--r-- 1 root root 9248 Oct 7 21:02 reiserfs_stage1_5 -rw-r--r-- 1 root root 55808 Mar 13 2009 splash.xpm.gz -rw-r--r-- 1 root root 512 Oct 7 21:02 stage1 -rw-r--r-- 1 root root 104988 Oct 7 21:02 stage2 -rw-r--r-- 1 root root 7072 Oct 7 21:02 ufs2_stage1_5 -rw-r--r-- 1 root root 6272 Oct 7 21:02 vstafs_stage1_5 -rw-r--r-- 1 root root 8904 Oct 7 21:02 xfs_stage1_5
注意一下,有一个stage1的文件,大小为512字节,正好是一个扇区的大小。其实这不是一个巧合,stage1确实是MBR的一个副本。还可以看到有很多文件是以stage1_5结尾的,事实上这些文件是各种文件系统的驱动文件,当stage1从不同的文件系统中读取stage2时将用到这些驱动文件。
对Grub的配置可以通过修改Grub的配置文件完成,一般配置文件为/boot/grub/grub. conf。修改后的配置将直接影响下次引导时的行为。下面是系统安装过程中自动生成的配置:
# grub.conf generated by anaconda # # Note that you do not have to rerun grub after making changes to this file # NOTICE: You have a /boot partition. This means that # all kernel and initrd paths are relative to /boot/,eg. # root (hd0,0) # kernel /vmlinuz-version ro root=/dev/sda3 # initrd /initrd-version.img #boot=/dev/sda default=0 timeout=5 splashimage=(hd0,0)/grub/splash.xpm.gz hiddenmenu title CentOS (2.6.18-194.el5) root (hd0,0) kernel /vmlinuz-2.6.18-194.el5 ro root=LABEL=/rhgb quiet initrd /initrd-2.6.18-194.el5.img
其中,default=0的含义是默认从第一个title处启动。这里的配置文件中只有一个title项,但是如果还有第二个title项,则可以配置默认从第二个title处引导系统,只要把default改为1就可以了(注意这里的计数是从0开始的)。
timeout=5的含义是显示这个title项时,同时有5秒倒计时,5秒内可以按回车键提前从默认的启动项中启动,也可以按上下键立即停止倒计时,选定一个title,然后按回车键确认从选定的title中启动。也可以选定某一个title后,按e键进入编辑模式,这样可以即时对Grub进行配置,但是这时的配置并不会写入配置文件中,而只是当时生效。
splashimage是指定启动时的背景图像。如果系统使用的是sata磁盘,则命名规则为:第一块磁盘是sda,第二块磁盘是sdb,以此类推。对磁盘进行分区后的分区命名规则是,第一个磁盘的第一个分区是sda1,第一个磁盘的第二个分区是sda2,第二个磁盘的第一个分区是sdb1,第二个磁盘的第二个分区是sdb2。而Grub使用hd0代表第一块磁盘,而这里(hd0,0)的含义是第一块磁盘的第一个分区。所以(hd0,0)/grub/splash.xpm.gz的绝对路径就是/boot/grub/splash.xpm.gz,这是一个压缩文件,Grub在启动时会自动对该文件做解压缩。
hiddenmenu是设置启动时是否显示菜单。
title是系统引导时显示的名字,这只是一种识别性的文字,可以任意修改。文件的最后3行是相互关联的,第一行root(hd0,0)参数指定了内核放置的分区;第二行kernel/vmlinuz-2.6.18-194.el5 ro root=LABEL=/rhgb quiet指定了内核的路径,表示内核是(hd0,0)分区中的vmlinuz-2.6.18-194.el5文件,ro root=LABEL=/rhgb quiet是启动内核时向内核传入的参数;最后一行initrd/initrd-2.6.18-194.el5.img指定了initrd文件的路径是(hd0,0)中的initrd-2.6.18-194.el5.img文件。
这里第一次提到initrd文件,其英文含义是boot loader initialized RAM disk,也就是boot loader用于初始化的内存磁盘,是系统启动时的临时文件系统,kernel通过读取initrd来获得各种可执行文件和设备驱动,并挂载真实的文件系统,然后卸载这个临时文件系统。在桌面或者Linux服务器中,initrd文件只是一个临时的文件系统,其生命周期很短,只会用作挂载真实文件系统的一个接力,在很多嵌入式系统中,由于不需要外接大存储设备,所以initrd会作为永久的文件系统直接使用。