4. 初始化脚本

内容:

4.a. 运行级别

启动你的系统

当你启动系统时,会发现有很多的文字信息输出。如果注意观察的话,会发现每次启动时这些文字信息都是相同的。所有这些动作的顺序我们称之为启动顺序,而且它们基本上是被静态定义的。

首先,你的引导程序会把你在引导程序配置文件中定义的内核镜像加载到内存中,之后它就告诉CPU可以运行内核了。当内核被加载且运行后,内核会初始化所有内核专有的结构体和任务,并开启init进程。

然后,这个进程确保所有的文件系统(在/etc/fstab中定义的)都已被挂载且能使用。接着,该进程会执行位于/etc/init.d下的一些脚本,这些脚本会启动一些你需要的服务,以使你能获得一个成功启动的系统。

最终,当所有的脚本执行完毕,init将激活终端(大多情况下只是激活虚拟终端,可以使用Alt-F1Alt-F2等来访问),并把一个叫agetty的特殊进程附于其上。这个进程会确保你可以通过运行login从这些终端登录到你的系统中。

初始化脚本

现在init不会随机的执行/etc/init.d下的脚本。甚至,它不会运行/etc/init.d下所有的脚本,它只会执行那些需要被执行的脚本。这个是由/etc/runlevels目录决定的。

首先,init会运行所有/etc/runlevels/boot目录下的符号链接所指向的/etc/init.d目录下的脚本。通常,它会按照字母顺序执行这些脚本,但是有些脚本中含有依赖关系,意味着系统要在执行另一个脚本之后才能运行此脚本。

/etc/runlevels/boot目录所引用的脚本都执行完毕后,init将继续运行/etc/runlevels/default目录下的符号链接所指向的脚本。同样它们会按照字母顺序执行这些脚本,除非一个脚本有依赖关系,那样的话现有次序就会被改变以使启动顺序更加合理。

Init进程是如何工作的

当然init自己不会决定所有的启动顺序。它需要一个配置文件来指定它的工作流程。这个配置文件就是/etc/inittab

如果你还记得我们刚刚描述的启动顺序,你会记得init首先做的是挂载所有的文件系统。这个功能其实是在/etc/inittab这个配置文件中定义好的。如下:

代码 1.1: /etc/inittab中系统初始化行

si::sysinit:/sbin/rc sysinit

这一行告诉init必须运行/sbin/rc sysinit来初始化系统。/sbin/rc脚本是负责系统初始化的,所以你可能会说init它本身并没做太多的事情——它只是把初始化系统任务交给了另一个进程。

接下来,init会执行所有在/etc/runlevels/boot目录下的具有符号链接的脚本。这是由下面这行定义的:

代码 1.2: 系统初始化,承上

rc::bootwait:/sbin/rc boot

同样,rc脚本将完成必要的工作。注意:给rc脚本的参数(boot)和要用的/etc/runlevels的子目录是一样的。

现在init进程将检查配置文件来判断应该运行哪个运行级别(runlevel)。它是通过读取/etc/inittab中的下面这行来决定的:

代码 1.3: 系统默认运行级别行

id:3:initdefault:

在这个例子(绝大部分的Gentoo用户将使用)里,运行级别id为3。根据这个信息,init进程会检查该执行什么来启动运行级别3

代码 1.4: 运行级别的定义

l0:0:wait:/sbin/rc shutdown
l1:S1:wait:/sbin/rc single
l2:2:wait:/sbin/rc nonetwork
l3:3:wait:/sbin/rc default
l4:4:wait:/sbin/rc default
l5:5:wait:/sbin/rc default
l6:6:wait:/sbin/rc reboot

定义了级别3的行将再次调用rc脚本来启动服务(现在参数为default)。请再次注意:rc使用的参数和/etc/runlevels下的子目录是一样的。

rc执行完毕后,init将会决定哪些虚拟终端需要被激活以及每个终端需要运行什么样的命令:

代码 1.5: 虚拟终端定义

c1:12345:respawn:/sbin/agetty 38400 tty1 linux
c2:12345:respawn:/sbin/agetty 38400 tty2 linux
c3:12345:respawn:/sbin/agetty 38400 tty3 linux
c4:12345:respawn:/sbin/agetty 38400 tty4 linux
c5:12345:respawn:/sbin/agetty 38400 tty5 linux
c6:12345:respawn:/sbin/agetty 38400 tty6 linux

什么是运行级别?

你已经看到init使用一种数字的方式来决定需要激活的运行级别。运行级别表示你系统运行的状态,它包含了你进入或退出一个运行级别时需要执行的一组脚本(运行级别脚本或者初始化脚本)。

