http://blog.csdn.net/zhoudaxia/article/details/6666872
1、init程序剖析
init进程是内核引导过程完成时创建的第一个进程。Linux使用了init进程来对组成Linux的服务和应用程序进行初始化。
当 init 进程启动时(使用传统的sysvinit版本),它会打开一个名为 /etc/inittab 的文件。这个文件是 init 的配置文件,定义了如何对系统进行初始化。这个文件还包含了有关出现电源故障时执行的操作(如果系统支持)、以及在检测到 Ctrl-Alt-Delete 键序列时应该如何反应的信息。请参看 清单 1 中该文件的简短片段,了解它所提供的内容。
inittab 配置文件使用通用格式定义了几项内容:id:runlevels:action:process。其中 id 是惟一标识该项的字符序列。runlevels 定义了操作所使用的运行级别。action 指定了要执行的特定操作。最后,process 定义了要执行的进程。
清单 1. inittab 文件摘录
# The default runlevel id:2:initdefault # Boot-time system configuration/initialization script si::sysinit:/etc/init.d/rcS # Runlevels l0:0:wait:/etc/init.d/rc 0 l1:1:wait:/etc/init.d/rc 1 l2:2:wait:/etc/init.d/rc 2 l3:3:wait:/etc/init.d/rc 3 l4:4:wait:/etc/init.d/rc 4 l5:5:wait:/etc/init.d/rc 5 l6:6:wait:/etc/init.d/rc 6 z6:6:respawn:/sbin/sulogin # How to react to ctrl-alt-del ca:12345:ctrlaltdel:/sbin/shutdown -t1 -a -r now
在 init 加载 /etc/inittab 之后,就会将系统切换到 initdefault 操作所定义的运行级别。如 清单 1 所示,即运行级别 2。我们可以将运行级别看作是系统的状态。例如,运行级别 0 定义了系统挂起状态,运行级别 1 是单用户模式。运行级别 2 到 5 是多用户状态,运行级别 6 表示重启。(注意有些发行版对于运行级别的表示是不同的)。还可以以另一种方式考虑运行级别,即它是一种定义可以执行哪些进程(定义系统状态的进程)的方法。
我们可以使用 telinit 工具(这是一个指向 init 工具的链接)与 init 进程进行通信。例如,如果目前在多用户模式下(runlevel 2),希望切换到单用户模式(runlevel 1),使用命令 telinit 1 即可(使用超级用户模式)。要查看系统的当前运行级别,请使用命令 runlevel。
正如 清单 1 定义的一样, initdefault 指定默认的 init 级别是 2 (多用户模式)。在定义初始的运行级别之后,则调用 rc 脚本以及参数 2(运行级别)来启动系统。这个脚本然后会调用各种服务和应用程序脚本来启动或停止特定的元素。在本例中,文件都是在 /etc/rc2.d/ 中定义的。例如,如果要启动 MySQL 应用程序(例如系统启动),可以这样调用:/etc/rc2.d/S20mysql start。在关闭系统时,则使用 stop 参数调用相同的脚本集。
最后,串行执行大量的脚本以启动各种需要的服务(通常可以在 Linux 的引导屏幕中看到)。即使这些服务彼此无关时,依然会顺次启动它们。其结果是引导过程非常耗时(尤其在具有很多服务的大型系统上更是如此)。
关于这个问题的一个很明显的解决方案是去掉 init 命令的串行特性,将其替换成并行化操作。在很多多处理系统中都可以看到这种用法。例如,socket striping,或者使用两个或多个 socket 并行地移动数据,就是一个基于这个主题的解决方案。独立磁盘冗余阵列(RAID)系统也是通过将磁盘分成条状(通常是并行的)来提高 I/O 性能。
修改初始化进程非常的简单。在引导时(使用 LILO 或 GRUB),指定一个新进程来开始处理系统初始化。指定 init=/sbin/mynewinit 作为内核引导行的一部分从而调用这个进程,而不是默认的 init 进程。在 /init/main.c:kernel_init()-->init_post()的内核源代码中您可以看到这种用法。如果在内核引导行中提供了一个 init 命令,引导时就会使用它。否则,内核就会尝试启动 4 个备选方法之一(第一个是 /sbin/init,接着是/etc/init, /bin/init, /bin/sh)。
init进程是由内核启动的第一个也是惟一的一个用户进程,它是后续所有进程的发起者,比如init进程启动/bin/sh程序后,才能够在控制台上输入各种命令。
init执行的基本流程如下:
(1)解析/etc/inittab:执行sysinit命令指定的进程,以前通常是/etc/init.d/rcS,在新版本的init程序中则通常是/etc/rc.d/rc.sysinit脚本。
(2)执行/etc/rc.d/rc.sysinit:这是由init执行的第一个脚本,此步进行的工作包括配置网络、配置内核参数、挂载root文件系统、检查文件系统、设置系统时钟、配置机器、开启交换空间等。
(3)执行/etc/rc.d/rcX.d/[K...][S...]:根据定义的initdefault运行级别,执行对应wait命令指定的程序,这会运行对应目录下的各个程序,并等待它们运行完。在rcX.d目录下,首先终止K开头的服务(用来关闭一个服务),然后启动S开头的服务(用来启动一个服务)。对每一个运行级别来说,在/etc/rc.d子目录中都有一个对应的下级目录。这些运行级别的下级子目录的命名方法为rcX.d, 其中X就是代表运行级别的数字。在各个运行级别的子目录中,都建立有到/etc/rc.d/init.d子目录中命令脚本程序的符号链接,链接的名称在K与S后有一个数字,表示执行顺序,数字小的先执行,例如K01tog-pegasus、S00microcode_ctl。对以K开头的脚本执行时系统会传递stop参数,而S开头的脚本系统会传递start参数。
(4)执行/etc/rc.d/rc.local:Redhat中运行模式2,3,5都把/etc/rc.d/rc.local作为初始化脚本中的最后一个文件,所以用户可以自己在这个文件中添加一些需要在其他初始化工作之后,登陆之前执行的命令。
(5)执行getty程序:为每个联机终端使用fork()创建一个子进程,并在子进程中运行getty程序,init进程则调用wait(),进入等待子进程结束状态。getty程序设置终端类型、属性、速度和线路规程等。对于字符界面的运行级别(如级别2和3),它会打开并初始化一个tty端口,显示提示信息。通常,若/etc/issue文本文件存在,则getty会首先显示其中的文本信息,然后显示登录提示信息(例如“plinux login:” ),出现字符登录界面,并等待用户键入用户名和口令。可以在inittab文件中配置使用哪一种getty程序(在“id:runlevels:action:process”的process部分指定,并可以传递相应的getty参数),如agetty, getty, mgetty, uugetty, mingetty,fbgetty等。getty程序只能由超级用户执行。
注意如果第1步中的inittab文件指定的默认运行级别是图形用户界面形式(如级别5),则init程序会转向去执行/etc/X11/prefdm脚本,它会执行/usr/sbin/gdm,启动图形登录界面。GDM管理的不只是X的启动,还有登录,注销,挂起等一系列操作。
启动登录界面(图形或字符界面),并输入完用户名后,getty会调用login程序。
(6)执行login程序:getty调用exec()执行login程序,以核对输入的用户名和口令。由于调用了exec(而不是fork),login的执行环境会覆盖getty的执行环境。login进程会读取/etc/passwd,以用户名和口令。login根据用户输入的用户名,从口令文件passwd中取得对应用户的登录项,然后调用getpass()以显示”password:”提示信息,读取用户键入的密码,然后使用加密算法对键入的密码进行加密处理,并与口令文件中该用户项中pw_passwd字段作比较。如果用户几次键入的密码均无效,则login程序会以出错码1退出执行,表示此次登录过程失败。此时父进程(进程init)的wait()会返回该退出进程的pid,因此会根据记录下来的信息再次创建一个子进程,并在该子进程中针对该终端设备再次执行getty程序,重复上述过程。
如果用户键入的密码正确,则login就会把当前工作目录(Currend Work Directory)修改成口令文件中指定的起始工作目录。并把对该终端设备的访问权限修改成用户读/写和组写,设置进程的组ID。然后利用所得到的信息初始化环境变量信息,例如起始目录(HOME=)、使用的shell程序(SHELL=)、用户名(USER=和LOGNAME=)和系统执行程序的默认路径序列(PATH=)。接着显示/etc/motd文件(message-of-the-day)中的文本信息,并检查并显示该用户是否有邮件的信息。最后login程序改变成登录用户的用户ID,并执行口令文件中该用户项中指定的shell程序,如/bin/bash或/bin/csh等。有关login程序的一些执行选项和特殊访问限制的说明,可参见Linux系统中的在线手册页(man -8 login)。
(7)执行shell程序或x-windows:如果用户名和口令正确,login调用exec执行shell命令行解释程序(当然,也可以执行X-windows的图形界面,如果用户设置了的话)。登录shell会首先从/etc/profile文件以及$HOME/.bash_profile文件(或.bashrc文件,若存在的话)读取命令并执行。因此用户可以把每次登录时都要执行的命令放在.bash_profile文件中。如果在进入shell时设置了ENV环境变量(或者在.bash_profile文件中设置了该变量),则shell还会从$ENV指定的文件中读去命令并执行。因此我们也可以把每次运行shell都要执行的命令放在ENV变量指定的文件中。设置ENV环境变量的方法是把下列语句放在你起始目录的.bash_profile文件中: ENV=$HOME/.anyfilename; export ENV。
运行shell时,原来的getty进程最终被替换成了bash进程,对应的getty,login,bash这三个程序也就具有相同的进程ID。在成功登录到Linux系统后,你会发现(使用”top”或”ps –ax”命令)自己终端原来的getty进程已经找不到了。因为getty进程执行了login程序,被替换成了login进程,并且最后被替换成你的登录shell进程。对于图形用户界面,login程序最后会被替换成图形界面进程(如gnome-session程序)。
(8)Linux运行时:init进程会负责收取孤儿进程。如果某个进程创建子进程之后,在子进程终止之前终止,则子进程成为孤儿进程。在Linux中所有的进程必须属于单棵进程树,所以孤立进程必须被收取。一旦进程成为孤儿,它会立即成为init进程的子进程。这是为了保持进程树的完整性。
(9)用户注销:当某个终端或虚拟控制台上的用户注销之后,该终端上的所有进程都会被终止(killed),包括bash。然后,init进程就会调用fork为该终端或虚拟控制台重新创建一个getty进程,以便能够让其他用户登录。这是为什么呢?你应该发现,当用户登录时,“getty”用的是“exec”而不是“fork”系统调用来执行“login”,这样,“login”在执行的时候会覆盖“getty”的执行环境(同理,用户注册成功后,“login”的执行环境也会被shell占用)。所以,如果想再次使用同一终端,必须再启动一个“getty”。对于图形界面,用户注销后会回到图形登录界面。
(10)系统关闭:init负责杀死所有其它的进程,卸载所有的文件系统并停止处理器的工作,以及任何其它被配置成要做的工作。
以Fedora 14桌面系统中为例(它使用新的upstart init程序,不过它兼容sysvinit),/etc/inittab文件内容如下:
# inittab is only used by upstart for the default runlevel. # # ADDING OTHER CONFIGURATION HERE WILL HAVE NO EFFECT ON YOUR SYSTEM. # # System initialization is started by /etc/init/rcS.conf # # Individual runlevels are started by /etc/init/rc.conf # # Ctrl-Alt-Delete is handled by /etc/init/control-alt-delete.conf # # Terminal gettys are handled by /etc/init/tty.conf and /etc/init/serial.conf, # with configuration in /etc/sysconfig/init. # # For information on how to write upstart event handlers, or how # upstart works, see init(5), init(8), and initctl(8). # # Default runlevel. The runlevels used are: # 0 - halt (Do NOT set initdefault to this) # 1 - Single user mode # 2 - Multiuser, without NFS (The same as 3, if you do not have networking) # 3 - Full multiuser mode # 4 - unused # 5 - X11 # 6 - reboot (Do NOT set initdefault to this) # id:5:initdefault:
Fedora的默认运行级别为5,是多用户的x-windows图形界面。与传统的sysvinit有所不同,在upstart中,只会为默认运行级别使用inittab文件,要添加其他的运行级别,应该放到/etc/init/rc.conf中,而不是inittab中。upstart系统现在首先运行的是/etc/init/rcS.conf,其内容如下(Fedora 14中):
start on startup stop on runlevel task # Note: there can be no previous runlevel here, if we have one it's bad # information (we enter rc1 not rcS for maintenance). Run /etc/rc.d/rc # without information so that it defaults to previous=N runlevel=S. console output exec /etc/rc.d/rc.sysinit post-stop script if [ "$UPSTART_EVENTS" = "startup" ]; then [ -f /etc/inittab ] && runlevel=$(/bin/awk -F ':' '$3 == "initdefault" && $1 !~ "^#" { print $2 }' /etc/inittab) [ -z "$runlevel" ] && runlevel="3" for t in $(cat /proc/cmdline); do case $t in -s|single|S|s) runlevel="S" ;; [1-9]) runlevel="$t" ;; esac done exec telinit $runlevel fi end script
upstart首先在系统引导时首先运行rc.sysinit脚本,然后搜索inittab的initdefault,用telinit切换到initdefault的级别来运行。upstart把原来/etc/inittab的功能被分散到/etc/init下的各个conf文件中。
注意不同的Linux发行版会对upstart做一些不同的定制。在ubuntu中,甚至默认不再有/etc/inittab文件了。当然他仍然会处理这个文件(如果有的话),如果你有需要,可以创建这个文件,添加需要的内容。Ubuntu 10.04中的/etc/init/rcS.conf内容如下:
# rcS - System V single-user mode compatibility # # This task handles the old System V-style single-user mode, this is # distinct from the other runlevels since running the rc script would # be bad. description "System V single-user mode compatibility" author "Scott James Remnant <[email protected]>" start on runlevel S stop on runlevel [!S] console owner script if [ -x /usr/share/recovery-mode/recovery-menu ]; then exec /usr/share/recovery-mode/recovery-menu else exec /sbin/sulogin fi end script post-stop script # Don't switch runlevels if we were stopped by an event, since that # means we're already switching runlevels if [ -n "${UPSTART_STOP_EVENTS}" ] then exit 0 fi # Switch, passing a magic flag start --no-wait rc-sysinit FROM_SINGLE_USER_MODE=y end script
这里先做一些前期检查,与Fedora不同的是,第一个执行的脚本换成了/etc/init/rc-sysinit.conf,其内容如下:
# rc-sysinit - System V initialisation compatibility # # This task runs the old System V-style system initialisation scripts, # and enters the default runlevel when finished. description "System V initialisation compatibility" author "Scott James Remnant <[email protected]>" start on filesystem and net-device-up IFACE=lo stop on runlevel # Default runlevel, this may be overriden on the kernel command-line # or by faking an old /etc/inittab entry env DEFAULT_RUNLEVEL=2 # There can be no previous runlevel here, but there might be old # information in /var/run/utmp that we pick up, and we don't want # that. # # These override that env RUNLEVEL= env PREVLEVEL= console output env INIT_VERBOSE task script # Check for default runlevel in /etc/inittab if [ -r /etc/inittab ] then eval "$(sed -nre 's/^[^#][^:]*:([0-6sS]):initdefault:.*/DEFAULT_RUNLEVEL="\1";/p' /etc/inittab || true)" fi # Check kernel command-line for typical arguments for ARG in $(cat /proc/cmdline) do case "${ARG}" in -b|emergency) # Emergency shell [ -n "${FROM_SINGLE_USER_MODE}" ] || sulogin ;; [0123456sS]) # Override runlevel DEFAULT_RUNLEVEL="${ARG}" ;; -s|single) # Single user mode [ -n "${FROM_SINGLE_USER_MODE}" ] || DEFAULT_RUNLEVEL=S ;; esac done # Run the system initialisation scripts [ -n "${FROM_SINGLE_USER_MODE}" ] || /etc/init.d/rcS # Switch into the default runlevel telinit "${DEFAULT_RUNLEVEL}" end script
可见Ubuntu是在rc-sysinit.conf中才处理inittab并切换到initdefault级别来运行。
init的完整初始化过程如下(包括启动字符界面和图形界面):
/sbin/init --->/etc/init/rcS.conf --->exec /etc/rc.d/rc.sysinit 执行第一个脚本(Ubuntu中为/etc/init/rc-sysinit.conf) --->/bin/hostname 获取主机名(设置$HOSTNAME) --->/etc/sysconfig/network 配置网络基本参数 --->/proc/mounts 检测并挂载procfs,sysfs到/proc,/sys --->/etc/init.d/functions 包含一些通用函数,会被/etc/init.d(是到rc.d/init.d的链接)下的脚本用到 --->/etc/sysconfig/i18n 设置终端字符集 --->/etc/sysconfig/init 设置终端和图形界面的一些参数 --->deamon(),killproc(),pidofproc() 一些通用函数 --->status(),echo_success(), --->update_boot_stage(),strstr() --->/selinux/enforce 检查SELinux的状态 --->/etc/system-release 打印熟悉的发行版信息 “Welcome to Fedora ..." --->/proc/cmdline 获取内核启动的命令行参数 --->/proc/sys/kernel/modprobe 获取modprobe的位置(为/sbin/modprobe) --->/sbin/sysctl 初始化硬件(通过sysctl设置运行时内核参数) --->kill $nashpid 杀死所有的nash进程(我们在initrd中使用的shell) --->/sbin/start_udev 启动udev((动态设备管理进程) --->/bin/taskset 设置进程的默认CPU亲合值(即优先使用哪个CPU,用在多处理器环境中) --->/etc/sysconfig/modules/*.modules 加载其他用户自定义的模块 --->sysctl -e -p /etc/sysctl.conf 配置内核参数 --->/proc/devices 获取设备号及相应设备名,以便进行设备初始化 --->/sbin/dmraid 激活software raid --->/sbin/kpartx “/dev/mapper/..." 为software raid上的每块硬盘创建设备映射 --->/.autofsck 是否自动执行文件系统检查 --->sulogin 若为单用户模式,执行单用户登录程序 --->plymouth --show-splash 显示启动时的背景画面 --->/etc/sysconfig/readonly-root 设置root文件系统挂载方式 --->从/etc/fstab挂载暂存设备 --->/etc/rwtab, /etc/rwtab.d/* 挂载其他有卷标的分区 --->ip addr show 获取并设置网上ip地址 --->从/etc/fstab挂载持久数据的存储设备 --->/etc/statetab, /etc/statetab.d/* 持载其他持久数据的存储设备 --->/sbin/fsck 检查文件系统 --->umount -a & reboot -f 如果检查失败,卸载文件系统并重启 --->以读写方式重新挂载root文件系统 如果文件系统检查没有失败 --->挂载所有其他的文件系统 --->cat /var/lib/random-seed > /dev/urandom 初始化伪随机数生成器 --->/usr/sbin/system-config-keyboard,passwd,... 配置机器相关参数(如果有需要的话) --->/etc/sysconfig/network 重新读取网络配置数据,并重设hostname --->清除相关的/, /var,/tmp数据 --->/sbin/swapon 开启各个交换区分(根据/proc/swaps) --->/usr/sbin/system-config-network-cmd 执行引导时的网络配置(传递内核启动的netprofile参数) --->dmesg -s 131072 > /var/log/dmesg 转储内核启动的消息信息 --->/etc/inittab --->id:5:initdefault: 查找initdefault定义的运行级别(为5,图形用户界面) --->telinit $runlevel 切换到对应级别运行 --->/etc/init/rc.conf --->exec /etc/rc.d/rc $RUNLEVEL --->/etc/profile.d/lang.sh 设置语言环境 --->/etc/rc.d/rc5.d/KNxxxx 先关闭相关服务(在关闭系统时也会执行) --->/etc/rc.d/init.d/xxxx --->/etc/rc.d/rc5.d/SNxxxx 再开启相关服务 --->etc/rc.d/rc5.d/xxxx --->/etc/rc.d/rc.local 在所有init脚本运行完之后运行,可在些添加自己的初始化命令(Ubuntu中为/etc/rc.local) --->/etc/init/start-ttys.conf 启动tty1-tty6设备 --->/etc/sysconfig/init 指定tty设备,通常为/dev/tty1-/dev/tty6 --->/etc/init/tty.conf --->exec /sbin/mingetty $TTY 在每个tty设备上启动mingetty --->成功后就可以通过Ctrl+Alt+F1..F6在各个不同的tty之间切换 ################################################# 字符界面 ################################################ --->fork()--->/sbin/mingetty 运行mingetty程序,出现字符登录界面 --->/etc/issue 在登录界面上显示发行版信息 --->exec("/bin/login",...) 运行/bin/login程序,验证用户名和口令 --->/etc/passwd 读取passwd文件核对用户名和口令 --->jackzhou:x:500:500:jackzhou:/home/jackzhou:/bin/bash --->切换到工作目录/home/jackzhou --->初始化环境变量$HOME,$PATH等 --->/etc/motd 显示当天的消息 --->检查新邮件 --->exec("/bin/bash",...) 运行bash程序 --->/etc/profile 执行这些脚本中的命令 --->.bash_profile或.bashrc --->ENV=$HOME/.anyfilename; export ENV 运行$ENV指向的脚本(如果设置了的话) --->bash运行中 mingetty,login最后替换成了bash,登录成功 ################################################## 图形界面 ############################################# --->/etc/init/prefdm.conf --->exec /etc/X11/prefdm -nodaemon 准备启动指定的X图形界面(X Display Manager) --->/etc/sysconfig/i18n 设置语言环境 --->/etc/sysconfig/desktop 读取指定的DM配置(如果有的话) --->exec /usr/sbin/gdm 启动指定的DM(gdm, kdm, wdm或xdm,默认为/usr/sbin/gdm) --->启动X server窗口 --->/etc/gdm/custom.conf 根据配置在X窗口中显示登录界面 --->用户选择语言、键盘布局、会话等 --->/usr/share/xsessions/gnome.desktop 读取会话要显示的名称 --->Exec=gnome-session 指定默认的会话程序 --->用户输入用户名和密码 --->用/bin/login验证用户名和密码 --->/etc/gdm/PreSession/* 执行会话前的一些任务(比如更改X窗口的默认背景) --->/etc/gdm/PostLogin/* 执行一些登录后立即需要运行的命令 --->/etc/gdm/Xsession gnome-session--->/etc/X11/xinit/Xsession 启动GNOME会话 --->/etc/X11/xinit/xinitrc-common 导入Xsession与xinitrc共用的代码 --->/etc/profile.d/lang.sh 设置i18n环境 --->/etc/X11/Xresources 读取用户登录时需要载入的全局资源 --->/etc/X11/Xmodmap 读取的全局的键盘配置(用于xdm和xinit,用startx启动图形界面时要用到) --->/etc/X11/xinit/xinitrc.d/* 运行所有的xinitrc脚本 --->exec -l $SHELL -c gnome-session 执行特定的环境设置(以前是执行./Xclients.d/Xclients.gnome-session.sh) --->/etc/X11/xinit/Xclients 运行各个X客户端的脚本(或者$HOME/.xsession,或者$HOME/.Xclients) --->/etc/sysconfig/desktop 读取指定的会话程序配置(如果有的话) --->exec "$(type -p gnome-session)" 默认运行gnome-session,进入GNOME桌面 --->GNOME桌面运行中 mingetty,login最后替换成了gnome程序,登录成功 --->/etc/gdm/PostSession/* GNOME会话结束时运行的脚本 #################################### 在字符界面下通过startx启动图形界面 ############################################## --->/bin/bash 在字符界面的Shell下 --->/usr/bin/startx --->记录$HOME目录和/etc/X11/xinit下的.xinitrc和.xserverrc文件 以$HOME目录下的为优先 --->解析用户指定的client、server、display参数及其选项 --->没有指定参数时就设为前面记录的.xinitrc和.xserverrc文件 --->XAUTHORITY=$HOME/.Xauthority 设置XAUTHORITY环境变量 --->设置X server的权限信息 --->xinit $client $clientargs -- $server $display $serverargs 启动X server和第一个X client --->/etc/X11/xinit/xinitrc 用来运行各个X client(上面没有指定第一个client时) --->/etc/X11/xinit/xinitrc-common 导入Xsession与xinitrc共用的代码 --->/etc/profile.d/lang.sh 设置i18n环境 --->/etc/X11/Xresources 读取用户登录时需要载入的全局资源 --->/etc/X11/Xmodmap 读取全局的键盘配置 --->/etc/X11/xinit/xinitrc.d/* 运行所有的xinitrc脚本 --->/etc/X11/xinit/Xclients 运行各个X client的脚本(或者$HOME/.Xclients) --->/etc/sysconfig/desktop 读取指定的会话程序配置(如果有的话) --->exec "$(type -p gnome-session)" 默认运行gnome-session,进入GNOME桌面 --->GNOME桌面运行中 mingetty,login最后替换成了gnome程序,登录成功 --->/etc/gdm/PostSession/* GNOME会话结束时运行的脚本
注意在rc.sysinit加载完/etc/sysconfig/modules/中(如果你希望额外加载一些比如遥控器之类的模块,你可以在这里增加脚本)的用户自定义的模块后,就会由update_boot_stage通知图形化的启动界面,准备进入启动画面,内核启动的命令行参数(在grub中可以看到)中会指定rhgb程序。rhgb程序的作用是在启动的时候建立一个临时的仅使用loopback网络的X窗口服务器,然后在这个窗口上显示启动进度,init程序的其他部分可以通过rhgb-client程序向这个进度窗口发送消息。在“配置机器相关参数”这一步中,如果存在/.unconfigured文件,会先调用rhgb-client向进度窗口发送消息,然后调用/usr/bin/system-config-keyboard配置键盘,调用 /usr/bin/passwd root配置超级用户密码,调用/usr/sbin/system-config-network-tui配置网络,调用/usr/sbin/timeconfig配置时区,调用 /usr/sbin/authconfig-tui --nostart配置网络登录,调用/usr/sbin/ntsysv配置默认的运行级别。然后清空包括/var/lock/,/var/run/, /tmp等在内的临时目录,并开启交换空间。运行完rc.sysinit后,rcS.conf就会查找inittab中的默认运行级别并切换到这个级别。
转到rc.conf,它调用/etc/rc.d/rc脚本,运行指定级别目录下的各个启动脚本。首先按照名称顺序运行那些K打头的脚本,然后按照名称顺序运行那些S打头的脚本。启动脚本(是符号链接)中的数字是怎么来的呢,它是由你指定的,如果你要增加自己的启动脚本到相应的启动级别中去,这个数字当然应该由你指定,因为只有你才知道这个脚本应该以什么样的优先级启动。但是对于那些已经存在的启动脚本,它们的优先级是在脚本中最前面的注释行中的chkconfig这一行指定的,在这一行中,你可以看到类似# chkconfig: 35 99 95的字样,它的含义是:这个脚本应该增加到运行级别3和运行级别5中,启动的优先级是99,关闭的优先级是95,当然,这些数字是由那些Fedora的开发者测试过没有问题,所以才写在这里的。二进制的程序/sbin/chkconfig将会读取这一行,并且在将服务增加到启动级别中去的时候自动生成文件名。文件名中的第一个字符S和K代表了什么含义呢?它代表了你在services控制面板中选择了打开这个服务还是关闭这个服务,如果你在那里打开了这个服务,则以S作为前导符,否则为K。结合我们前面介绍的启动过程,你就可以知道,在启动的时候,Fedora会首先保证那些K打头的脚本是关闭的(通过以stop参数调用这个脚本),然后才会逐个启动那些S打头的脚本(通过以start参数调用这个脚本)。对于每个启动脚本文件,如果想知道启动了时候都做了些什么,可以查看相应脚本中的start()函数,比如对于avahi-dnsconfd这个脚本,我们可以看到,它只是运行了/usr/sbin/avahi-dnsconfd -D这个命令。
最后执行的初始化脚本是rc.local,你可以在这个脚本中添加自定义的需要启动的服务或需要执行的命令。在所有需要启动的服务都启动完毕以后,rc脚本通过rhgb-client程序通知rhgb图形界面退出,rhgb的使命就完成了。
init程序在运行完rc.local后,执行/etc/init/start-ttys.conf的配置,它查找init配置文件中的$ACTIVE_CONSOLES指定的每个tty设备(为tty1-tty6),并调用tty.conf启动这些tty设备。tty.conf会在/dev/tty1-/dev/tty6设备上启动/sbin/mingetty。从现在开始,你可以通过Ctrl+Alt+F1..F6在各个不同的tty之间进行切换了。
上面“fork()--->/sbin/mingetty”这一部分是指传统字符界面的启动及登录过程,这个过程在前面已经介绍比较清楚了。现在的Linux桌面系统都是登录到图形用户界面,登录屏幕是图形界面的形式。下面“/etc/init/start-ttys.conf”这一部分是指图形界面的启动和登录过程。
1)直接启动图形界面
在启动mingetty后,如果运行级别为5,init程序会执行prefdm.conf的配置,它调用/etc/X11/prefdm脚本,准备启动图形界面登录管理器。prefdm将会读取位于/etc/sysconfig/desktop中的配置文件,如果没有指定任何配置文件,prefdm运行的顺序依次为gdm,kdm,wdm和xdm,从而出现图形登录界面。后面的启动部分就属于GNOME,KDE或者其它相应的窗口管理器了。
注意,对于Ubuntu,默认运行级别为2,在/etc/rc2.d目录中包含了启动登录管理器gdm的脚本。
无论是gdm,xdm还是kdm,所做的事情都是类似的,即启动一个X server窗口,基于这个X窗口提供一个图形化的用户登录界面,以便在实际进入X窗口系统之前,对用户进行验证,并且提供用户选择自己希望的语言,窗口管理器等的机会。除此之外,dm程序一般还支持别的一些操作,比如提供直接关机的选项以及根据配置决定是否打开XDMCP服务的端口等。gdm的配置定义在/etc/gdm/custom.conf中。
XDMCP服务是X窗口显示管理器控制协议的缩写,它允许用户在远程电脑上运行X窗口服务,然后通过XDMCP协议使用本地的XDM登录,登录以后的后续操作将使用远程的X窗口作为显示系统。一个很简单的例子,首先使用gdmsetup程序(系统管理菜单中的登录窗口)打开XDMCP的支持(远程选项卡更改为与本地相同),然后打开一个终端窗口,运行Xnest :1 -query 127.0.0.1命令(Xnest并不是默认安装的命令),你将在一个新开的窗口中看到和你的登录屏幕一模一样的登录屏幕,你可以登录其它用户,进行所有和本地用户一样的操作。显然如果你是在另外一台电脑上,只需要把相应的ip地址改掉就可以了。并不一定非要使用Xnest程序,你甚至可以在远程的Win32系统上进行基于XDMCP的远程登录,这首先需要你在你的windows系统上运行一个X 窗口系统,有很多种类似的实现,包括X-win32和cygwin在内的各种免费和收费版本都是一个不错的选择,事实上,一台强劲的服务器通过这种方法可以将N台落魄的486PC转变成可以运行高级科学运算的X终端。除了这些方法,你还可以使用内置于gnome之中的vino程序,这个程序可以基于本地的X窗口打开一个兼容于vnc的服务,你可以使用各种类型的vncviewer来连接这个服务并进行远程操作(参见首选项菜单中的远程桌面),这种实现方式下,远程显示的屏幕和本地屏幕是完全相同的。或者你也可以使用单独的vncserver,这种使用方式和XDMCP的使用方法类似,只是登录的用户和使用的窗口管理器都是由vncserver指定好的。
在登录界面上,用户可以选择语言、键盘布局、会话等。系统内建的几个会话包括安全模式终端,安全模式gnome以及上一次的成功登录等,其它的会话则是从配置文件中读取的,gdm将会在多个目录中寻找设定的会话,包括/etc/X11/sessions/,/usr/share/gdm/BuiltInSessions/,/usr/share/xsessions/等,路径可以通过daemon/SessionDesktopDir配置项进行更改,gdm在这些目录中寻找扩展名为desktop的文件,比如默认会话对应的文件是 /usr/share/gdm/BuiltInSessions/default.desktop,而gnome会话对应的文件为/usr/share/xsessions/gnome.desktop。这些配置文件定义了在不同的语言中这个会话要显示的名称。
当用户选择了一个X会话,输入了正确的用户名和密码以后,gdm执行命令的顺序依次是,首先它将执行位于daemon/PreSessionScriptDir配置项路径下(默认为/etc/gdm/PreSession/)的所有脚本文件,来执行启动会话前的一些任务,比如更改X窗口的默认背景之类,然后它将调用位于daemon/PostLoginScriptDir配置的目录中(默认为/etc/gdm/PostLogin)的脚本,执行一些在刚刚登录以后需要运行的命令,然后它将以前面提到的desktop文件中定义的exec参数的值作为参数,调用daemon/BaseXsession配置项指定的脚本(默认为/etc/gdm/Xsession),比如如果你选择的是默认会话,那么执行的命令将会是/etc/gdm/Xsession default,如果你查看这个文件你将发现,在这种情况下,它将首先检查是否存在主目录的.xsession文件,如果存在就执行它,否则检查是否存在主目录下的.Xclients文件,如果存在则执行它,否则就将执行/etc/X11/xinit/Xclients文件,这个文件根据 /etc/sysconfig/desktop配置文件中的设置运行各个X client,第一个X client默认为执行gnome-session。
运行完gnome-session,我们就进入了GNOME桌面环境。而配置在daemon/PostSessionScriptDir配置项(默认值为/etc/gdm/PostSession/)所设定的目录中的脚本将在GNOME会话结束以后运行,这意味着无论出于什么原因,gnome程序已经完全退出了,也许是你选择了注销命令,也许是X窗口崩溃了,如果你有这方面的需要,可以将相应的脚本放在对应的目录中。
2)通过startx启动图形界面
还有一种启动图形界面的方式,就是在登录到字符界面后,通过运行startx脚本来启动X图形界面。startx并不使用gdm来启动X窗口,而是用xinit程序启动X。/usr/bin/xinit是一个二进制文件,并非是一个脚本。它的主要功能是启动一个X服务器,同时启动第一个基于X的客户端应用程序。当第一个Client退出时,xinit将杀死X server进程,然后自己终止运行。
参考xinit的man文档,可知其用法为:xinit [[client] options ] [-- [server] [display] options]。其中client用于指定第一个X客户端应用程序,client后面的options是传给这个应用程序的参数,server是用于指定启动哪个X服务器,一般为/usr/bin/X或/usr/bin/Xorg,display用于指定display number,一般为0,表示第一个display,option为传给server的参数。
如果不指定client,xinit会查找HOME(环境变量)目录下的.xinitrc文件,如果存在这个文件,xinit直接调用execvp函数执行该文件,以启动指定的X客户端程序。如果这个文件不存在,那么client及其options默认为: xterm -geometry +1+1 -n login -display :0 。
如果不指定server,xinit会查找HOME(环境变量)目录下的.xserverrc 文件,如果存在这个文件,xinit直接调用execvp函数执行该文件,以启动指定的X服务器。如果这个文件不存在,那么server及其display为: X :0 。如果系统目录中不存在X命令,那么我们需要在系统目录下建立一个名为X的链接,使其指向真正的X server命令(Ubuntu下为Xorg)。
下面是几个关于xinit应用的例子:
(1)xinit /usr/bin/xclock -- /usr/bin/X :0
该例子将启动X server, 同时将会启动xclock。请注意指定client或server时,需要用绝对路径,否则xinit将因无法区别是传给xterm或server的参数还是指定的client或server而直接当成是参数处理。
(2)在HOME下新建.xinitrc文件,并加入以下几行:
xsetroot -solid gray & xclock -g 50x50-0+0 -bw 0 & xterm -g 80x24+0+0 & xterm -g 80x24+0-0 & twm
当xinit启动时,它会先启动X server,然后启动一个clock,两个xterm,最后启动窗口管理器twm。请注意最后一个命令不能后台运行,否则所有命令都后台运行的话xinit就会返回退出,同样的,除最后一个命令外都必须后台运行,否则后面的命令将只有在该命令退出后才能运行。
看到这里,眼尖的人或许早以看出xinit的功能完全可以由脚本来实现,例如要启动X Server和一个xterm,就像xinit默认启动的那样,只需要在新建一个脚本或在rc.local中加入:
X& export DISPLAY=:0.0 xterm
这个实现完全正确,然而却并没有完全实现xinit所具有的功能,xinit的功能就是当最后一个启动的client(如上面第二个例子中的twm窗口管理器)退出后,X服务器也会退出。而我们的脚本实现中当我们退出xterm后并不会退出X server。
startx可以看作是xinit的前端,用法和xinit的基本一样:startx [[client] options ] [-- [server] [display] options]。为什么呢?这是因为startx其实就是一个脚本,它启动X server就是通过调用xinit命令实现的,startx的参数将全部传给xinit。因此,这些参数的意义和xinit的参数是一样的。
下面是两个关于startx命令的简单例子:
(1)startx -- -depth 16:以16位色启动X 服务器。
(2)startx -- -dpi 100:以100的dpi启动X 服务器。
startx会先记录$HOME下的.xinitrc和.xserverrc文件(如果有的话),系统目录/etc/X11/xinit/下的xinitrc和.xserverrc文件。然后解析用户的参数,如果该用户指定了该参数,那么startx就会以该参数来启动xinit,否则就会解析$HOME目录下的.xinitrc和.xserverrc文件,如果该文件不存在,就会解析系统目录下(/etc/X11/xinit/)的xinitrc和xserverrc文件,如果这个文件也不存在,那 startx就将以默认的client(xterm)和server(/usr/bin/X)为参数来启动xinit。
在Fedora 14中,只有/etc/X11/xinit/xinitrc文件,由它来运行Xclients脚本,这个脚本用于运行各个指定的X client,其中的第一个X client即为gnome-session,这就是GNOME桌面环境。从代码可知,xinitrc的功能与Xsession几乎一样,只有一些细微的差别(在Ubuntu中xinitrc是直接调用Xsession的)。
完整的Linux init程序启动过程如下图:
图1 Linux init程序启动过程
2、initng介绍
由于传统的init进程(sysvinit)是一个串行化的进程,因此可对这部分系统进行充分优化。实际上,您可以使用任何方法来对init进程进行优化。其中最简单方法是禁用不必要的服务。例如,如果您运行的是一个桌面系统(而不是一个服务器),就可以禁用诸如 apache、sendmail 和 mysql 之类的服务,这样可以缩短init序列。
其他的一些init程序版本解决了这个问题。一种是基于依赖关系的(即使用依赖关系来提供并行化,如新版的initng),一种是一个基于事件的系统(即进程依赖于事件来表示自己何时启动或停止,如upstart)。
initng(下一代 init)可以完全取代异步启动进程的init,它能更加快速地完成init进程。initng 背后的基本思想是只要满足了服务的依赖关系就可启动。这样系统就可以在 CPU 和 I/O 之间实现较好的平衡。当从磁盘上加载一个脚本或等待硬件设备启动的同时,可以运行另一个脚本来启动另外一个服务。
作为一个基于依赖关系的解决方案,initng 使用自己的初始化脚本集,它们对服务和守护进程的依赖性进行了编码。清单 2 展示了一个示例。这个脚本指定了需要为给定的运行级别启动的服务。该服务具有两个依赖关系,使用 need 关键字定义,分别是 system/initial 和 net/all。在 system/my_service 可以启动之前,这些服务必须是可用的。当这些服务可用时,exec 关键字就开始起作用了。exec 关键字(以及 start 选项)定义了如何使用任何可用的选项启动服务。要停止这个服务,就会使用 exec 关键字以及 stop 选项。
清单 2. 为 initng 定义服务
service system/my_service { need = system/initial net/all; exec start = /sbin/my_service --start --option; exec stop = /sbin/my_service --stop --option; }
您可以使用服务定义对整个系统进行编码,如清单 2 所示。那些没有依赖关系的服务可以立即(并行地)启动,而具有依赖关系的服务则必须等待以安全启动。您可以将 initng 看作一个基于目标的系统。其目标就是要启动的服务。没有进行显式的规划;相反,依赖关系简单地定义了服务初始化的流程,这个过程中隐含着并行化的操作。
initng 的典型安装需要 initng 发行版(源代码或二进制文件)和 ifiles 发行版。您可以使用 ./configure、make 和 make install 编译自己的 initng 发行版。您必须使用 cmake 来编译 ifiles 文件(这是脚本文件)。根据系统需求的不同,您可能需要创建新的服务/守护进程定义(不过很可能 initng 社区中已经有人这样做了)。然后您还必须修改 LILO 或 GRUB 的配置以指向新的 /sbin/initng。要控制 initng,需要使用 ngc(对应telinit 与传统的 init)。它们的语法有些不同,不过功能是相同的。
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 也为跟踪和调试提供了附加功能。