一台安装了Linux操作系统的计算机的启动,从BIOS加电开始,随后进入Bootloader,由Bootloader加载Linux内核并初始化。
内核本身不能做任何有意义的事情,内核初始化的最后一步,就是创建PID为1、名为init的守护进程,该进程是操作系统中所有用户进程的祖先——所有用户进程都由它创建。
所谓Init机制,就是确定了init进程行为的一套规范及其实现(此实现通常就反应在/sbin/init上)。Linux发展到今天,依次出现了sysvinit、upstart 、systemd三种广泛应用的Init机制。
sysvinit就是System V风格的初始化机制,它来源于System V风格的UNIX。绝大部分Linux发行版均兼容sysvinit。
sysvinit引入了运行级别(runlevel )的概念,不同模式下需要进行的系统初始化工作不一样。各发行版对运行级别的定义各有不同,但是通常:0表示关机、1表示单用户模式、6表示重启。
sysvinit正常启动(单用户或者GUI模式)系统时,大致执行的工作流如下:
sysvinit正常关闭(关机或者重启)系统时,则以数字顺序,执行rc*.d中的K开头的脚本。
sysvinit在启动、关闭时均是串行的执行各脚本的,这一方面让问题排查简单,一方面则导致运行速度缓慢。
很多Linux发行版为sysvinit配套了管理工具,例如service、chkconfig等。
参考Linux运行级别和启动顺序了解sysvinit的更多知识。
从2.6版本的内核开始,即插即用被良好的支持,Linux被越来越多的用在个人计算机领域,这让sysvinit面临两个问题:
早期的Ubuntu版本(6.10+)即启用了upstart这一init机制,以解决上述问题。
upstart是一个事件驱动的init机制,例如,当U盘插入到接口时,内核会通知upstart,后者会进行挂载操作。upstart的优势包括:
很多现有的服务已经基于System V风格的Init脚本来编写,Upstart必须兼任它。尽管Upstart完全是基于任务+事件的,但是它模拟了对运行级别的支持。
修改/etc/init/rc-sysinti.conf中的DEFAULT_RUNLEVEL可以改变默认的“运行级别”。在启动过程中,rc任务会根据设置的运行级别,访问系统中的sysvinit脚本。
Job就是一个明确定义的工作单元,它可以启动一个系统服务,或者运行一个配置命令。每个Job都会等待一个或者多个事件,一旦事件发生,upstart就会触发Job完成工作。
Job可以分为三种类型:
| Job类型 | 说明 |
| task job | 表示一个在有限时间内会完成的任务,例如删除一个文件 |
| service job |
表示后台服务进程,例如HTTP服务。这种任务通常不会停止,它启动后即由init进程管理,后者会在它崩溃后重新启动之 某些事件可以触发service job的终止,但是upstart也提供了手工管理的工具 |
| abstract job | 和upstart内部运作机制有关。尽管没有脚本段(Sections)或者exec节(stanzas),这种任务仍然可以启动,但是它没有对应的子进程(PID),除非被管理员停止,这种任务会一直运行 |
根据Job的作用范围,可以将其分为系统级服务、会话级服务。后者仅为某个用户服务。
在Job的生命周期中,它可以处于以下状态之一:
| 状态 | 说明 |
| Waiting | 初始状态 |
| Starting | 即将开始任务。此状态触发starting事件 |
| Pre-start | 任务启动前应该完成的准备工作 |
| Spawned | 准备执行 script 或者 exec 段 |
| Post-start | 任务启动完毕后执行的后置工作。此状态触发started事件 |
| Running | post-start之后的一个临时状态 |
| Pre-stop | 任务停止前应该完成的准备工作 |
| Stopping | pre-stop之后的一个临时状态。此状态触发stopping事件 |
| Killed | 任务即将被停止 |
| Post-stop | 任务停止后的清理工作。此状态触发stopped事件 |
事件是一个通知,由Upstart发送给所有订阅者(任务或者其它事件),管理员随时可以手工的触发事件:
initctl emit myevent
一个Job的启动/停止行为,可能是其它Job启动/停止行为的结果,这是依靠事件来驱动的。Upstart提供了一系列特殊事件(starting、started、stopping、stopped),用来广播Job的状态转换。尽管某些Job状态的名称与事件名称相同,但是它们不是一回事。
根据产生、消费方式的不同,事件分为:
| 事件分类 | 说明 |
| Signals |
非阻塞的异步事件,发出信号后调用者立即返回,调用者不关心谁关注此信号,手工触发信号的方式如下: initctl emit --no-wait mysignal |
| Methods | 阻塞的同步事件,这类事件的行为类似于编程语言中的方法/函数调用,调用者必须等待事件的订阅者处理完毕。手工触发时,initctl执行后的$?反应订阅者的处理结果 |
| Hooks |
介于信号和方法之间:
钩子的典型例子是starting、stopping这两个Job事件 |
系统中预定义的事件取决于发行版,可以执行man 7 upstart-events 查看之。
Job需要进行的工作由一个配置文件描述,该文件位于/etc/init目录中,以.conf结尾。
配置文件由多个小节(stanza) 构成,小节定义Job某个方面的特性:
| 类 | 小节 | 说明 |
| 文 档 |
author | 定义任务的作者 |
| description | 定义任务的描述 | |
| 进 程 控 制 |
expect |
Upstart基于进程PID来跟踪Job,如果不明确指定,Upstart使用它执行exec/script小节时产生的第一个进程的PID 但是,大部分作为系统服务的Job都会守护进程化(daemonize),这些Job会再一次的fork出子进程,以确保守护程序与Job的初始进程没有关联性 Upstart本身无法知晓任务是否fork两次,这就需要使用expect来声明:
此外,expect stop 期望Job的主进程触发一个SIGSTOP信号来指示它已经准备好,Upstart会等待此信号,然后发送SIGCONT信号给Job主进程提示它继续,并执行任务的post-start脚本 |
| kill signal |
设置Upstart发送何种信号以停止Job,默认SIGTERM,示例: kill signal INT kill signal SIGINT |
|
| kill timeout |
设置Upstart强制杀死Job进程前等待的秒数,默认5 |
|
| reload signal |
设置任务需要被reload时,Upstart发送的信号,默认SIGHUP |
|
| 进 程 定 义 |
exec |
指定Job的主逻辑。定义一个需要执行的单行的命令,如果命令行中包含任何Shell元字符,会直接传递给Shell,以确保Shell重定向、变量展开等功能的正常。语法格式: exec COMMAND [ ARG ]... # 示例 # exec /usr/bin/my-daemon --option foo -v |
| pre-start |
定义Job进入Pre-start状态时需要执行的逻辑,例如清空临时/缓存目录,最好不要包含耗时的逻辑。语法格式: pre-start exec|script # 脚本可以具有多行内容 pre-start script # 在这里编写Shell脚本 ... # 下面的这行脚本用于取消Job的启动 stop ; exit 0 end script |
|
| post-start |
定义Job主线程产生之后(spawned)需要执行的逻辑,此时Job的started事件尚未发布。语法格式类似pre-start 可以使用该小节来实现这样的功能:让Job进入started状态前,需要一个延迟或者达到某个条件。例如MySQL,在接受网络流量之前,它需要先执行Recovery操作 |
|
| pre-stop |
定义Job主线程被杀死(SIGTERM)之前、stopping事件被发布之前需要执行的逻辑。语法格式类似pre-start 停止Job时Upstart发送SIGTERM信号,如果超过Kill timeout(默认5秒)Job尚未退出,Upstart会发送SIGKILL,这可能导致数据丢失(例如磁盘操作)。可以把敏感操作转移到pre-stop中以避免此情况 你也可以使用该小节来取消服务的停止 |
|
| post-stop | 在Job进程被杀死后,执行清理工作 | |
| script | 指定Job的主逻辑。定义一个多行的脚本,以end script 标注脚本块的结束 | |
| 事 件 定 义 |
manual | 提示Upstart,忽略start on和stop on小节,总是手工的启动、停止 |
| start on |
该小节定义导致Job启动的一系列事件,语法格式: # EVENT为事件的名称,可以指定多个事件,并用and或者or连接,支持用( ) 来限定逻辑操作的优先级 # KEY和VALUE可以用来限定环境变量,这些变量可以由事件定义,并传递给Job的小节 # VALUE支持fnmatch风格的通配符 # KEY和VALUE可以使用 != 表示不相等 start on EVENT [[KEY=]VALUE]... [and|or...] 该小节的内容必须编写在单行中 如果要 在系统的基础设施准备完毕后启动Job,参考下面的例子: # 在本地文件系统、非环回网卡准备好后,启动该Job # 如果你的Job绑定了非0.0.0.0地址,则它需要在目标网卡启动后,才能启动 start on (local-filesystems and net-device-up IFACE!=lo) # 在指定的运行级别启动Job start on runlevel [2345] # 在抽象任务network-services启动完毕之后,启动Job start on started network-services 如果要在一个服务启动之后,启动Job,使用: start on started other-service 反之,如果要在一个服务启动之前,启动Job,使用: start on starting other-service |
|
| stop on |
该小节定义一系列导致Job停止的事件(如果Job正在运行),语法格式: stop on EVENT [[KEY=]VALUE]... [and|or...] 用法类似于start on,示例: stop on runlevel [016] stop on stopping network-services stop on stopped other-service |
|
| 任 务 环境 |
env | 设置一个所有小节都能看到的环境变量,语法格式:env KEY[=VALUE] |
| export | 导出Job中通过env小节设置的所有环境变量到该Job触发的全部事件中去 | |
| 其 它 |
normal exit |
指定那些退出码(exit status)表示正常退出,默认0。示例: # 0和13视为正常退出 normal exit 0 13 # 如果因为SIGUSR1 SIGWINCH信号而退出,也视为正常 normal exit 0 13 SIGUSR1 SIGWINCH
|
| respawn |
在不指定此小节的情况下,静默退出的Job进入 stop/waiting 状态 使用此小节的情况下,一旦Job从它的主逻辑exec/script退出,Upstart都会重新启动它,一并执行定义的pre-start,、post-start、 post-stop小节(pre-stop 不被执行) 使用此特性的例子包括各种网络服务,例如MySQL,只要不是管理员有意的停止它,都应该一直运行 |
|
| respawn limit |
限制重新启动的次数,语法格式: # 最多重新启动limit次,每次启动间隔INTERVAL秒 respawn limit COUNT INTERVAL | unlimited # 示例: respawn limit 10 5 respawn limit unlimited |
|
| instance | 声明Job的实例名,有时你需要运行一个Job的多个实例 | |
| task |
从概念上说,task是一种短暂运行完毕的Job 使用此关键字后,导致此Job启动的事件将被阻塞,直到当前Job转换到stopped状态 |
当开发自己的Job时,需要注意一下内容:
这里我们讲解一下MySQL的Job定义:
# 任务的描述信息
description "MySQL Server"
author "Mario Limonciello <superm1@ubuntu.com>"
# 在2345运行级别启动服务
start on runlevel [2345]
# 在rc服务启动之前,如果目标运行级别是016则停止服务
stop on starting rc RUNLEVEL=[016]
# 当任务以外退出时重启,最多重启2次,间隔5秒
respawn
respawn limit 2 5
# 设置整个文件内可见的变量
env HOME=/etc/mysql
# 设置进程的umask
umask 007
# 延长强杀超时,因为MySQL刷出缓冲需要时间
kill timeout 300
pre-start script
# 在启动前的准备工作,普通Shell脚本,略
# 访问变量的语法与Bash相同
$HOME
end script
# 主逻辑脚本
exec /usr/sbin/mysqld
post-start script
# 启动后的清理工作
end script
和Upstart相关的命令包括:initctl,关于此命令的细节请参考Linux命令知识集锦。
Systemd是主流Init系统中最新的一个,是Upstart的竞争者。值得注意的是,作为Upstart起源的Ubuntu从15.04版本开始,也改用了Systemd。
Systemd具有以下特点:
Systemd把系统启动过程中的每一项工作,例如:
等都抽象为一个配置单元,它们的角色类似于Upstart的Job。配置单元被划分为不同的类型:
| 配置单元类型 | 说明 |
| service | 封装一个后台服务,例如MySQL,最常用 |
| socket | 封装系统中的一个套接字,每个socket单元都有一个相应的service单元,当第一个连接到达时,与socket关联的service就会启动 |
| device | 封装一个存在于Linux设备树中的设备,每个使用udev规则标记的设备都会在Systemd中作为一个device单元。例如/dev/sda2会自动被转换为dev-sda2.device单元 |
| mount |
封装文件系统中的一个挂载点,Systemd会对这个挂载点进行监控管理,例如在系统启动时执行挂载,在某些情况下卸载 |
| automount | 封装文件系统中需要自动挂载的挂载点,每个automount单元都对应一个mount单元,当自动挂载点被访问时,它被自动的挂载 |
| swap | 管理交换分区 |
| target | 对其它配置单元进行逻辑分组,这样可以很方便的实现运行级别,例如multi-user.target相当于sysvinit中的运行级别5,分组中的所有配置单元都被执行 |
每个配置单元都有一个对应的配置文件,例如MySQL服务对应mysql.service。配置文件的语法比起System V脚本要简单的多。
Systemd尽量的保证了并发启动,但是某些任务之间存在天然的依赖关系,不能通过套接字激活(socket activation)、D-Bus激活、autofs这三种方法来解除依赖。为了满足先后启动的需要,Systemd允许在配置文件中声明单元的依赖关系。
要声明一个强依赖,可以使用A require B 的语法,声明弱依赖则使用A want B 。如果服务之间存在依赖循环,则先去掉弱依赖,如果循环仍然存在,则Systemd会报错。
前面我们提到过target这类特殊的配置单元,用于分组其它配置单元。利用Target把sysvinit某个运行级别中需要启动的所有服务归纳到同一个组中,可以很方便的模拟运行级别。Systemd预定义了运行级别和Target之间的映射:
| 运行级别 | 运行级别含义 | Systemd targets |
| 0 | 关机 | runlevel0.target, poweroff.target |
| 1,s,single | 单用户模式 | runlevel1.target, rescue.target |
| 2,4 | 用户定义的运行级别 | runlevel2.target, runlevel4.target, multi-user.target |
| 3 | 多用户模式,非图形化 | runlevel3.target, multi-user.target |
| 5 | 多用户模式,图形化 | runlevel5.target, graphical.target |
| 6 | 重现启动 | runlevel6.target, reboot.target |
| emergency | 紧急Shell | emergency.target |
Systemd通过并行加速启动过程,要提高并行度,必须解除服务之前的依赖关系。Systemd通过三种手段来解除依赖,这三种手段都基于延迟(Lazy)启动的思想。
绝大部分服务之间的依赖是套接字依赖——被依赖者(A)的套接字(S)必须准备好监听,依赖者(B)才能启动。传统的Init机制都是启动A,然后再启动B。
Systemd的想法是,直接启动S,当B第一次向S发起连接时,延迟的启动A。能让服务A的套接字先于A本身启动,需要依靠内核的一个特性:子进程可以继承父进程的文件描述符(文件句柄,file descriptor) ,而套接字是一种文件句柄。因此先于A启动S的流程可以如下:
其实这种内核特性以前就被使用过,例如inetd。
所谓D-Bus即桌面总线(Desktop-bus),是一种低延迟、低开销、高可用的进程间通信机制。它被越来越多的用于应用程序之间的通信,以及应用程序与内核的通信。例如NetworkManager就使用D-Bus和其它应用程序交互。
D-Bus支持所谓Bus activation特性:如果A依赖于B的D-Bus服务,而B尚未运行,则D-Bus可以在A第一次请求B的时候,启动B。在B启动完毕之前A会一直等待。依赖于D-Bus的这一特性,也可以实现并行启动。
文件系统相关的活动是相当耗时的,因为与之相关的配置单元而导致串行化执行,会大大的拖累系统启动速度。
Autofs是一个实现按需挂载的软件,它利用了内核的automounter模块:当open()系统调用作用在/mnt/dsk/file1时,/mnt/dsk可能尚未挂载,此时内核将让open()调用挂起,并通知Autofs执行挂载,完毕后把控制权返回给open()调用。
Systemd在内部集成了Autofs的实现。对于系统中的挂载点它会在启动时创建一个临时的Mock,这样,依赖于此挂载点的服务就可以立即启动。当文件系统相关的配置单元都启动成功后,Systemd再把那些Mock替换为真正的挂载点。如果在文件系统尚未准备好时,Mock就被访问,Systemd则让访问者陷入等待。
Systemd允许用户管理自己控制下的服务 —— 启动、停止、禁用自己的配置单元,每个用户都具有自己Systemd实例。
用户级单元可以放在/usr/lib/systemd/user、 ~/.config/systemd/user等位置。
使用--user参数,systemctl管理当前用户的systemd单元:
systemctl --user enable myapp.service
配置单元同样由文件文件来定义,命名格式为unitName.unitType (后缀为此配置单元的类型),存放路径取决于具体发行版,例如/etc/systemd/system,Ubuntu则存放在/lib/systemd/system目录中。
配置单元由一系列的段组成,格式类似于Windows下的INI文件。
Unit段:基础元数据和依赖关系
| 配置项 | 说明 |
| Description | 对单元的描述 |
| Documentation | 文档URL |
| Requires | 当前单元所强依赖的其它单元,多个单元用空白分隔 ,如果依赖没法满足,该单元无法启动 |
| Wants | 当前单元所弱依赖于的其它单元 |
| BindsTo | 类似于Requires,如果所依赖的单元退出,则当前单元也退出 |
| Before | 限定当前单元必须在指定的单元之前启动 |
| After | 限定当前单元必须在指定的单元之后启动 |
| Conflicts | 指定不能和当前单元同时运行的单元 |
| Condition*** |
限定当前单元运行必须满足的条件(如果不满足则不运行),例如: # 只有/etc/ssh/sshd_not_to_be_run文件不存在时,才运行此单元 ConditionPathExists=!/etc/ssh/sshd_not_to_be_run |
| Assert*** | 限定当前单元运行必须满足的条件(如果不满足则报启动失败) |
Service段:专用于服务类单元的配置
| 配置项 | 说明 |
| Type | 定义启动时的进程行为,有效值: simple 默认值,执行ExecStart指定的命令,启动服务主进程 forking 以fork调用创建子进程,并退出父进程 oneshot 一次性服务,Systemd会等待该服务退出,然后继续init过程 dbus 当前服务通过D-Bus启动 notify 当服务启动完毕,通知Systemd继续 idle 当其它单元都启动完毕后,才执行此单元 |
| ExecStart |
启动服务的命令行 默认情况下,这些生命周期钩子执行失败会终止后续流程。要改变此逻辑,使用=-操作符: ExecStartPre=-/bin/chmod 700 /run/thing 这样,即使 ExecStartPre 执行失败,后续的ExecStart仍然会执行 |
| ExecStartPre |
启动服务的前置命令行 这些生命周期钩子可以指定多次,前面的先执行 |
| ExecStartPost | 启动服务的后置命令行 |
| ExecReload | 重新加载服务时执行的命令行 |
| ExecStop | 停止服务时执行的命令行 |
| ExecStopPost | 停止服务的后置命令行 |
| RestartSec | 重启的间隔秒数 |
| Restart |
重启服务的时机,有效值: no 不重启 干净退出:退出码为0,或者因为SIGHUP, SIGINT, SIGTERM or SIGPIPE退出。或者定义在 SuccessExitStatus中的其它退出码、信号 |
| TimeoutSec | 启动超时的秒数,超过这个时间还未启动完毕,终止服务 |
| Environment | 用于指定环境变量 |
Install段:定义如何启动单元,是否开机启动
| 配置项 | 说明 |
| WantedBy |
指定一个或者多个目标(Target)对当前单元具有弱依赖,当systemctl enable当前单元时,会把当前单元的符号连接放到/etc/systemd/system下的Target名称.wants目录中 该名称项可用于模拟sysvinit的初始化方式 |
| RequiredBy | 指定一个或者多个目标(Target)对当前单元具有强依赖 |
| Alias | 调用systemctl时可以使用的当前单元的别名 |
| Also | 指定当前单元被systemctl enable时,会同时激活的其它单元 |
Systemd支持模板化的Unit文件,用于从单个配置文件启动多个单元。调用模板化Unit时,需要使用特殊的@符号:<service_name>@<argument>.service。
其中argument是传递给模板的字符串,在模板文件中可以这样访问:
Ceph Mon就是这样的模板Unit:
[Unit]
Description=Ceph cluster monitor daemon
After=network-online.target local-fs.target time-sync.target
Wants=network-online.target local-fs.target time-sync.target
PartOf=ceph-mon.target
[Service]
LimitNOFILE=1048576
LimitNPROC=1048576
EnvironmentFile=-/etc/default/ceph
Environment=CLUSTER=ceph
; 访问参数
ExecStart=/usr/bin/ceph-mon -f --cluster ${CLUSTER} --id %i --setuser ceph --setgroup ceph
ExecReload=/bin/kill -HUP $MAINPID
PrivateDevices=yes
ProtectHome=true
ProtectSystem=full
PrivateTmp=true
TasksMax=infinity
Restart=on-failure
StartLimitInterval=30min
StartLimitBurst=5
RestartSec=10
[Install]
WantedBy=ceph-mon.target
在目录下有指向上述模板的符号链接,参数就是通过符号链接的名字传递的:
cd /etc/systemd/system# ls ceph-mon.target.wants/ -la # ceph-mon@xenon.service -> /lib/systemd/system/ceph-mon@.service # ceph-mon@Xenon.service -> /lib/systemd/system/ceph-mon@.service
这里我们讲解一下MySQL的Unit定义:
# MySQL systemd service file [Unit] # 配置单元描述 Description=MySQL Community Server # 声明依赖 After=network.target [Install] # 该服务在多用户模式下启动 WantedBy=multi-user.target [Service] User=mysql Group=mysql PermissionsStartOnly=true # 启动服务之前需要运行的命令 ExecStartPre=/usr/share/mysql/mysql-systemd-start pre # 启动服务主程序 ExecStart=/usr/sbin/mysqld # 启动服务之后需要运行的命令 ExecStartPost=/usr/share/mysql/mysql-systemd-start post TimeoutSec=600 Restart=on-failure RuntimeDirectory=mysqld RuntimeDirectoryMode=755
我们再看看multi-user.target这个配置单元组的定义:
[Unit] Description=Multi-User System Documentation=man:systemd.special(7) # 该组被启动之前basic组必须被启动,同样的,basic停止前该组必须先停止 Requires=basic.target Conflicts=rescue.service rescue.target After=basic.target rescue.service rescue.target AllowIsolate=yes [Install] # 为本单元组定义别名,调用systemctl命令时可以使用别名 Alias=default.target
和Systemd相关的命令包括:systemctl、journal、systemd-analyze、hostnamectl、localectl、timedatectl、loginctl等,关于此命令的细节请参考Linux命令知识集锦。
Leave a Reply to Xu hui Cancel reply