在Gentoo中定义了七种运行级别:三个内部运行级别和四个用户自定义运行级别。这些内部运行级别分别叫做sysinitshutdownreboot,它们所做的就如同像它们的名字那样:初始化系统、关闭系统和重启系统。

用户定义的运行级别都在/etc/runlevels目录下有同名的子目录:bootdefaultnonetworksingle。运行级别boot会启动所有其他运行级别必须要使用到的系统服务。其余的三个运行级别的不同之处主要在于它们要启动的服务:default是用来日常工作用的;nonetwork是在无网络的情况下使用的;还有single是用户修复系统时用的。

使用初始化脚本

实际上rc进程调用的脚本都称为初始化脚本。每个在/etc/init.d下的脚本都可以在执行时带上以下参数,如:startstoprestartpausezapstatusineediuseneedsmeusesme或者broken

要启动、停止或者重启一个服务(和所有依赖于它的服务),应该用参数startstoprestart。示例如下:

代码 1.6: 启动Postfix服务

# /etc/init.d/postfix start

注意: 只有那些需要给定服务的服务会被暂停或重启。而其他依赖(使用但不需要)于它的服务则不会被暂停或重启。

如果你想要停止一个服务,但是不想影响依赖于它的服务,可以用pause这个参数:

代码 1.7: 停止Postfix服务但又保持相关依赖着的服务继续运行

# /etc/init.d/postfix pause

如果你要查看一个服务的运行状态(如:启动、停止、暂停……),你可以使用参数status

代码 1.8: 查看postfix服务的运行状态

# /etc/init.d/postfix status

如果状态信息告诉你服务正在运行,但是你知道它实际上没有运行,这种情况下你可以使用参数zap将状态信息重设为“停止”:

代码 1.9: 重设postfix的状态信息

# /etc/init.d/postfix zap

有些时候我们也需要知道某个服务的依赖性,这时可以使用参数iuse或者ineed。使用ineed你可以查看这个服务正常工作真正必要的服务,而另一个方面iuse将会显示这个服务可能使用到的所有服务,但这并不一定是正常工作所必需的。

代码 1.10: 列出Postfix服务所依赖的所有必要服务列表

# /etc/init.d/postfix ineed

同样的,我们也可以知道哪些服务需要这个服务(needsme)或者哪些服务可以使用这个服务(usesme):

代码 1.11: 列出哪些服务需要Postfix服务

# /etc/init.d/postfix needsme

最后,我们还可以知道这个服务需要但又缺少的依赖关系:

代码 1.12: 列出Postfix所缺少的依赖关系

# /etc/init.d/postfix broken

4.b. 使用rc-update

什么是rc-update?

Gentoo的初始化系统使用依赖关系树(dependency-tree)来决定什么服务会首先被启动。因为这是个很乏味的工作,我们不想让我们的用户去手动来完成它,所以我们创建了简化运行级别和初始化脚本的管理工具(rc-update)。

使用rc-update你可以从一个运行级别中添加或删除初始化脚本。rc-update工具会自动调用/sbin/depscan.sh脚本来重新创建依赖关系树。

添加和删除服务

在Gentoo的安装过程中你已经添加初始化脚本到“default”运行级别。那时你可能还不清楚“default”是干什么的,但是现在你应该知道了。rc-update脚本需要由第二个参数来决定其行为:adddel或者是show

要添加或删除一个初始化脚本,只需要给rc-update add或者del参数,并随后跟上初始化脚本和运行级别。如下:

代码 2.1: 从default级别中删除Postfix服务

# rc-update del postfix default

rc-update -v show命令将会显示出所有已存在的初始化脚本,并列出它们在哪个运行级别中运行:

代码 2.2: 获得初始化脚本的信息

# rc-update -v show

你也可以运行rc-update show(没有-v参数)来只查看已经启用的初始化脚本和他们的运行级别。

4.c. 配置服务

我们为什么需要额外的配置?

初始化脚本有时候是很复杂的。因此我们不想让用户自己直接编辑初始化脚本,这样将更容易出错。然而能够配置这样的一个服务也挺重要。比如:你可能想给某个服务本身添加更多的选项。

把配置信息独立于脚本之外存放的另一个原因是,你不用担心升级初始化脚本会覆盖掉你之前所做的改动。

/etc/conf.d目录

