3、upstart介绍
upstart是一个基于事件的init的替代程序,这意味着服务的启动和停止都基于事件的通信。 upstart 正在由 Scott James Remnant 进行开发,早期用于Ubuntu发行版,不过它想要成为任何 Linux 发行版上 init 的通用替代程序。现在已经用在了包括Ubuntu、Fedora等主流的Linux系统中了,前面的代码分析就是基于upstart的。由于upstart在Ubuntu中使用最广泛,下面的介绍以Ubuntu 10.04为原型系统。
(1)作业
Upstart init守护进程读取/etc/init目录下的作业配置文件,并使用inotify来监控它们的改变。配置文件名必须以.conf结尾,可以放在/etc/init/下的子目录中。每个文件定义一个服务或作业,其名称按路径名来称呼。例如定义在/etc/init/rc-sysinit.conf中的作业就称为rc-sysinit,而定义在/etc/init/net/apache.conf的作业称为net/apache。这些文件必须是纯文本且不可执行的。
作业文件的每行由一个配置节开始,直到行尾或出现节的结束标志。在每一个节中,一行可以用双引号或单引号结束,也可用反斜杆继续本行的内容。一个节和它的各个参数用空格分开。作业文件中"#"开始的行为注释行。常用的配置节有:
exec COMMAND [ARG]...: 定义作业要运行的主进程,注意若有特殊的字符(如引号或$符)将导致整个命令被传递给Shell来运行。例如 exec /usr/sbin/acpid -c $EVENTSDIR -s $SOCKET。
script ... end script: 定义Shell脚本来运行指定的主进程,该脚本由sh来执行。Shell的-e选项总是被使用,因此任何命令失败将导致脚本终止。注意作业的主进程只能用exec或script节中的一种来定义,不能同时用exec和script配置节来定义。下面script配置节的一个例子:
script
dd bs=1 if=/proc/kmsg of=$KMSGSINK
exec /sbin/klogd -P $KMSGSINK
end script
pre-start exec|script...: 本进程在作业的starting事件完成之后,主进程运行之前执行。通常用来准备相关环境,例如创建必要的目录。
post-start exec|script...: 本进程在作业的started事件触发之前,主进程产生之前执行。通常用来发送必要的命令给主进程,或者用来延迟started事件,直到主进程准备好接收客户端的访问。
pre-stop exec|script...: 本进程在作业被stop on节中的一个事件停止或被stop命令停止时执行。它在作业的stopping事件之前,及主进程被杀死之前执行。通常用来发送必要的shutdown命令给主进程,或调用不带参数的start命令来取消stop。
post-stop exec|script...: 本进程在主进程被杀死之后,作业的stopped事件触发之前执行。通常用来清理相关环境,例如删除临时的目录。
Upstart主要有三种作业:
Task Job: 有确定的生命周期和终止状态,例如删除一个文件。
Service Job: 长期运行的进程,例如守护进程,通常不会自己终止。例如数据库、web服务器、ftp服务器等。
Abstact Job: 没有exec节或script节的作业,这样的作业仍然可以被启动和终止,但是不会被分配PID。这样作业启动后如果没有被管理员终止,会永久的运行。抽象作业只存在于Upstart自己内部,但有时个它非常有用,例如定义“永久运行”的作业,用来同步等。
作业的10种状态:
waiting: 初始状态。
starting: 作业开始启动。
pre-start: 运行pre-start配置节。
spawned: 运行script或exec节。
post-start: 运行post-start节。
running: 运行完post-start节之后的临时状态,表示作业正在运行(但可能没有关联的PID)。
pre-stop:运行pre-stop节。
stopping:运行完pre-stop节之后的临时状态。
killed: 作业要被终止。
post-stop: 运行post-stop节。
作业的状态通过inictl status命令输出的中status域来显示给用户。下面是合法的作业状态转移:
当前状态 目标
start stop
waiting starting n/a
starting pre-start stopping
pre-start spawned stopping
spawned post-start stopping
post-start running stopping
running stopping pre-stop或stopping
pre-stop running stopping
stopping killed killed
killed post-stop post-stop
post-stop starting waiting
例如,如果作业的当前状态是starting,且它的目标是start,则它将转移到pre-start状态。注意作业的状态改变可能会非常快以致于你看不到initctl输出中的相关值。你可以通过把log-priority设为debug或info级别来详细记录状态的改变,更多细节参考initctl log-priority命令。通过启动、终止、重启一个作业或触发一个事件可以引起作业状态的转移。可以用"tail -f"命令查看系统log文件。
(2)事件
作业可以由管理员通过start和stop工具来启动或停止,也可以通过Upstart触发某种事件来启动或终止。Upstart要求您更新初始化配置文件来支持基于事件的操作模式。管理员可能使用initctl emit <event> 来创建一个事件,事件名可以和作业名相同。与事件相关的配置节主要有:
start on EVENT [[KEY=]VALUE]... [and|or...]: 定义能导致作业自动启动的事件集。KEY和VALUE指定环境变量及其值。例如:
start on started gdm or started kdm
start on device-added SUBSYSTEM=tty DEVPATH=ttyS*
start on net-device-added INTERFACE!=lo
stop on EVENT [[KEY=]VALUE]... [and|or...]: 定义导致作业自动停止的事件集。语法与start on类似。
您可以创建新事件。例如,要创建一个名为myevent的任意事件,并使用echo命令表示该事件的接收,请使用下面这个简短的作业:
start on myevent
exec echo myevent received
console output
这段代码指定在接收到myevent事件时将触发该作业。然后代码执行指定的操作(向控制台发出文本)。使用upstart配置(/etc/init/)中给出的文件,可以使用initctl工具触发它:
initctl emit myevent。
upstart使用的作业配置文件的工作方式类似与传统的rc init文件,它们是基于异步事件自发操作的。清单3提供了一个简单的样例配置,它可以接收3个事件: startup启动作业;shutdown和 runlevel-3,停止作业。shell 执行作业的 script 部分的内容(使用 -e 选项来结束出错脚本)。
清单 3. sysvinit rc 2 脚本的简化 upstart 脚本
start on startup
stop on shutdown
stop on runlevel-3
script
set $(runlevel --set 2 || true)
exec /etc/init.d/rc 2
end script
initctl 工具提供了类似于 telinit 的功能,不过增加了一些特定于 upstart 的特性。initctl用来启动或终止作业、列表作业、以及获取作业的状态、发出事件、重启 init 进程,等等。正如您前面看到的一样,您可以使用 initctl 和 emit 选项为 upstart 生成一个事件。list 选项让您可以通过标识作业状态来深入了解系统操作。它告诉您目前正在等待哪些服务,以及哪些服务目前是活动的。initctl 工具还可以显示用于调试而接收的事件。
Upstart的事件数量是没有限制的,但init守护进程和telinit工具定义了一组常用的标准事件。主要有:
starting: 当作业被调度并开始运行时,由Upstart触发。
started: 当作业正在运行时被触发。
stopping: 当作业开始终止时被触发。
stopped: 当作业已经完成时(成功或失败)被触发。
当upstart init进程启动时,它会发出startup事件,这将激活实现了System V兼容性的事件和runlevel事件。随着作业的启动和停止,init守护进程将触发starting, started, stopping, stopped事件。另一个核心事件shutdown则是在系统关闭时发出的。其他核心事件包括ctrlaltdel,它说明您按下了Ctrl-Alt-Delete,或kbdrequest,它用来说明您按下了 Alt-Up(向上箭头)键组合。
以Upstart自己的启动过程为例,其流程如下(Ubuntu中):
1)Upstart执行内部初始化。
2)Upstart自己触发一个单一的称为startup的事件。这个事件触发其余的系统初始化过程。注意没有"startup"作业(因此没有/etc/init/startup.conf文件)。
3)init运行mountall作业(/etc/init/mountall.conf),因为mountall作业中有"start on startup"配置节,满足触发后运行的条件。
4)mountall作业依次触发一系列的事件,包括local-filesystems, all-swap等。
下面是mountall.conf的内容:
# mountall - Mount filesystems on boot
#
# This helper mounts filesystems in the correct order as the devices
# and mountpoints become available.
description "Mount filesystems on boot"
start on startup
stop on starting rcS
expect daemon
task
emits virtual-filesystems
emits local-filesystems
emits remote-filesystems
emits all-swaps
emits filesystem
emits mounting
emits mounted
# temporary, until we have progress indication
# and output capture (next week :p)
console output
script
. /etc/default/rcS
[ -f /forcefsck ] && force_fsck="--force-fsck"
[ "$FSCKFIX" = "yes" ] && fsck_fix="--fsck-fix"
# set $LANG so that messages appearing in plymouth are translated
if [ -r /etc/default/locale ]; then
. /etc/default/locale
export LANG LANGUAGE LC_MESSAGES
elif [ -r /etc/environment ]; then
. /etc/environment
export LANG LANGUAGE LC_MESSAGES
fi
exec mountall --daemon $force_fsck $fsck_fix
end script
post-stop script
rm -f /forcefsck 2>dev/null || true
end script
从内容可以看出,mountall作业用来以正确的顺序挂载文件系统。在mountall作业执行完之后,由于触发了filesystem事件,Upstart接着会去执行rc-sysinit作业,它负责生成System v兼容的runlevel事件。前面我们已经分析过,Ubuntu在rc-sysinit作业中处理inittab并切换到initdefault级别来运行(Fedora是在rcS作业中完成)。
Upstart有三种事件类型:
Signal Event: 非阻塞的,即异步的。触发信号型事件会立即返回,调用者继续往下执行。信号型的意思就是广播者并不关心谁会接收它,也不需要等待是否发生某种事情,它只是用来提供信息,用作通信。使用带--no-wait选项的initctl emit命令来创建信号型事件。例如initctl emit --no-wait mysignal。注意事件触发的非阻塞特性并不会直接影响那些与此事件有关的作业,它只是影响触发者程序,允许其继续执行,而无需等待任何使用这个事件的作业。作业本身的非阻塞特性则会影响作业自己,它使得作业不能被终止或延迟,不能以任何形式持有触发者的操作。
Method Event: 阻塞的,即同步的。它通常与Task job结合使用。方法型事件的行为类似于编程语言中的method或function call,调用者需要等待这个工作的完成。例如initctl emit mymethod,这个方法型事件被同步地触发,调用者需要等待直到initctl命令完成。在mymethod事件上启动的任务可能运行成功,也可能失败,假设有一个作业/etc/init/myapp.conf,如下:
start on mymethod
task
exec /usr/bin/myapp $ACTION
你应该启动myapp作业,并检查这个“方法”是否正常完成:
# inictl emit mymethod ACTION=do_something
[ $? -ne 0 ] && { echo "ERROR: myapp failed"; exit1; }
Hook Event: 阻塞的,即同步的。钩子介于信号和方法之间。它是一种通知,表示系统发生了一些改变,不同于信号,钩子型事件的触发者需要等待作业的完成。因此钩子通常用来标志即将发生的改变一些事情。starting和stopping是钩子型事件,被Upstart触发以表明作业即将启动或即将终止。
注意事件与状态是有区别的,虽然Upstart内部使用状态(这些状态可以通过initctl status和list命令显示给用户看),但事件是配置文件指定作业期望行为的一种方式,starting, started, stopping, stopped是事件,不是状态。这些事件在一些特殊的状态转移发生之前触发。例如,starting事件在与此事件相关的作业实际进行运行队列之前被触发。
(3)作业环境:
env KEY=VALUE: 定义一个缺省的环境变量,其值可以被启动作业的事件或命令覆盖。
export KEY: 把环境变量的值导出到starting, started, stopping, stopped事件中。
(4)服务、任务和重生(respawning)
默认情况下,作业是一个服务,这意味着当作业正在运行时,启动作业的动作就被认为执行完了,当事件以0状态码退出时,服务将重新启动。
task: 这个配置节指定作业是一个任务,而不是服务。这意味着启动作业的动作直到作业本身已经运行或停止了,才算是完成,当事件以0状态码退出时,表示任务执行完成,不再重启。注意start命令和任何starting, stopping事件将被阻塞,直到一个服务正在运行或一个任务已经完成。
respawn: 这个配置节表示服务或任务如果非正常终止,将自动启动。除stop命令外的终止都是非正常的终止。
(5)实例
默认情况下,在同一时间任何作业只允许有一个实例存在,尝试再次启动一个正在运行的作业将导致错误。通过用配置节instance NAME定义不同的实例名,可以允许作业有多个实例存在。只要正在运行的实例中没有同名的,就允许启动这个实例。例如:
instance $CONFFILE
exec /sbin/httpd -c $CONFFILE
instance $TTY
exec /sbin/getty -8 38300 $TTY
(6)文档和外部工具相关
主要的配置节有description, author, version, emits。
(7)进程环境
主要的配置节有console, umask, nice, oom, chroot, chdir, limit等。
console output|owner: 作业的标准输入、输出和错误文件描述符默认是连接到/dev/null的,指定本节则将它们连接到系统控制台/dev/console。若设置了owner,还会作业设为系统控制台的owner,这意味着当按下某些组合键如Ctrl+C时,作业会从内核中接收到相关的信号。
umask UMASK: 设置文件模式掩码。
(8)作业生命周期
启动一个作业的流程:
1)Upstart把作业的目标从stop改成start。正如目标的名字指示的一样,作业(实例)现在尝试启动。目标可以用initctl list和status命令显示。
2)Upstart触发starting事件,指示作业即将启动。这个事件包括两个环境变量:JOB指定作业名;INSTANCE指定实例名,如果启动单一的实例(没有instance配置节),则实例名为空。
3)starting事件完成。
4)如果pre-start节存在,则产生pre-start进程。如果pre-start失败,Upstart把目标从start改成stop,设置表示失败的变量并触发stopping和stopped事件。
5)Upstart产生主进程。即运行script或exec配置节,如果没有script或exec配置节,则Upstart什么也不做。
6)Upstart确定作业的最终PID,可参考expect fork和expect守护r进程。
7)如果post-start配置节存在,则产生post-start进程。。如果post-start失败,Upstart把目标从start改成stop,设置表示失败的变量并触发stopping和stopped事件。
8)Upstart触发started事件。这个事件包含与starting同样的环境变量。对Service job,当started事件完成后,主进程即完全地运行起来了。如果是Task job,则任务执行完成(成功或失败)。
终止一个作业的流程:
1)Upstart把作业的目标从start改为stop。现在作业(实例)尝试终止。
2)如果pre-stop配置节存在,则产生pre-stop进程。如果pre-stop失败,Upstart设置表示失败的变量,并触发stopping和stopped事件。
3)如果作业有script或exec配置节,则终止主进程,首先向主进程发送SIGTERM信号,然后Upstart等待kill timeout秒数(默认为5秒),如果进程仍然在运行,则向进程发送SIGKILL信号,因为进程不能选择忽略此信号,因此能保证进程被终止。
4)Upstart触发stopping事件。这个事件有一系列的相关环境变量,包括:
JOB: 与本事件关联的作业名。
INSTANCE: 实例名。
RESULT: "ok"表示作业正常退出,"failed"表示作业失败,注意退出结果的显示可以用normal exit配置节修改。
PROCESS: 导致作业失败的配置节名称。如果RESULT=ok,则本变量不会被设置。如果设置了,可能值有pre-start, post-start, main(表示script或exec配置节), pre-stop, post-stop, respawn(表示作业产生次数超过了respawn limit配置节设置的限制)。
EXIT_STATUS或EXIT_SIGNAL: 如果作业自己退出则设置EXIT_STATUS,如果由于接收到信号退出则设置EXIT_SIGNAL。如果两个变量都没有设置,则进程在产生的过程中出现了问题(例如指定要运行的命令没有找到)。
5)如果post-stop配置节存在,则生成post-stop进程。如果post-start失败,Upstart设置表示失败的变量并触发stopped事件。
6)Upstart触发stopped事件。当stopped事件完成后,作业即完全终止。stopped事件与stopping事件有相同的环境变量集。
(9)运行级别
Debian/Ubuntu系统的运行级别如下:
0:系统关闭。
1:单用户模式。
2:多用户的包括网络的图形界面模式(默认)。
3:与2相同,但未使用。
4:与2相同,但未使用。
5:与2相同,但未使用。
6:系统重启。
还有两个伪运行级别:
N:以前的运行级别不能确定。
S:单用户模式的别名。
显示运行级别可用runlevel命令,改变运行级别可用reboot, shutdown或telinit。改变默认运行级别可修改rc-sysinit.conf中的DEFAULT_RUNLEVEL变量的值,或在内核启动命令行中加入
DEFAULT_RUNLEVEL=1之类的参数。 传统上,默认运行级别被编码在/etc/inittab中,然而这个文件在Upstart中不再使用(但是为了向后兼容,Upstart仍然支持它)。
(10)系统启动
在前面我们详细地分析过init启动系统的过程(以Fedora为例子),这里我们以Ubuntu为例子,并从Upstart的视角来阐述。在系统引导时,当initramfs文件系统运行起来时(用于设置RAID、解锁加密的文件系统卷等),将会运行/sbin/init并分配PID为1,这样Upstart接过控制权。在默认运行级别2上的启动流程如下:
1)Upstart执行内部的初始化。
2)Upstart触发一个单一的称为startup的事件,这个事件触发其余的系统初始化过程。
3)init运行一些指定了start on startup的作业。这其中最著名的就是mountall作业,用来挂载硬盘和文件系统。
4)mountall作业依次触发一系列的事件,包括local-filesystems, virtual-filesystems, all-swaps等。当系统设备和挂载点可用时,它运行mountall守护程序来完成挂载硬盘和文件系统的工作。
5)virtual-filesystems事件引发udev作业启动。它运行uded守护程序来管理系统的设备,并监控设备的改变。
6)udev作业引发upstart-udev-bridge作业启动。
7)upstart-udev-bridge作业将会在某个点处触发"net-device-up IFACE=lo"事件,以表示本地网络(例如IPv4的127.0.0.0)可用。
8)在最终的文件系统挂载之后,mountall将会触发filesystem事件。
9)由于rc-sysinit作业中有start on filesystem and net-device-up IFACE=lo节,Upstart将会启动rc-sysinit作业(Fedora中没有这个作业,而是仍然由传统的rc-sysinit脚本完成工作)。
10)rc-sysinit作业最后调用telinit命令,格式为 telinit "${DEFAULT_RUNLEVEL}"。
11)telinit命令触发runlevel事件,即执行runlevel RUNLEVEL=2 PREVLEVEL=N。注意这就是telinit所做的全部工作,它自己并不会切换运行级别,而通过runlevel程序实现。
12)runlevel事件引发很多其他的Upstart作业启动,包括/etc/init/rc.conf,它用来启动遗留的SystemV init系统。
(11)系统关闭
在系统关闭过程中,有一些重要的事实需要知道:
1)Upstart决不会关闭自己。Upstart会在系统断电时终止,如果它之前终止过,说明是一个bug。
2)Upstart决不会终止没有stop on配置节的作业。
3)Ubuntu既使用Upstart作业,也使用SysV作业。核心的服务由Upstart处理,一些额外的服务可以在遗留的SystemV模式下运行。这主要是为向后兼容,因此在Ubuntu的Universe和Mutiverse软件库中有大量的软件包,为避免更改每个软件包以使它能在Upstart下工作,Upstart允许使用已经存在的SystemV(还包括Debian兼容的)脚本。
关闭系统需要先执行关机动作,例如在图形用户界面中单击"Shut Down...",运行命令shutdown -h now等。关机的流程如下:
1)假设当前运行级别为2,关机动作将会使Upstart触发runlevel事件,即 runlevel RUNLEVEL=0 PREVLEVEL=2。
2)作业/etc/init/rc.conf将被运行。这个作业调用/etc/init.d/rc,并传递新的运行级别(“0“)。
3)SystemV系统调用/etc/rc0.d/中必要的脚本(都是指向/etc/init.d/中脚本的链接),来终止SystemV服务。
4)其中有一个/etc/init.d/sendsigs脚本,这个脚本中有个do_stop()函数,它负责杀死所有没有被终止的进程(包括Upstart进程)。
(12)系统重启
先要执行重启动作,例如在图形界面中单击"Restart...",运行shutdown-r now 或reboot。重启的流程如下:
1)假设当前运行级别为2,重启动作将会使Upstart触发runlevel事件,即 runlevel RUNLEVEL=6 PREVLEVEL=2。
2)作业/etc/init/rc.conf将被运行。这个作业调用/etc/init.d/rc,并传递新的运行级别(“6“)。
3)SystemV系统调用/etc/rc6.d/中必要的脚本(都是指向/etc/init.d/中脚本的链接),来终止SystemV服务。
4)其中有一个/etc/init.d/sendsigs脚本,这个脚本中有个do_stop()函数,它负责杀死所有没有被终止的进程(包括Upstart进程)。
(13)恢复模式
Ubuntu提供了恢复模式以应对系统出现问题的情况。这由friendly-recovery软件包来处理。
总的来说,Upstart是传统init的一个有趣的替代程序,并且具有一些独特的优点。实际上已经不存在什么理由再使用运行级别了,因为系统将充分利用硬件进行引导。任何没有给出的硬件都不会触发需要它的任务。Upstart 也可以很好地处理热插拔设备。例如,如果在完成系统引导很长时间以后插入了一块 PCMCIA 网卡,那就会生成 network-interface-added 事件。这个事件会引起动态主机配置协议(Dynamic Host Configuration Protocol,DHCP)作业来对这个网卡进行配置,生成一个 network-interface-up事件。当为这个新接口分配一个默认路由时,会生成一个 default-route-up 事件。此时,需要网络接口的作业(例如邮件服务器或 Web 服务器)就可以自动启动(如果接口消失,这些服务将会自动停止)。
编译和安装 upstart 非常简单,并且遵循典型的 configure、make 和 make install 模式。 Upstart 提供了一组示例作业,它们与典型的 init 配置兼容。与 initng 类似,新应用程序必须要根据需求编写自己的作业(可能还需要添加新事件)。不管怎样,部署新的 init 系统都会有一些风险。不过 upstart 的优点当然值得去冒这些风险并执行其他必要的操作。
正如上面介绍的一样,initctl 工具提供了人们对 telinit 所期望的功能。不过 initctl 也为跟踪和调试提供了附加功能。