摘要
Linux 发行版众多,各发行版很难统一技术方案,从而导致安装服务的方式千差万别。本文主要介绍sysv-init
和 systemd
两种主要技术方案差别以及他们的相互联系。厘清主流服务加载机制和原理,帮助读者提前避坑。
前提
本文的实验环境基于 Ubuntu 22.04.4 LTS,安装在 KVM 虚拟机中,安装模式为 Minimal Server ,内核版本为 5.15.0-107-generic。
本文主要讨论 Linux 系统的服务配置,其他的类 Unix 系统,如 FreeBSD, Darwin (MacOS) 等不在讨论范围内,请注意加以区别。
历史
在 Linux 和其他类 Uniux 系统中,初始化进程(init
)是系统启动时由内核执行的第一个进程,其它的进程都是 init 进程的后代,因此承担着重大职责,其中即包括初始化系统和引导守护服务进程。System V 是商业 Unix 系统的初始设计方案,Linux 出现以后顺理成章地继承 Sys-V 实现,成为 Linux 众多发行版广泛采用的的初始化方案。
随着 Linux 系统的快速发展,又出现了一些新的框架,典型的如 Upstart
(源自Ubuntu)和 Systemd(源自RedHat)。早期 Systemd
由于质量和野心问题遭到社区的广泛抵制,自 Ubuntu 15.04 起,Systemd 得到 Ubuntu 新发行版本的支持,加上 RedHat 自有的 Fedora 和 CentOS 系统,Systemd 已经成为最有影响力的标准配置。
因此,本文将主要以 Sys-V-init 和 Systemd 为主要介绍对象。对于其他的发行版,试验前请首先确认系统内置的初始化框架。
Sys-V-init
Sys-V-init 机制虽然已经被取代,但是仍有大量具有历史感的系统尚在运行,并且各路大神珍藏的启动脚本宝库也可谓璀璨夺目;为了更有效的"借鉴"上古珍宝,首先从介绍 Sys-V-Init 开始。
runlevel
Linux 系统使用"运行级别"这个概念来决定大致的目标任务,它决定了系统启动后哪些服务和进程将被加载和运行。这个概念类似 Windows 系统的 "安全模式"、"维护模式"。Sys-V 预置了 0~6 共7种运行级别定义:
- 0级:关机 - 所有进程都将被终止,系统被关闭。
- 1级:单用户模式 - 仅允许一个用户登录系统,通常用于系统维护或恢复。
- 2级:多用户模式,不带NFS - 多个用户可以登录系统,但网络文件系统(NFS)不可用。
- 3级:多用户模式,文本模式 - 这是一个完全的多用户模式,支持网络功能,但通常只提供命令行界面,没有图形界面。
- 4级:未定义 - 在某些系统中,这个级别可能用于定义自定义的运行模式。
- 5级:多用户模式,图形界面 - 类似于级别3,但提供了图形用户界面(GUI),允许用户使用图形界面登录和操作系统。
- 6级:重启 - 系统将重新启动。
Sys-V-init 启动以后,首先从 /etc/inittab
文件中读取一些配置信息,包括:
- 系统需要进入的 runlevel
- 捕获组合键的定义
- 定义电源 fail/restore 脚本
- 启动 getty 和虚拟控制台
rc[X].d 和 init.d
使用 Sys-V-init 的系统中,服务脚本文件保存在 /etc/rc[X].d
/ 和 /etc/init.d/
当中,其中 'X' 为 0~6 的数字,对应 7 个运行级别。
两者的关系可以简单总结为:init.d 目录存储服务脚本,rc[X].d 系列目录存储脚本符号链接。Sys-V-init 按照当前的运行级别从对应的 rc[X].d 目录加载启动脚本,然后按照顺序执行。
比较形象的看法可以认为 rc[X].d 是入口,init.d 是仓库。需要说明的是:这些并不是绝对的,有的系统中 init.d 反而是一个符号链接;但是无论什么样的目录组织方式,只是实现目的的不同方法而已。
~# ls /etc/init.d/ssh -al
-rwxr-xr-x 1 root root 4060 Feb 25 2022 /etc/init.d/ssh
~# ls /etc/rc5.d/S01ssh -al
lrwxrwxrwx 1 root root 13 Aug 9 2022 /etc/rc5.d/S01ssh -> ../init.d/ssh
服务脚本应符合LSB
规范,包括 start
、stop
、reload
等命令参数的处理,后面将会提到。形如 service sshd start
的调用,无非是两步操作的组合:首先 service
脚本决定 sshd
脚本的加载位置,sshd
脚本负责响应 start
命令。
脚本的名字
需要注意的是,Sys-V 实际加载的脚本有固定的命名规则,通常 init.d 目录中存储的是脚本原始名称,如 sshd,而 rc[X].d 中根据任务不同,存储的脚本名称为 S[nn]<script-name>
或 K[nn]<script-name>
这种形式。'S' 和 'K' 代表了不同运行级别下,该服务的启动和停止状态;后面的数字则表示其加载顺序。
Linux 标准基准规范脚本
LSB
的官方定义:"The Linux Standard Base (LSB) defines a system interface for compiled applications and a minimal environment for support of installation scripts." 它是 Linux 社区对 Unix 生态 POSIX
标准的继承和拓展,目前已经发展到版本 5。
符合 LSB 服务脚本定义的示例如下:
### BEGIN INIT INFO
# Provides: my_daemon
# Required-Start: postgresql networking
# Required-Stop: postgresql networking
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: This is a test daemon
# Description: This is a test daemon
# This provides example about how to
# write a Init script.
### END INIT INFO
# Codes here ...
case $1 in
start)
# Starting the daemon.
;;
stop)
# Stop the daemon.
;;
restart)
# Restart the daemon.
;;
status)
# Check the status of the process.
;;
*)
# For invalid arguments, print the usage message.
echo "Usage: $0 {start|stop|restart|reload|status}"
exit 2
;;
esac
这个脚本由大致由三部分组成:
- 符合 LSB 标准的头部注释
- 实现逻辑的代码
- 处理服务参数的代码
符合标准的启动脚本,通过inserv
或 update-rc.d
命令,可以将脚本正确的安放到 rc[X].d 目录中。
头部注释
上面例子中的头部注释包含了服务脚本的元信息:
- Provides 指定服务的名称,必须唯一;
- Required-start 指定先于本服务启动的其他服务,注意必须目标服务的
Provides
字段匹配; - Required-Stop 指定本服务结束以后,必须被终止的其他服务;
- Default-Start、Default-Stop 定义了本服务默认须被加载的运行级别和不应被加载的运行的级别;
- Short-Description and Description 服务描述,查询服务状态时呈现给用户。
命令参数
上面例子中包含了服务需要处理的最基本参数,当用户启动、停止、重启脚本时,Sys-V-init 将传入对应的参数:
- start 启动
- stop 停止
- restart 重启
- status 查询状态
创建服务小结
简短的说,创建一个适配 SysV-init 的服务仅需三步:
- 第一步编写服务逻辑脚本,添加符合 LSB 标准的头部和命令参数处理;
- 第二步将脚本拷贝至 /etc/init.d 目录,并修改访问、运行权限(如果启用
SELinux
,还需要设置安全上下文); - 第三步运行
inserv
或update-rc.d
命令,完成服务脚本"注册"到 rc[X].d 目录的过程。
服务注册试验
创建服务
# 使用 here-document 命令创建服务
SCNAME=demo-daemon;\
SCFILE=/etc/init.d/$SCNAME; \
cat << "EOF" > $SCFILE; \
chmod +x $SCFILE; \
chown root:root $SCFILE; \
update-rc.d -f $SCNAME defaults
#! /bin/sh
### BEGIN INIT INFO
# Provides: demo-daemon
# Required-Start: networking
# Required-Stop: networking
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: demo-daemon
# Description: This is a demo daemon
# This provides example about how to
# write a Init script.
### END INIT INFO
touch /etc/init.d/demo-daemon.log
# print loading
# Get function from functions library
# . /etc/init.d/functions
case $1 in
start)
# Starting the daemon.
;;
stop)
# Stop the daemon.
;;
restart|reload|condrestart)
# Restart the daemon.
;;
status)
# Check the status of the process.
;;
*)
# For invalid arguments, print the usage message.
exit 2
;;
esac
EOF
检查服务安装
find / -name *demo-daemon*
# 服务名称前缀 'S' 和 'K' 与 LSB 头部对应相对应。
/etc/init.d/demo-daemon
/etc/rc0.d/K01demo-daemon
/etc/rc1.d/K01demo-daemon
/etc/rc2.d/S01demo-daemon
/etc/rc3.d/S01demo-daemon
/etc/rc4.d/S01demo-daemon
/etc/rc5.d/S01demo-daemon
/etc/rc6.d/K01demo-daemon
# 注意额外生成了一些其他的文件(systemd提前出场,展示出对 init 体系一定程度的兼容!!!)
/run/systemd/generator.late/demo-daemon.service
/run/systemd/generator.late/graphical.target.wants/demo-daemon.service
/run/systemd/generator.late/multi-user.target.wants/demo-daemon.service
启动服务
service demo-daemon start
systemctl restart demo-daemon
systemctl status demo-daemon
● demo-daemon.service - LSB: demo-daemon
Loaded: loaded (/etc/init.d/demo-daemon; generated)
Active: active (exited) since Sun 2024-06-16 11:21:17 UTC; 5s ago
Docs: man:systemd-sysv-generator(8)
Process: 4573 ExecStart=/etc/init.d/demo-daemon start (code=exited, status=0/SUCCESS)
CPU: 6ms
Jun 16 11:21:17 haclush2-vm11 systemd[1]: Starting LSB: demo-daemon...
Jun 16 11:21:17 haclush2-vm11 systemd[1]: Started LSB: demo-daemon.
清理服务
SCNAME=demo-daemon;\
update-rc.d -f $SCNAME remove; \
for SCFILE in `find / -name *$SCNAME*`; do rm -f $SCFILE; done
Systemd
init 进程有两个有两个缺点。
- 一是启动时间长:
init
串行启动服务进程,前一个进程启动完成再启动下一个。 - 二是启动脚本复杂:
init
只执行启动脚本,不管其他事情。脚本需要处理各种情况,往往使脚本变得很长/复杂。
Systemd 为了解决以上问题而设计,当然它的目标并不仅仅是系统服务,其涉足范围已经超过了原始 init 进程的俗成(设计时借鉴了OSX 中 launchd 的很多理念)。
Linux 社区内对此存在两种矛盾的看法。
引导
Systemd 守护进程 (System Deamon Process) 在启动时要求其进程 id 必须为 1,故必须由初始进程调用,并且不能其加载进程不能被 fork()
。使用脚本执行 exec /usr/sbin/init --show-status=true $SYSTEMD_OPTS
可引导 Systemd 启动。
Systemd 进入正式系统初始化任务前,首先加载单元文件,以管理员设定的任务目标指引行事。
单元文件(unit file
)
类型
单元文件包含描述这个单元并定义其行为的配置指令。systemctl
把单元文件的内容翻译为 Systemd 配置,手工编辑或创建单元文件以及其符号连接都是被允许的。单元文件名的格式如下:
<unit_name>.<type_extension>
这里的 unit_name 代表单元名称,type_extension 标识单元类型。例如,您可以找到系统上存在的 sshd.service
和 sshd.socket
单元。按照类型(Types of Units)不同,单元文件可分为 12 类:
类型 | 描述 |
---|---|
Service unit | 系统服务,告诉 Systemd 运行什么任务,以及以何种方式运行 |
Target unit | 多个 Unit 构成的一个组 |
Slice Unit | 进程组 |
Device Unit | 硬件设备 |
Mount Unit | 文件系统的挂载点 |
Automount Unit | 自动挂载点 |
Path Unit | 文件或路径 |
Scope Unit | 不是由 Systemd 启动的外部进程 |
Snapshot Unit | Systemd 快照,可以切回某个快照 |
Socket Unit | 进程间通信的 socket |
Swap Unit | swap 文件 |
Timer Unit | 定时器 |
位置
存储单元文件的位置在三个目录,目录 /etc/systemd/system/
是为管理员创建或自定义的单元文件保留的。不同的目录的位置存在优先级和和用途的差异:
表 1.1. systemd 单元文件位置
目录 | 描述 |
---|---|
/usr/lib/systemd/system/ | 存放与安装包一起分发的 systemd 单元文件。 |
/run/systemd/system/ | 存放在运行时创建的 systemd 单元文件;优先于安装包服务单元文件目录。 |
/etc/systemd/system/ | 存放使用 systemctl enable 命令创建或管理员自定义的单元文件。优先于带有运行时单元文件的目录。 |
优先的意思是,高优先级目录下的单元文件被加载,低优先级对等目录下的文件则被直接忽略。(含 drop in 目录) |
扩展
可通过 Drop in
目录机制扩展单元文件,Systemd 自动合并单元文件和投入片段(.conf
文件)。例如,为 sshd.service
添加自定义配置,可创建 sshd.service.d/custom.conf
文件,并在其中插入额外的指令。
依赖
如 sshd.service.wants/
和 sshd.service.requires/
形式的目录包含了 sshd
服务依赖的单元文件的符号链接。systemd
可以在安装过程中根据 [Install] 单元文件选项或在运行时根据 [Unit] 选项自动创建符号链接;也可以手工创建这些目录和符号链接。
名称以 @
结尾的单元文件,如 httpd@.service
,被视作通用单元文件模板,根据传入参数生成实例化单元文件。例如,systemctl enable httpd@reverse-proxy.service
中 @
后的实列名称用于区别同一个 httpd
模板生成的服务实例。此例子实际上会在当前运行目标 target
中生成名为 httpd@reverse-proxy.service 的符号链接;使用 httpd@.service 的同名链接而不指定实例名称时,目标名称将被用做实例名称。
模板生成的单元实例同样可以使用 drop in 目录进行扩展。
目标
了解单元文件的基本规则的基础上,目标(target
)是 Systemd 体系中另一个与服务依赖密切相关的概念,类似于 SysV/init 中的 runlevel 概念。在 .target
文件中仅简单定义了目标包含的服务名称,但是依靠默认目标、其 wants/requires 、其他服务的 wantedby/requiredby
(或手工创建的链接)三者,Systemd 守护进程可以构建一颗依赖树,目标作为根或枝,组织起所有的资源。
常用 systemctl
命令
# 显示服务树的状态
systemctl status
# 显示具体服务的状态,会包含引用到的
systemctl status <ServiceName>.service
# 显示失败的服务
systemctl --failed
# 显示服务的详细配置
systemctl show <ServiceName>.service [--all/--property=<PROPERTY_NAME>]
# 显示服务的依赖关系
systemctl list-dependencies <ServiceName>.service
# 列出所有单元
systemctl list-units --type=[target/service/...]
# 列出所有单元文件
systemctl list-unit-files --type=[target/service/...]
冷知识点
在使用 Systemd 过程中的一些问题探讨,过于琐碎因此不再说明,仅作导读。
ExecStopPost= &SERVICE_RESULT
systemd ExecStopPost and $SERVICE_RESULT behavior
OnFailure= vs OnSuccess=
Confusing systemd behaviour with OnFailure= and Restart=
OnFailure= vs ExecStopPost=
Run an arbitrary command when a service fails Proper way to use OnFailure in systemd
oneshot vs ExecStopPost
systemd ExecStopPost not running if ExecStartPre fails?
WantedBy vs Wants
Best practice for Wants= vs WantedBy= in Systemd Unit Files
Systemd
完全不会处理 [install]
段,这完全由 systemctl
命令解释和执行,简单说仅仅被 systemctl enable <ServiceName>
产生影响
After= vs [Requires/wants]=
In systemd, what's the difference between After= and Requires=? Systemd: Requires vs wants
Systemd 和 SysV-Init 兼容问题
当使用 update-rc.d
注册脚本服务时,内部会调用 /usr/lib/systemd/system-generators/systemd-sysv-generator
生成 Systemd 所需要的 Unit 文件。
此外,使用 systemctl enable/disable
服务时,也会调用 update-rc.d
脚本,从而使 SysV-init 脚本和 Systemd Unit 文件保持一致。
最后若脚本文件更新,Systemd 也将检测到脚本变化,并提示用 systemctl reload
命令重新加载服务。
后记
Systemd 涉及的内容太多太复杂,实在没有办法挖掘其底层原理,幸而本文主要目的是说明 Init 和 Systemd 两种实现的服务注册差别,防止张冠李戴的情况。关于 Systemd ,网上有不少文章作了深入介绍,比如:RedHat: 使用 systemd 单元文件 以及 阮一峰:Systemd 入门教程:命令篇,已经列出在参考资料当中。
参考资料
Rethinking PID 1
全面易懂的 Systemd
服务管理
LINUX PID 1和SYSTEMD
How to Write Linux Init Scripts Based on LSB Init Standard Linux boot process Linux Standard Base Specifications Archive How does systemd use /etc/init.d scripts? 阮一峰:Systemd 入门教程:命令篇 阮一峰: systemd 入门教程:实战篇