Gentoo提供了一个简单的方法来配置这样的一个服务:每一个可以配置的初始化脚本在/etc/conf.d里有一个文件。比如:apache2的初始化脚本(叫做/etc/init.d/apache2)有一个配置文件叫/etc/conf.d/apache2,它包含了Apache 2服务器启动时你要给它的选项:

代码 3.1: 在/etc/conf.d/apache2中定义的变量

APACHE2_OPTS="-D PHP5"

这样的一个配置文件包含了变量,且只含有变量(就如同/etc/make.conf一样),可以使得配置服务非常简便。它还允许我们提供更多有关这个变量的信息(以注释形式)。

4.d. 撰写初始化脚本

需要这样吗?

当然不是,自己写一个初始化脚本通常情况下不是必需的,因为Gentoo给已提供的服务准备了一个立刻就能使用的初始化脚本。但是,你可能没有通过Portage来安装一个服务,在这种情况下你通常将需要创建一个初始化脚本。

如果服务软件提供者提供的初始化脚本不是明确地专为Gentoo而写的,请不要使用它,因为Gentoo的初始化脚本和其他发行版的初始化脚本是不兼容的!

布局

一个初始化脚本的基本布局如下所示:

代码 4.1: 初始化脚本的基本布局

#!/sbin/runscript

depend() {
  (依赖关系信息)
}

start() {
  (启动服务所必需的命令)
}

stop() {
  (停止服务所必需的命令)
}

restart() {
  (重启服务所必需的命令)
}

所有的初始化脚本都需要定义函数start()函数。其他所有的部分都是可选的。

依赖关系

在这里我们可以定义两种依赖关系:useneed。我们之前提到过,依赖关系中needuse要更加严格。紧跟着依赖关系类型之后你需要输入你所依赖的服务,或者虚拟依赖关系。

一个虚拟依赖关系是由一个服务提供的,但它不仅仅只有那个服务提供。我们自己定制的初始化脚本可以依赖于一个系统日志服务,但是可能我们有很多可用的系统日志程序(如:metalogd、syslog-ng、sysklogd……)。你不可能同时需要所有的系统日志程序(因为没有一个系统会同时安装和运行所有的系统日志程序),我们确保所有的这些服务都提供了一个虚拟依赖关系。

让我们来看看postfix服务的依赖关系信息:

代码 4.2: Postfix的依赖关系信息

depend() {
  need net
  use logger dns
  provide mta
}

就像你看到的,postfix服务信息如下:

控制执行顺序

在一些情况下你可能并不需要某一个服务,但是,你可能需要这个服务在系统中其他某可能存在的服务之前(或之后)启动(注意这个条件——这不再是依赖关系了)以及运行在同一个运行级别(注意这个条件——只牵扯到同一运行级别的服务)。你可以使用before或者after设置用来提供这个信息。

下面我们来看一下Portmap服务的配置情况:

代码 4.3: Portmap服务的depend()函数

depend() {
  need net
  before inetd
  before xinetd
}

你也可以使用“*”来代替相同运行级别的所有服务,虽然这种做法不是很明智。

代码 4.4: 让初始化脚本在它所属的运行级别中第一个运行

depend() {
  before *
}

如果你的服务需要写入本地磁盘,则需要localmount服务。如果它在/var/run目录下放置文件,例如一个pid文件(pidfile),那么我们需要它在bootmisc之后运行。示例如下:

代码 4.5: depend()函数示例

depend() {
  need localmount
  after bootmisc
}

标准函数

紧随函数depend(),你还需要定义函数start()。它包含了初始化该服务所需要的所有命令。我们推荐使用函数ebegineend函数来通知用户要发生的事情:

代码 4.6: start()函数示例

start() {
  ebegin "Starting my_service"
  start-stop-daemon --start --exec /path/to/my_service \
    --pidfile /path/to/my_pidfile
  eend $?
}

--exec--pidfile都应该用在start和stop函数中。如果这个服务并没有创建一个pidfile,那么如果可能的话就用--make-pidfile创建一个,不过你应该进行必要的测试以确保可行。否则,就不要使用pid文件(pidfile)。你也可以添加--quiet选项到start-stop-daemon中,但是通常我们不建议使用它,除非这个服务输出的信息极端冗长。使用--quiet选项的服务如果启动失败,那么可能会给调试带来困难。

注意: 我们要确保--exec选项实际上调用的是一个服务,而不仅仅是一个启动和退出服务的shell脚本——那是初始化脚本应该做的事。

如果你需要更多的start()函数示例,可以阅读/etc/init.d目录下的初始化脚本源代码。

你还可以定义其他的函数:stop()restart()。但你不是一定要定义这些函数,因为如果我们使用start-stop-daemon时我们的初始化系统可以足够智能地填充这些函数。

