5. 初始化脚本 (Initscripts)
5.a. 运行级别
启动您的系统
在系统启动时, 您会看到许多的文字漂浮而过. 如果您足够留心, 会发现每次重启系统时这些文字的内容其实都是一样的. 这一系列动作被称之为启动流程 (boot sequence) 并且已 (或多或少) 固定地 (statically) 定义下来了.
首先, 您的启动器会把启动器配置文件中定义的内核映象加载到内存中, 此后它告诉 CPU 去运行内核. 当内核已加载并运行, 它将初始化所有内核相关 (kernel-specific) 的结构 (structures) 与任务 (tasks) 并开启 init 进程.
该进程确保所有文件系统 (/etc/fstab 中定义的) 都已挂载并为随后的使用准备就绪. 然后它就执行 /etc/init.d 中的数个脚本, 这些脚本将开始一些服务以让系统成功启动.
最后, 当所有脚本执行完毕, init 会启动终端 (大部分情况下是几个隐藏于 Alt-F1, Alt-F2... 之后的虚拟终端) 并为之分配 (attaching) 一个名为 agetty 的特殊进程. 该进程保证随后您可通过运行 login 由这些终端中登录.
初始化脚本
init 不是说随意地执行 /etc/init.d 中的某个脚本, 更不会把 /etc/init.d 中的所有脚本都给执行了, 而是仅仅运行那些需要运行的. 它通过查看 /etc/runlevels 来决定到底运行那些脚本.
首先, init 运行所有在 /etc/runlevels/boot 中有符号链接到 /etc/init.d 中的脚本. 通常, 它会以字母顺序启动这些脚本的运行, 不过有些脚本里含有依赖性信息, 这将告诉系统必需先运行了某个脚本之后才能运行它们.
在 /etc/runlevels/boot 中引用 (Cure: 都是一些指向 /etc/init.d 下脚本的符号链接) 的脚本都执行完毕之后, init 继续执行所有在 /etc/runlevels/default 中有符号链接 (到 /etc/init.d) 的脚本. 同样, 它会以字母顺序来决定运行的先后顺序, 除非某个脚本声明了依赖性信息, 则会相应地改变运行顺序以保证有效的启动流程 (a valid start-up sequence).
初始化是如何进行的
当然, init 不会全都自己做决定. 它需要一个配置文件以指定都作出那些动作. 这个配置文件就是 /etc/inittab.
如果您还记得我们刚才为您阐释的启动流程, 您应该知道 init 的第一个动作就是挂载所有文件系统. 这在 /etc/inittab 中的这行里定义:
代码清单 1: /etc/inittab 中系统初始化的一行 |
si::sysinit:/sbin/rc sysinit |
该行告诉 init 说它必需先运行 /sbin/rc sysinit 以初始化系统. 大部分工作由 /sbin/rc 脚本去完成, 因此您或许会说 init 其实没干什么 -- 确实, 它把系统初始化的任务都交给 (delegates) 另一个进程去完成了.
接着, init 执行在 /etc/runlevels/boot 中有符号链接的所有脚本. 这在下面这行中定义:
代码清单 2: 系统初始化, 继续 |
rc::bootwait:/sbin/rc boot |
还是由 rc 脚本完成所需的任务. 留意一点, 传递给 rc 的参数名 (boot) 即是 /etc/runlevels 下的子目录名.
现在, init 将查看自己的配置文件以了解需要运行那个运行级别. 它从下面这行中做出决定:
代码清单 3: initdefault 行 |
id:3:initdefault: |
这一情况下 (大部分 Gentoo 用户都会用到), 运行级别的 id 是 3. 据此, init 开始检查都需要运行些什么以启动运行级别 3:
代码清单 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 将决定哪些虚拟终端 (virtual terminal) 应该启用, 以及每个终端上需要运行哪些命令:
代码清单 5: 各个虚拟终端 (virtual consoles) 之定义 |
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 以编号的方式 (numbering scheme) 来决定启动那个运行级别. 一个运行级别是这样一个状态: 您的系统在运行之中, 且包含了一个脚本集合 (运行级别脚本 (runlevel scripts) 或初始化脚本 (initscripts)), 这些脚本在您进入或离开这一运行级别时都要被执行.
Gentoo 中定义了七个运行级别: 三个为内部运行级别 (internal runlevels), 四个为用户自定义运行级别 (user-defined runlevels). 内部运行级别为 sysinit (系统初始), shutdown (系统关闭) 以及 reboot (系统重启), 它们进行的工作名符其实: 初始化系统, 关闭系统以及重启系统.
用户自定义运行级别则在 /etc/runlevels 下有其相应的子目录: boot, default, nonetwork 以及 single. boot 运行级别启动所有系统所需 (system-necessary) 的服务, 它们为其他运行级别所用. 余下的三个运行级别仅在启动的服务上有所区别: default 用以日常使用 (day-to-day operations), nonetwork 用于无须网络连接时, 而 single 则用于需要进行系统修复时.
脚本的使用
rc 脚本所启动的那些脚本称之为初始化脚本 (init scripts). /etc/init.d 下的每个脚本都可以 start, stop, restart, pause, zap, status, ineed, iuse, needsme, usesme 或 broken 为参数运行.
我们使用 start, stop 和 restart 来分别启动, 停止或重启一个服务 (及其依赖的所有服务):
代码清单 6: 启动 Postfix |
# /etc/init.d/postfix start |
注: 仅有那些需要 (need) 该服务的其他服务才会被停止或重启. 而其他依赖 (于它的) 服务 (depending services) (那些会使用该服务却不是非得需要 (need) 它的服务) 则分毫不受影响. |
如果您想停止一个服务, 而不停止依赖于它的其他服务 (but not the services that depend on it), 可以使用 pause 参数:
代码清单 7: 停止 Postfix 但任保持其它依赖 (于它的) 服务继续运行 |
# /etc/init.d/postfix pause |
如果您想查看某个服务的状态 (开始了, 停止了, 暂停了, ...), 可以使用 status 参数:
代码清单 8: Postfix 的状态信息 |
# /etc/init.d/postfix status |
如果状态信息告诉您该服务正在运行, 但您知道它确实没有运行, 则您可以 zap 参数来重置其状态信息为 "stopped (停止的)":
代码清单 9: 重置 postfix 的状态信息 |
# /etc/init.d/postfix zap |
要了解一个服务都有哪些依赖关系, 您可使用 iuse 或 ineed 参数. 以 ineed 参数可以看到那些对该服务正常运作非常必要的其他服务. 而以 iuse 参数则可以看到那些会被该服务所用到的其他服务, 但是这些服务对该服务的正常运作没有影响.
代码清单 10: 列出 postfix 所依赖的必要的其他服务 |
# /etc/init.d/postfix ineed |
类似地, 您可以了解哪些服务需要 (require) 该服务 (needsme) 或会用到 (use) 它 (usesme).
代码清单 11: 列出所有需要 postfix 的服务 |
# /etc/init.d/postfix needsme |
最后, 您还可以查看某个服务需要的却未被满足的 (missing) 依赖关系:
代码清单 12: 列出 postfix 所缺少的依赖 (missing dependencies) |
# /etc/init.d/postfix broken |
5.b. 使用 rc-update
何为 rc-update?
Gentoo 的初始化系统使用一个依赖关系树 (dependency-tree) 来决定首先启动哪些服务. 这是一件冗长乏味的工作, 我们不想让用户们自己手工去做, 于是我们创建了一些工具以简化管理运行级别和初始化脚本的工作.
使用 rc-update 您可从运行级别中添加或移除 (Cure: remove 此处不宜译为删除, 因为仅仅是删掉一个符号链接而已, 而原脚本还在)初始化脚本. 随后 rc-update 工具会自动让 depscan.sh 脚本去重建依赖关系树.
服务的添加与移除
您已在 Gentoo 的安装过程中把一些初始化脚本添加到 "default (默认)" 运行级别了. 那时您未必理解 "default (默认)" 的用处, 现在就应该明白了. rc-update 脚本需要第二个参数来定义其动作: add (添加), del (移除) 或 show (显示).
要添加或移除一个初始化脚本, 只要给 rc-update 传递 add 或 del 参数, 其后的参数分别是初始化脚本的名字以及运行级别. 例如:
代码清单 13: 把 postfix 从默认运行级别中移除 |
# rc-update del postfix default |
rc-update show 命令将列出所有初始化脚本以及它们会在哪个运行级别被执行:
代码清单 14: 获取初始化脚本信息 |
# rc-update show |
5.c. 配置服务
为何需要额外的配置?
初始化脚本会很复杂. 因此, 让用户直接去编辑它们并不是一件多么有趣儿的事, 而且这很容易出错 (error-prone). 重要的是, 应该能够配置某个服务. 例如, 您或许想为某个服务提供更多的操作 (more options).
另一原因则是, 保持这些配置不和初始化脚本混在一块, 有利于更新初始化脚本时不至于担心您自己对配置的变更被覆盖了 (undone).
/etc/conf.d 目录
Gentoo 提供了一个简单的方式以配置服务: 所有可被配置的初始化脚本都在 /etc/conf.d 下有其相应的文件. 例如, apache2 的初始化脚本 (名为 /etc/init.d/apache2) 有其相应的配置文件 /etc/conf.d/apache2, 在这个文件里, 您可以加入在 Apache 2 服务器启动时想为其添加的其他操作.
代码清单 15: /etc/conf.d/apache2 中定义的变量 |
APACHE2_OPTS="-D PHP4" |
这样的配置文件包含了一个个变量 (variables and variables alone) (正如 /etc/make.conf), 这让我们可以非常容易地配置服务. 还可以让我们为变量提供更多的信息 (注释形式).
5.d. 写初始化脚本
真得自己写吗?
否. 因为 Gentoo 已经为所有服务都提供了即时可用 (ready-to-use) 的初始化脚本, 所以通常没啥必要自己去写初始化脚本. 不过, 您也许自己没通过 Portage 而装了某个服务, 这样的情况下, 您或许就得自己创建一个初始化脚本了.
如果该服务的初始化脚本不是专门为 Gentoo 所写的, 那么请不要用它: 因为 Gentoo 的初始化脚本和其他发行版所用的并不兼容!
格式
代码清单 16: 一个初始化脚本的基本格式 |
#!/sbin/runscript depend() { (依赖关系信息) } start() { (启动服务所需的命令) } stop() { (停止服务所需的命令) } restart() { (重启服务所需的命令) } |
任一初始化脚本都必需定义了 start() 函数. 其他部分则可有可无 (optional).
依赖性
您可以定义两种依赖关系: use (使用) 和 need (需要). 如前面提到的, need 依赖关系比 use 要严格. 根据这些依赖类型, 您输入所需的服务, 或实质性 (virtual)依赖 (dependency).
一个实质性依赖由一个服务提供的, 但不仅仅由该服务提供. 您的初始化脚本可以依赖于一个系统日志器 (system logger), 但我们有多个系统日志器可用 (metalogd, syslog-ng, sysklogd, ...). 您不可能需要 (need) 它们中的每一个 (没哪个系统会把这些日志器都装上且运行), 于是, 我们让这些服务都提供同一个实质性依赖.
我们来看看 postfix 服务的依赖信息.
代码清单 17: Postfix 的依赖信息 |
depend() { need net use logger dns provide mta } |
如您所见, postfix 服务:
- 需要 (require) (实质性的) net 依赖 (例如, 由 /etc/init.d/net.eth0 提供)
- 使用 (uses) (实质性的) logger 依赖 (例如, 由 /etc/init.d/syslog-ng 提供)
- 使用 (uses) (实质性的) dns 依赖 (例如, 由 /etc/init.d/named 提供)
- 提供 (provides) (实质性) 的 mta 依赖 (许多邮件服务器会用到)
控制其次序
一些情况下, 您或许不是 (从依赖角度) 非得需要某个服务, 不过就是想让它在其他服务之前被启动 - 如果这个服务可用的话 (留意这一条件 - 不再有任何依赖关系问题) - 并 (和其他服务) 运行于同一运行级别. (留意这一条件 - 仅限于同一运行级别的那些服务). 您可以通过使用 before 和 after 设置来提供这一信息.
我们来看看 Portmap 服务中的例子:
代码清单 18: Portmap 服务中的 depend() 函数 |
depend() { need net before inetd before xinetd } |
您还可以用 "*" 来表示同一运行级别中的所有服务, 尽管我们并不建议这么做:
代码清单 19: 在某运行级别中让初始化脚本第一个被运行 |
depend() { before * } |
标准函数
depend() 函数之外, 您还需要定义 start() 函数. 该函数中包含了初始化一个服务所必需的所有命令. 我们建议您使用 ebegin 和 eend 函数来告知用户目前都在发生些什么事情:
代码清单 20: start() 函数例 |
start() { ebegin "Starting my_service" start-stop-daemon --start --quiet --exec /path/to/my_service eend $? } |
您如果想看更多的 start() 函数的例子, 请阅读 /etc/init.d 目录下那些初始化脚本的源码. 至于 start-stop-daemon, 如果您需要关于它的更多的信息, 则可参看其写得很棒的手册页.
代码清单 21: 查看 start-stop-daemon 的手册页 |
# man start-stop-daemon |
其他可定义的函数包含: stop() 和 restart(). 您不是非得定义这些函数不可! 只要您使用的是 start-stop-daemon, 我们的初始化系统聪明得足以由它自己为您添上这些功能.
添加自定义操作
如果您需要让自己的初始化脚本支持更多的操作, 则应该把这些操作添加到 opts 变量中, 并创建与该操作同名的函数. 例如, 要想支持一个名为 restartdelay 的操作:
代码清单 22: 支持 restartdelay (延时重启) 操作 |
opts="${opts} restartdelay" restartdelay() { stop() sleep 3 # 重启前等待 3 秒 start() } |
服务配置变量
要支持 /etc/conf.d 的配置文件, 您什么都不用做: 在您的脚本被执行时, 以下的文件会被自动读入 (sourced) (即是说, 变量随时可用):
- /etc/conf.d/<your init script>
- /etc/conf.d/basic
- /etc/rc.conf
另外, 如果您的初始化脚本提供了一个实质性依赖 (virtual dependency) (如 net), 则该依赖相关的文件 (如 /etc/conf.d/net) 同样会被读入.
5.e. 改变运行级别的行为
谁将受益?
许多手提电脑用户都知道这一情形: 在家时需要启动 net.eth0, 而在路上时 (因为网络不可用) 则不想启动. Gentoo 中您完全可以按自己的意愿去改变运行级别的行为.
例如, 您可以创建第二个 "default" 运行级别, 且让一些其他的初始化脚本在该级别中运行. 然后您就可以在启动时选择使用哪个默认运行级别.
使用 SOFTLEVEL
首先, 为您的第二个 "default" 运行级别创建相应的运行级别目录. 例如, 我们创建 offline (离线) 运行级别:
代码清单 23: 创建一个运行级别目录 |
# mkdir /etc/runlevels/offline |
然后把需要的初始化脚本都添加到这一新建的运行级别中. 例如, 如果您想该运行级别除了没有 net.eth0 外其他都和原 default 级别一样:
代码清单 24: 添加必要的初始化脚本 |
# ls /etc/runlevels/default acpid domainname local net.eth0 netmount postfix syslog-ng vixie-cron # rc-update add acpid offline # rc-update add domainname offline # rc-update add local offline # rc-update add syslog-ng offline # rc-update add vixie-cron offline |
现在编辑启动器为 offline 运行级别添加新的选项. 例如, 在 /boot/grub/grub.conf 中:
代码清单 25: 为 offline (离线) 运行级别添加选项 |
title Gentoo Linux Offline Usage root (hd0,0) kernel (hd0,0)/kernel-2.4.25 root=/dev/hda3 softlevel=offline |
Voila, 搞定啦. 如果您在启动时选择了新的选项, 则将进入 offline 运行级别而非原来的 default 运行级别.
使用 BOOTLEVEL
使用 bootlevel 与 softlevel 几乎完全一样. 仅有的不同在于, 您这里定义第二个 "boot" 运行级别而非第二个 "default" 运行级别.