Linux 服务配置概述

89 阅读12分钟

摘要

Linux 发行版众多,各发行版很难统一技术方案,从而导致安装服务的方式千差万别。本文主要介绍sysv-initsystemd 两种主要技术方案差别以及他们的相互联系。厘清主流服务加载机制和原理,帮助读者提前避坑。

前提

本文的实验环境基于 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种运行级别定义:

  1. 0级:关机 - 所有进程都将被终止,系统被关闭。
  2. 1级:单用户模式 - 仅允许一个用户登录系统,通常用于系统维护或恢复。
  3. 2级:多用户模式,不带NFS - 多个用户可以登录系统,但网络文件系统(NFS)不可用。
  4. 3级:多用户模式,文本模式 - 这是一个完全的多用户模式,支持网络功能,但通常只提供命令行界面,没有图形界面。
  5. 4级:未定义 - 在某些系统中,这个级别可能用于定义自定义的运行模式。
  6. 5级:多用户模式,图形界面 - 类似于级别3,但提供了图形用户界面(GUI),允许用户使用图形界面登录和操作系统。
  7. 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 规范,包括 startstopreload 等命令参数的处理,后面将会提到。形如 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 标准的头部注释
  • 实现逻辑的代码
  • 处理服务参数的代码

符合标准的启动脚本,通过inservupdate-rc.d 命令,可以将脚本正确的安放到 rc[X].d 目录中。

头部注释

上面例子中的头部注释包含了服务脚本的元信息:

  • Provides 指定服务的名称,必须唯一;
  • Required-start 指定先于本服务启动的其他服务,注意必须目标服务的 Provides 字段匹配;
  • Required-Stop 指定本服务结束以后,必须被终止的其他服务;
  • Default-StartDefault-Stop 定义了本服务默认须被加载的运行级别和不应被加载的运行的级别;
  • Short-Description and Description 服务描述,查询服务状态时呈现给用户。

命令参数

上面例子中包含了服务需要处理的最基本参数,当用户启动、停止、重启脚本时,Sys-V-init 将传入对应的参数:

  • start 启动
  • stop 停止
  • restart 重启
  • status 查询状态

创建服务小结

简短的说,创建一个适配 SysV-init 的服务仅需三步:

  • 第一步编写服务逻辑脚本,添加符合 LSB 标准的头部和命令参数处理;
  • 第二步将脚本拷贝至 /etc/init.d 目录,并修改访问、运行权限(如果启用 SELinux,还需要设置安全上下文);
  • 第三步运行 inservupdate-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 UnitSystemd 快照,可以切回某个快照
Socket Unit进程间通信的 socket
Swap Unitswap 文件
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/...]

man systemd.unit

冷知识点

在使用 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 入门教程:命令篇,已经列出在参考资料当中。

参考资料

RedHat: 使用 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 入门教程:实战篇