虽然你不创建一个stop函数,但下面还是给出了示例:

代码 4.7: stop()函数示例

stop() {
  ebegin "Stopping my_service"
  start-stop-daemon --stop --exec /path/to/my_service \
    --pidfile /path/to/my_pidfile
  eend $?
}

如果你的服务运行了一些其他语言的脚本(例如:bash、python、或者是perl),并且这个脚本之后改了名字(例如:foo.py改为foo),那么你需要给start-stop-daemon添加--name选项。同时,你必须要明确说明改过的名字。在这个例子中,一个服务启动了foo.py脚本,之后该脚本改名为foo

代码 4.8: 一个启动foo脚本的服务

start() {
  ebegin "Starting my_script"
  start-stop-daemon --start --exec /path/to/my_script \
    --pidfile /path/to/my_pidfile --name foo
  eend $?
}

关于start-stop-daemon更多的信息你可以从man手册中得到:

代码 4.9: 获得start-stop-daemon的man帮助

$ $ man start-stop-daemon

Gentoo初始化脚本的语法是基于Bourne Again Shell(bash)的,所以你可以自由地在你的初始化脚本中使用bash兼容的结构。

添加自定义选项

如果你想要初始化脚本支持比我们当前所看到的更多的选项,你可以添加选项名称到opts变量中,然后创建一个与选项同名的函数。示例:支持一个名为restartdelay的选项:

代码 4.10: 支持restartdelay选项

opts="${opts} restartdelay"

restartdelay() {
  stop
  sleep 3    #在再次启动前等待3秒钟
  start
}

服务配置变量

要支持/etc/conf.d里的配置文件,你不需要作任何操作:当你的初始化脚本执行的时候,下面的文件将会被自动source(也就是说可以使用里面的变量):

而且,如果你的初始化脚本提供了一个虚拟依赖关系(如:net),那么和这个依赖关系相关的文件(如:/etc/conf.d/net)也会被source。

4.e. 改变运行级别的行为

谁可能从此获益?

许多笔记本用户会遇到这样的情形:在家时你需要启动net.eth0,而在路上你就不需要启动net.eth0(因为没有网络可用)。使用Gentoo,你可以根据你的意愿来改变运行级别的行为。

例如:你可以创建另一个可以启动的“default”运行级别,并分配其他的一些初始化脚本给它。然后你可以在启动时选择你想要运行哪个“default”运行级别。

使用softlevel

首先,给你的另一个“default”运行级别创建一个运行级别目录。作为一个例子我们创建offline运行级别:

代码 5.1: 创建一个运行级别目录

# mkdir /etc/runlevels/offline

下面我们添加必要的初始化脚本到新创建的运行级别中。示例:如果你需要一个与你当前的default运行级别一模一样的副本,但不包括net.eth0

代码 5.2: 添加必要的初始化脚本

(复制所有在default运行级别中的服务到offline运行级别中)
# cd /etc/runlevels/default
# for service in *; do rc-update add $service offline; done
(删除在offline运行级别中不需要的服务)
# rc-update del net.eth0 offline
(显示在offline运行级别中活动的服务)
# rc-update show offline
(部分输出示例)
               acpid | offline
          domainname | offline
               local | offline
            net.eth0 |

虽然net.eth0已经从offline运行级别中被移除了,udev仍然尝试启动任何它检测到的设备并启动对应的服务。因此,你将需要把每一个你不想启动的网络服务(还有其他任何udev可能启动的设备的服务)加入/etc/conf.d/rc,如下所示。

代码 5.3: 在/etc/conf.d/rc里禁用设备启动的服务

RC_COLDPLUG="yes"
(下面指定你不想自动启动的服务名称)
RC_PLUG_SERVICES="!net.eth0"

注意: 欲知更多有关设备启动的服务的信息,请看/etc/conf.d/rc文件中的注释。

现在可以编辑你的引导程序的配置文件并且添加一个offline运行级别的条目。示例:在/boot/grub/grub.conf中:

代码 5.4: 增加一个offline运行级别的条目

title Gentoo Linux Offline Usage
  root (hd0,0)
  kernel (hd0,0)/kernel-2.4.25 root=/dev/hda3 softlevel=offline

至此,所有的设置都好了。如果你启动你的系统并在启动时选择新添的条目,将会使用运行级别offline而不是default

使用bootlevel

使用bootlevel完全类似于softlevel。仅仅不同的是在这里你要定义另一个“boot”运行级别而不是另一个的“default”运行级别。