Linux系统命令及Shell脚本实践指南
上QQ阅读APP看书,第一时间看更新

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会作为永久的文件系统直接使用。