一般情况下,我们是不需要知道Linux启动时候的初始化过程的,这其实是一件挺烦躁的事情。就像格雷厄姆说的,画家不一定需要知道颜料是由什么化学物质组成的。
但是作为程序员同学,其实有时候是没有其他选择的,特别是有强迫症的同学。
我肯定是个有强烈强迫症的同学,这点请不要怀疑。如果身边有同学让我帮他看看程序,找找bug,我首先会将他的代码和命名格式化一遍。
在程序员长成道路上,我遇到过如下问题:
service XXOO start
这样的命令能让XXOO启动,而service XXOO stop
就能让XXOO停止?service是怎么做到stop和start的?还有,为什么有时候提示"XXOO"不能识别?我尝试过好几次,发现同时从大局和细节上把握这些问题,其实还是有些难度的。有些博主写的博客过于粗放,只能做一个大体了解;而有些文档却又太晦涩难懂,让人望而生畏。我曾一度想放弃,但强迫症的天性让我无法抗拒 - 这是命。经过一系列痛苦的翻阅和在这个世界上各种犄角旮旯上扣细节,总算弄明白了这些问题。
这是个人的一个学习总结,但也希望分享给那些和我同样有重度强迫症的同学,不必再次去经历。所以,这篇文章的目的是希望能从一个全局和细节的角度去介绍Linux系统的初始化启动过程。但是由于道行过浅,许多地方其实并不严谨,如有错误还请各位前辈不吝指出。
主要从三个方面尝试介绍和总结一下这方面的知识:
另外,由于我主要对ubuntu比较熟悉,其他linux发行版并不十分了解,所以本文主要记录的是Ubuntu下的两类init系统:system v init 和 Upstart。systemd并不在文章范围之内。
正文开始。
操作系统启动过程中,linux内核加载完成之后,内核初始化的最后一步就是运行 init 程序。init程序负责在系统启动时运行一些服务程序或脚本,来让一些重要和必要的服务开机就能运行起来。系统基本服务程序如network, crond, iptables等和用户安装服务程序如mysqld, nginx等,都是通过init系统来完成开机启动过程。
linux世界中init系统有许多种类,不同的发行版采用了不同的实现。大多数Linux发行版的init系统是和System V相兼容的,被称为"System V init(sysvinit)",这是人们最熟悉的init系统。早期Ubuntu也是使用的sysvinit,但是Ubuntu从6.10开始,开始用 Upstart 替换sysvinit,成为Ubuntu新一代init系统。现在也有一些linux发行版如Fedora、Debian也开始或者计划采用 systemd 来作为init系统。
( System V 是Unix众多版本中的一个分支,于1983年首次发布 )
在2014年Debian项目决定在未来的版本中使用systemd后,马克·沙特尔沃思(Mark Richard Shuttleworth)宣布Ubuntu将开始计划将自身迁移到systemd,以保持与上游一致。但是到目前为止(ubuntu 14.10),ubuntu的默认的init系统还是Upstart,Upstart也兼容sysvinit,所以本文主要介绍"System V init"和Upstart这两种init系统。
( Mark Richard,南非,是Canonical公司的老板,Ubuntu这个分支也是他创立的,这个人还自费两千万美元乘坐宇宙飞船在太空中翱翔了10天。 )
Ubuntu下,init系统程序位于 /sbin/init ,大多数Linux发行版的init程序都位于目录 /sbin/ 或者 */bin/*之下。
先介绍sysvinit中的一个概念: 运行级别(Run Level) 。它是一个数字,代表系统现在处于什么样的运行模式中,sysvinit根据运行级别来判断需要启动哪些服务。常有的运行级别有:
Run level | Name | Description |
---|---|---|
0 | Halt | 关机时进入 |
1 | Single-user mode | 超级用户模式,在系统启动出出错、文件系统出错等情况下进入 |
2 | Multi-User Mode(without networking) | 多用户模式,如果你没有网络接口可以配置进入此模式 |
3 | Multi-User Mode | 多用户模式,我的服务器默认为此运行级别 |
4 | Not used/user definable | 保留,用户也可以使用该运行级别来自定义自己的需求 |
5 | GUI mode | 图形界面模式 |
6 | Reboot | 重启时进入 |
s or S | Single-user/Maintenance mode | 特殊用途 |
另外,介绍两个重要的文件(目录):
sysvinit在启动时,就会读取/etc/inittab文件,获得默认的运行级别(假设为N),然后依次启动/etc/rcN.d/中的相应程序,完成开机的初始化过程。
由于很多程序是需要放在多个运行级别下运行的,所以为了避免冗余,/etc/rcN.d/目录之下放的其实是真正启动程序的软连接,真正的启动程序一般存放于/etc/init.d/之下。例如,在我的机器之下:
$ ls -lh /etc/rc3.d/
total 0
lrwxrwxrwx 1 root root 16 Apr 19 2013 K02puppet -> ../init.d/puppet
lrwxrwxrwx 1 root root 14 Mar 25 2014 K10cups -> ../init.d/cups
lrwxrwxrwx. 1 root root 19 Apr 19 2013 K10saslauthd -> ../init.d/saslauthd
lrwxrwxrwx 1 root root 18 Mar 25 2014 K15svnserve -> ../init.d/svnserve
lrwxrwxrwx 1 root root 16 Dec 12 10:25 K36mysqld -> ../init.d/mysqld
lrwxrwxrwx. 1 root root 20 Apr 19 2013 K50netconsole -> ../init.d/netconsole
lrwxrwxrwx. 1 root root 21 Apr 19 2013 K87restorecond -> ../init.d/restorecond
lrwxrwxrwx 1 root root 15 Mar 25 2014 K89rdisc -> ../init.d/rdisc
lrwxrwxrwx 1 root root 19 Mar 25 2014 K92ip6tables -> ../init.d/ip6tables
lrwxrwxrwx 1 root root 18 Mar 25 2014 K92iptables -> ../init.d/iptables
lrwxrwxrwx 1 root root 17 Jun 16 2014 S01sysstat -> ../init.d/sysstat
lrwxrwxrwx 1 root root 17 Mar 25 2014 S10network -> ../init.d/network
lrwxrwxrwx. 1 root root 16 Apr 19 2013 S11auditd -> ../init.d/auditd
lrwxrwxrwx. 1 root root 21 Apr 19 2013 S11portreserve -> ../init.d/portreserve
lrwxrwxrwx 1 root root 17 Mar 25 2014 S12rsyslog -> ../init.d/rsyslog
lrwxrwxrwx 1 root root 14 Dec 12 10:25 S13sssd -> ../init.d/sssd
lrwxrwxrwx. 1 root root 20 Apr 19 2013 S22messagebus -> ../init.d/messagebus
lrwxrwxrwx 1 root root 15 Mar 25 2014 S25netfs -> ../init.d/netfs
lrwxrwxrwx 1 root root 19 Mar 25 2014 S26udev-post -> ../init.d/udev-post
lrwxrwxrwx 1 root root 14 Mar 25 2014 S55sshd -> ../init.d/sshd
lrwxrwxrwx 1 root root 17 Jun 16 2014 S80postfix -> ../init.d/postfix
lrwxrwxrwx 1 root root 15 Jun 16 2014 S90crond -> ../init.d/crond
lrwxrwxrwx. 1 root root 13 Apr 19 2013 S95atd -> ../init.d/atd
lrwxrwxrwx 1 root root 11 Mar 25 2014 S99local -> ../rc.local
你可能觉得这些程序(软连接)的命名方式有点奇怪,是的,它很奇怪,但是sysvinit就是用程序的文件名来存储程序的一些简单控制信息。程序文件名的格式为: S/K + NN + NAME。系统进入默认运行级别时,init会杀掉所有以K开头的程序,启动以S开头的程序,按照NN的大小,从低到高开始启动/停止程序。NAME则是程序的名字,也是启动之后进程的名字。Sysvinit通过这种命名,来达到控制启动顺序的目的。
我的机器默认的运行级别是3(Multi-User mode),所以开机启动的时候会启动或停止 /etc/rc3.d/目录下面的程序。根据前面的规则,进入该运行级别时,这些进程如果在运行的话,会被依次关闭:puppet -> cups/saslauthd -> svnserve等。这些程序会被依次启动:sysstat -> network -> auditd等。
值得特别注意的是其中的/etc/rc.local程序,这是一个可执行shell脚本,不仅仅在运行级别3(Multi-User mode)下有,在级别2(Multi-User mode without networking)和级别5(GUI mode)都会有。所以只要机器正常开机,这个脚本就会自动运行。一般情况下该脚本内容为空,如果你需要将一些程序加入开机自启的话,就将程序命令增加到这个脚本中就可以了。
以上就是sysvinit的初始化过程。
其实上面是一个抽象和简化版的初始过程,更加本质的初始过程是这样的(可跳过):
典型的/etc/inittab的配置是这样的:
# Level to run in
id:2:initdefault:
# Boot-time system configuration/initialization script.
si::sysinit:/etc/rc.sysinit
# What to do in single-user mode.
~:S:wait:/sbin/sulogin
# /etc/init.d executes the S and K scripts upon change
# of runlevel.
#
# Runlevel 0 is halt.
# Runlevel 1 is single-user.
# Runlevels 2-5 are multi-user.
# Runlevel 6 is reboot.
l0:0:wait:/etc/rc 0
l1:1:wait:/etc/rc 1
l2:2:wait:/etc/rc 2
l3:3:wait:/etc/rc 3
l4:4:wait:/etc/rc 4
l5:5:wait:/etc/rc 5
l6:6:wait:/etc/rc 6
# What to do at the "3 finger salute".
ca::ctrlaltdel:/sbin/shutdown -t3 -r now
# Runlevel 2,3: getty on virtual consoles
# Runlevel 3: mgetty on terminal (ttyS0) and modem (ttyS1)
1:23:respawn:/sbin/mingetty tty1
2:23:respawn:/sbin/mingetty tty2
3:23:respawn:/sbin/mingetty tty3
4:23:respawn:/sbin/mingetty tty4
S0:3:respawn:/sbin/agetty ttyS0 9600 vt100-nav
S1:3:respawn:/sbin/mgetty -x0 -D ttyS1
其中这几行的作用就是:在系统进入N运行级别时,执行命令 "/etc/rc.d/rc N":
l0:0:wait:/etc/rc 0
l1:1:wait:/etc/rc 1
l2:2:wait:/etc/rc 2
l3:3:wait:/etc/rc 3
l4:4:wait:/etc/rc 4
l5:5:wait:/etc/rc 5
l6:6:wait:/etc/rc 6
rc
这个程序按照文件命名,按序启动或停止 /etc/rcN.d/ 目录下相应的程序。所以,真正操作/etc/rcN.d/目录下程序的启动和停止的其实是rc,并不是init程序。但是我们任然可以把这个过程归结于"system v init"系统的功能。
关于inittab的详细介绍可以看这里
将一个编好的服务程序(作为守护进程存在,提供服务,比如nginx/sshd),作为固定服务加入系统中的话,在sysvinit中这样做就好了:
举例来说,假设是你编写了一个nginx的服务,现在要将其添加进系统启动服务中:
在/etc/rc2.d/、/etc/rc3.d/、/etc/rc5.d/中创建启动软连接:
# ln -s /etc/init.d/nginx /etc/rc2.d/S20nginx
# ln -s /etc/init.d/nginx /etc/rc3.d/S20nginx
# ln -s /etc/init.d/nginx /etc/rc5.d/S20nginx
在/etc/rc0.d/,/etc/rc1.d/,/etc/rc6.d/中,创建停止服务软连接:
# ln -s /etc/init.d/nginx /etc/rc0.d/K20nginx
# ln -s /etc/init.d/nginx /etc/rc1.d/K20nginx
# ln -s /etc/init.d/nginx /etc/rc6.d/K20nginx
这样,当系统启动的时候,就会启动nginx,而关机、重启的时候,会stop nginx。
sysvinit初始化启动过程比较明朗:开机时按序启动/etc/rcN.d/中的以"S"开头的程序。/etc/rcN.d/中的程序大多真正存放于/etc/init.d/目录之下。
可以看到在sysvinit中,服务是按照顺序来执行的,这很影响效率。另一方面,sysvinit中,服务是预设的,不能实时启动(比如在系统被挂载了一个磁盘的时候自动启动)。而Upstart可以解决这些问题。它是基于事件机制的,可以按需启动服务,性能和很多其他方面都比sysvinit强,所以upstart被后来的Ubuntu等linux发行版采用。
在Upstart中,程序执行单位被称作作业(Job),所有的init作业都必须放置于目录/etc/init/之下,使用Upstart自己的配置文件来描述Job内容。Upstart启动时,从 /etc/init/ 目录中读取各个Job的配置文件,获取所有Job。然后发出Startup信号,所有监听这个信号的作业会被执行。在作业执行过程中,作业本身也可以自己发出信号,其他监听这个信号的服务接着就会被启动执行。Upstart通过这样的方式来达到异步和实时控制作业的启动执行。
比如在我的博客服务器中(精简):
$ ls -lh /etc/init/
total 356K
(...)
-rw-r--r-- 1 root root 297 Feb 9 2013 cron.conf
-rw-r--r-- 1 root root 489 Nov 11 2013 dbus.conf
-rw-r--r-- 1 root root 273 Nov 19 2010 dmesg.conf
-rw-r--r-- 1 root root 1.4K Apr 11 2014 failsafe.conf
-rw-r--r-- 1 root root 267 Apr 11 2014 flush-early-job-log.conf
-rw-r--r-- 1 root root 1.3K Mar 14 2012 friendly-recovery.conf
-rw-r--r-- 1 root root 284 Jul 23 2013 hostname.conf
-rw-r--r-- 1 root root 557 Apr 16 2014 hwclock.conf
-rw-r--r-- 1 root root 444 Apr 16 2014 hwclock-save.conf
-rw-r--r-- 1 root root 579 Aug 26 2014 irqbalance.conf
-rw-r--r-- 1 root root 689 Apr 10 2014 kmod.conf
-rw-r--r-- 1 root root 268 Feb 21 2014 mountall-bootclean.sh.conf
-rw-r--r-- 1 root root 1.3K Feb 21 2014 mountall.conf
(...)
-rw-r--r-- 1 root root 1.8K Feb 19 2014 mysql.conf
-rw-r--r-- 1 root root 2.5K Mar 20 2014 networking.conf
-rw-r--r-- 1 root root 534 Feb 16 2014 passwd.conf
(...)
-rw-r--r-- 1 root root 661 Apr 11 2014 rc.conf
(...)
-rw-r--r-- 1 root root 1.6K Apr 11 2014 rc-sysinit.conf
这些"*.conf"都是作业配置文件,在这个文件中指出作业什么start,什么时候stop,主进程是什么等。
xxxx:/etc/init$ ls | xargs grep "startup"
friendly-recovery.conf:emits startup
friendly-recovery.conf: initctl emit startup
hostname.conf:# This task is run on startup to set the system hostname from /etc/hostname,
hostname.conf:start on startup
kmod.conf:start on (startup
mountall.conf:start on startup
plymouth-ready.conf:start on startup or started plymouth-splash
plymouth-upstart-bridge.conf:start on (startup
udev-fallback-graphics.conf:# We only want this job to happen once per boot, hence 'startup and ...'.
udev-fallback-graphics.conf:start on (startup and
udev-finish.conf:start on (startup
udevmonitor.conf:start on (startup
udevtrigger.conf:start on (startup
作业hostname、kmod、mountall等都会监听startup信号(start on EVENT这个指令表示在EVENT发生时启动该程序)。Startup收集作业配置信息完成后,会发出"startup"信号,这些作业就会被执行了。
你可能想,那其他程序怎么办?
其他程序会监听这些程序发射的事件信号,当该事件发生时,那些程序也会被执行。比如mountall这个job就会发射"filesystem"这些类似的基础事件信号,这个代表着文件系统已经就绪了,很多其他作业(比如networking)都是监听这个信号,这样一级一级传递,启动程序就会被按照事件发生顺序一级一级的启动执行了。
最开始我们说到Upstart是兼容sysvinit的,怎么做到的?
在/etc/init/下有两个重要的作业: 作业 rc-sysinit 和 作业 rc
我们看一下这两个文件内容:
作业rc-sysinit配置:
XXXX:/etc/init$ cat 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 <scott@netsplit.com>"
start on (filesystem and static-network-up) or failsafe-boot
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
emits runlevel
# 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
作业rc的配置:
XXXX:/etc/init$ cat rc.conf
# rc - System V runlevel compatibility
#
# This task runs the old System V-style rc script when changing between
# runlevels.
description "System V runlevel compatibility"
author "Scott James Remnant <scott@netsplit.com>"
emits deconfiguring-networking
emits unmounted-remote-filesystems
start on runlevel [0123456]
stop on runlevel [!$RUNLEVEL]
export RUNLEVEL
export PREVLEVEL
console output
env INIT_VERBOSE
task
script
if [ "$RUNLEVEL" = "0" -o "$RUNLEVEL" = "1" -o "$RUNLEVEL" = "6" ]; then
status plymouth-shutdown 2>/dev/null >/dev/null && start wait-for-state WAITER=rc WAIT_FOR=plymouth-shutdown || :
fi
/etc/init.d/rc $RUNLEVEL
end script
你不必看懂每个具体配置细节,只要知道这三个配置指令就可以了:
所以根据以上配置,Upstart兼容sysvinit的过程是这样的:
telinit
完成运行级别信号的发送。/etc/init.d/rc $RUNLEVEL
这个命令, 来完成/etc/rcN.d/下相应程序的启动。这样,就完成了兼容sysvinit的过程。
所以,在Upstart之下,你可以有2种方式添加系统服务程序:
这里介绍一下我们常用的service命令到底怎么回事。
语法:
service SCRIPT COMMAND [OPTIONS]
这个命令的作用是: 运行一个sysvinit 程序或Upstart作业!,所以其是一个既支持Upstart作业,又支持sysvinit程序的命令。运行时,service首先从/etc/init.d/中去找SCRIPT,如果没找着再去/etc/init/目录下去找同名作业配置文件。然后运行这些程序/作业。
需要注意的是,service命令和/etc/rcN.d/这个目录没有任何关系。
例子:
service nginx start # 启动nginx服务
service nginx stop # 停止nginx服务
service nginx restart # 重启nginx服务
在运行时,COMMAND 和 OPTTIONS都会被完整的传递给SCRIPT。故service nginx start
其实最终执行的命令是/etc/init.d/nginx start
,"start"这个操作,并不是由service来完成,service仅仅是起到一个寻找脚本位置的作用而已!
三个目录 =>
两个文件 =>
一个命令 =>
写了这么多看似很繁琐的东西,不过是在尝试解释init系统怎么工作的,会有什么用途我并不清楚。但至少有一点,"I know how it works now."
(来自 monklof's blog)
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.