第 11 章 Linux 服务管理

作者: Brinnatt 分类: ARM64 Linux 基础精修 发布时间: 2022-01-21 12:07

开篇前要先做一下说明,Centos 操作系统从 7.x 版本才开始支持 arm64 架构,也就是 Centos 6.x 及以前的版本都不支持 arm64 架构。Centos 7.x 和 Centos 6.x 管理服务的方式截然不同,而 Centos 7.x 又兼容 Centos 6.x 的服务管理方式,所以有必要都介绍一下,会用到 x86 架构的 centos 6.4。

11.1、服务的概念

服务是向外提供服务的进程,一般来说都会放在后台,既然要持续不断的提供外界随时发来的服务请求,服务进程就需要常驻在内存中,且不应该和终端有关,否则终端退出服务程序就退出了。

另外,要能够接待外界的请求为外界提供服务,那么就需要有个专属于这个服务的 "服务窗口",这个服务窗口就是端口号,通过端口号就能找到服务的提供者。

提供服务的一端叫做服务端,向服务端请求服务的叫做客户端。两端的交互过程如下:

  1. 服务端启动服务进程,此时将开放对应的端口号;
  2. 客户端指定服务端 IP 地址和端口号向该服务端发起请求;
  3. 服务端所在主机的内核接收到请求数据包,然后分析数据包发现请求的是某某端口号,内核知道该端口号是哪个应用程序监听的端口,所以将请求报文发送给对应的应用程序;
  4. 应用程序收到报文后,将和客户端建立连接,并进行数据传输。

注意:并非所有服务都总是提供端口号的,例如 xinetd 这个服务,只有在需要的时候才接管相应的端口,如 rsync 监听端口为 222 时,那么请求 rsync 时,xinetd 在监听过程中的端口号就是 222。在不被请求的时候,xinetd 是没有端口号的。

11.2、服务类型

在 Linux 中,服务分为独立守护进程和超级守护进程。

  • 独立守护进程是自行监听在后台的,基本上所有的服务都是独立守护进程类的服务。
  • 超级守护进程专指 xinetd 这个服务,这个服务代为管理着一些特殊的服务,这类服务在被请求的时候才会由 xinetd 通知它启动服务,服务提供完毕后就关闭服务,这类服务称为瞬时守护进程,即只存在于瞬时。
    • 但要明白,超级守护进程 xinetd 本身是一个常驻内存的独立守护进程,因为它要监听来自外界对其管理的瞬时守护进程的请求。
    • 不过一般不工作的时候,xinetd 不占用端口号,在工作的时候它占用被请求的瞬时守护进程的端口号,并处于监听状态。

在 11.2.X 这一小节将使用 x86 Centos 6.4 做所有实验,其它的没有做说明的都是使用 ARM64 架构的操作系统做所有实验。

11.2.1、独立守护进程

在 x86 CentOS 6.4 上,所有的服务脚本都在 /etc/rc.d/init.d/ 目录下,/etc/init.d/ 是它的软链接。在此目录下的脚本都是 LSB 风格的脚本,它们基本上都能接受 start/stop/restart/reload/status 等参数。

[root@x86 ~]# ls /etc/init.d/
auditd            halt       lvm2-lvmetad  network      rsyslog    sshd
blk-availability  ip6tables  lvm2-monitor  postfix      sandbox    udev-post
crond             iptables   netconsole    rdisc        saslauthd
functions         killall    netfs         restorecond  single
[root@x86 ~]#

要管理独立守护进程类的服务

# /etc/init.d/service_name   restart|start|stop|status      # 方法一
# service  service_name      restart|start|stop|status      # 方法二

要让服务能够被 service 命令管理,将其服务脚本放在 /etc/init.d 目录下即可。

11.2.2、xinetd 超级守护进程

xinetd 即 extended internet daemon,xinetd 是新一代的网络守护进程服务程序,又叫超级 Internet 服务器。经常用来管理多种轻量级 Internet 服务。

11.2.2.1、管理瞬时守护进程

该类服务不能直接使用 service 命令来启动。只能去 /etc/xinetd.d/ 目录下的对应文件中进行设置(当然,也可以在 /etc/xinetd.conf 中配置),然后由 xinetd 进行管理。

# 安装 xinetd
[root@x86 ~]# yum install xinetd -y

# 安装 rsync,默认归于 xinetd 成为瞬时守护进程
[root@x86 ~]# yum install rsync -y

[root@x86 ~]# chkconfig --list
......
xinetd          0:off   1:off   2:off   3:on    4:on    5:on    6:off

xinetd based services:
    chargen-dgram:  off
    chargen-stream: off
    daytime-dgram:  off
    daytime-stream: off
    discard-dgram:  off
    discard-stream: off
    echo-dgram:     off
    echo-stream:    off
    rsync:          off # 默认归于 xinetd 成为瞬时守护进程
    tcpmux-server:  off
    time-dgram:     off
    time-stream:    off

首先得保证 xinetd 是已经工作在后台的。

[root@x86 ~]# service xinetd start
Starting xinetd:                                           [  OK  ]
[root@x86 ~]# 

然后管理瞬时守护进程,该类服务比较特别,其自启动状态和服务运行状态是同步的,也就是说 chkconfig 设置了其自启动则表示启动该服务,否则为停止该服务。

另外,对其指定级别是无效的,它们的启动级别继承于 xinetd 的启动级别,并且 xinetd 会接管其触发的瞬时守护进程的端口号。

例如启动 rsync 这个瞬时守护进程。

[root@x86 ~]# chkconfig rsync on
[root@x86 ~]# 
[root@x86 ~]# chkconfig --list
......
xinetd          0:off   1:off   2:off   3:on    4:on    5:on    6:off

xinetd based services:
    chargen-dgram:  off
    chargen-stream: off
    daytime-dgram:  off
    daytime-stream: off
    discard-dgram:  off
    discard-stream: off
    echo-dgram:     off
    echo-stream:    off
    rsync:          on  # 已启动
    tcpmux-server:  off
    time-dgram:     off
    time-stream:    off
[root@x86 ~]#

11.2.2.2、瞬时守护进程的配置

瞬时守护进程关联三个配置文件,一个是 xinetd 的配置文件 /etc/xinetd.conf 提供默认配置;一个是 /etc/xinetd.d/ 下的配置文件针对对应的服务提供配置;最后一个 /etc/services,设置了 xinetd 下的 service 对应的端口号。

例如配置 rsync

# /etc/xinetd.d/rsync 的默认配置。
[root@x86 ~]# cat /etc/xinetd.d/rsync
# default: off      
# description: The rsync server is a good addition to an ftp server, as it \
#       allows crc checksumming etc.
service rsync                               # 定义rsync服务,名称要和/etc/xinetd.d/下的文件同名
{
        disable         = yes               # yes表示不启动,no表示启动,等价于chkconfig rsync {on|off},所以这里设置后将直接在chkconfig中生效
        flags           = IPv6              # 不用管
        socket_type     = stream            # 这代表的是tcp类型的套接字
        wait            = no                # 该服务是单线程还是多线程的,yes表示该服务为单线程,xinetd只启动服务器不处理请求,服务器处理请求连接;no表示该服务为多线程,xinetd继续处理新服务请求并处理连接。一般udp为yes,tcp为no。
        user            = root              # 以什么身份运行rsync
        server          = /usr/bin/rsync    # 启动服务对应的二进制文件
        server_args     = --daemon          # 服务程序启动时传递的参数
        log_on_failure  += USERID           # 连接失败的日志记录,+表示在全局对应的条目上新增此处指定的USERID
}
# /etc/services 定义 rsync 端口号
[root@x86 ~]# grep "\<rsync\>" /etc/services 
rsync           873/tcp                         # rsync
rsync           873/udp                         # rsync
[root@x86 ~]#
[root@x86 ~]# grep -v "^#" /etc/xinetd.conf 

defaults
{
    log_type    = SYSLOG daemon info            # 登录文件的记录服务类型
    log_on_failure  = HOST                      # 发生错误时需要记录的信息为主机 (HOST)
    log_on_success  = PID HOST DURATION EXIT    # 成功启动或登陆时的记录信息

    cps     = 50 10     # 表示每秒50个入站连接,如果超过限制,则等待10秒。主要用于对付拒绝服务攻击。
    instances   = 50    # 表示最大连接进程数为50个
    per_source  = 10    # 接受一个整数或“UNLIMITED”作为参数。指定每个源IP地址的最大服务实例数。
    v6only      = no    # 是否仅允许 IPv6 ?可以先暂时不启动 IPv6 支持!
    groups      = yes   # 如果groups属性被设置为“yes”,那么服务器执行时将访问服务器的有效UID所访问的组。或者,如果设置了group属性,则执行服务器并访问指定的组。如果将groups属性设置为“no”,则服务器不带补充组运行。
    umask       = 002   # 这个选项可以在"defaults"部分设置,为所有服务设置一个umask。xinetd将自己的掩码设置为先前的掩码或022。如果没有使用umask选项,所有子进程将继承这个umask。
}
includedir /etc/xinetd.d
[root@x86 ~]#

11.2.2.3、xinetd 守护 sshd

sshd 本身是个独立守护进程,为了增强 sshd 的功能,我们把 sshd 托管到 xinetd 下面,注意要先关闭 sshd 服务进程,并禁止开机自启,如果你是远程操作,请一定要把握操作顺序,并知道自己在干什么。

  1. 关闭 sshd 服务,禁止开机自启

    [root@x86 ~]# service sshd stop
    [root@x86 ~]# 
    [root@x86 ~]# chkconfig sshd off
  2. 在 /etc/xinetd.d/ 目录下面创建 sshd 的配置文件,默认是没有的,手动创建

    [root@x86 ~]# cat /etc/xinetd.d/sshd 
    service ssh                          # 代表被托管服务的名称
    {
           disable = no             # 是否禁用托管服务,no 表示开启托管服务
           log_on_failure += USERID # 设置失败时,UID 添加到系统登记表
           socket_type = stream     # socket连接方式,stream是tcp,dgram是udp
           cps = 25 30                  # 每秒25个入站连接,如果超过限制,则等待30秒。
           protocol = tcp               # 代表ssh走的是tcp协议连接
           wait = no                    # 该服务是单线程还是多线程的,一般udp为yes,tcp为no
           user = root                  # 以什么用户进行启动
           server = /usr/sbin/sshd      # 被托管服务的二进制启动脚本
           server_args = -i         # sshd 是独立守护进程,必须传入 -i 参数,否则无法被xinetd托管
           server_args = -D $OPTIONS    # 如果还要给server传参,该参数必须在 -i 参数下面,否则会出现连接闪退情况
    }
    [root@x86 ~]#
    • xinetd 启动的服务们有一个与独立守护进程不兼容的特定接口,这些服务不能打开和监听套接字,它们必须通过 stdin/stdout 与客户端通信。需要指定 server_args = -i 传给独立守护进程,才能被 xinetd 托管
    • socket_type 和 protocol 至少要有一个指定 tcp 协议,否则无法启动 sshd 服务。
    • 这里的 /usr/sbin/sshd 二进制脚本启动时有自己的配置文件 /etc/ssh/sshd_config,也就是说 sshd 原有的功能是不变的。
  3. 查看 sshd 22 号端口是否由 xinetd 托管

    [root@x86 ~]# lsof -i :22
    COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
    sshd    1427 root    3r  IPv4  12622      0t0  TCP 192.168.114.131:ssh->192.168.114.1:60477 (ESTABLISHED)
    xinetd  9959 root    5u  IPv6  24146      0t0  TCP *:ssh (LISTEN)
    [root@x86 ~]#
  4. 在 /etc/xinetd.d/sshd 里面添加访问控制

    [root@x86 ~]# cat /etc/xinetd.d/sshd
    service ssh                             
    {
           disable = no
           log_on_failure += USERID
           cps = 25 30
           wait = no
           user = root
           protocol = tcp
           server_args = -i
           server_args = -D $OPTIONS
           server = /usr/sbin/sshd
        only_from = 192.168.114.0/24    # 表示允许114网段访问
           no_access = 192.168.1.128        # 但是不允许128访问
    }
    [root@x86 ~]#
    
    # 测试,这台服务器地址就是 192.168.114.128
    [root@mysql ~]# ip -4 add | grep inet
       inet 127.0.0.1/8 scope host lo
       inet 192.168.114.128/24 brd 192.168.114.255 scope global dynamic ens32
    [root@mysql ~]#
    # 试验成功,拒绝连接
    [root@mysql ~]# ssh 192.168.114.131
    ssh_exchange_identification: read: Connection reset by peer
    [root@mysql ~]#
    • 注释不要加在配置文件里面,我是为了说明意思,加在里面启动不了服务的。
  5. 再添加多个控制

    • 控制这个服务最多只能 3 个连接,每个源 IP 只能 1 个连接
    • 控制只能 9:00 到 18:00 才能 ssh 连接
    • 指定日志记录到 /var/log/xinetd_ssh.log 里
    [root@x86 ~]# cat /etc/xinetd.d/sshd
    service ssh                             
    {
           disable = no
           log_on_failure += USERID
           cps = 25 30
           wait = no
           user = root
           protocol = tcp
           server_args = -i
           server_args = -D $OPTIONS
           server = /usr/sbin/sshd
        only_from = 192.168.114.0/24
           no_access = 192.168.114.128
        instances = 3                               # 最大连接数为3
           per_source = 1                               # 每个源IP只能有1个连接
           access_times = 9:00-18:00                    # 只能9:00到18:00才能ssh连接
           log_type = file /var/log/xinetd_ssh.log      # 指定日志记录到/var/log/xinetd_ssh.log里
    }
    [root@x86 ~]#

11.2.3、管理服务开机自启

chkconfig 命令能管理 /etc/init.d/ 目录下存在且脚本的内容满足一定条件的服务。

要能让 chkconfig 管理服务的开机是否自启动行为,只需将脚本放在 /etc/init.d 目录下,然后在脚本的前部加上 chkconfig 行和 description 行。如:

#!/bin/bash

# chkconfig: - 85 15
# description: The Apache HTTP Server is an efficient and extensible
  • 这两行必须在所有非注释行的前面,且这两行必须得被"注释"。
  • 其中 chkconfig 行 "-" 表示适用于运行级别 123456 上,85 表示开机启动时,它的启动顺序为 85,15 表示关机停止服务时,它的停止顺序为 15。description 行随便给一点描述信息就可以,但是必须得给 "description:" 关键字。

然后,就可以有 chkconfig 来管理服务的开机自启动了。

chkconfig [--add | --del] <name>  # 将/etc/init.d中可以被chkconfig管理的服务添加到chkconfig的管理列表中,或者从列表中删除
chkconfig [--list] [name]         # 列出指定名称的服务的开启自启动信息。name可以使用all来表示列出所有chkconfig管理列表中的服务
chkconfig [--level <levels>] <name> <on|off|reset>  # 将指定名称的服务在指定级别上打开开机自启动或关闭开机自启动功能。
                                                    # reset则表示重置为脚本中指定的级别

当然,除了 chkconfig 可以管理开机自启动,将启动命令放在 /etc/rc.d/rc.local 文件中也是可以的。

11.3、systemd 服务

下面会分一些子章节来介绍 systemd 服务,Linux 操作系统启动加载完内核后,启动第一个初代进程就是 systemd 进程。实际上从 centos 5、centos6 再到 centos 7 经历过 init、upstart 和 systemd 三个阶段,毫无疑问 systemd 作为初代进程是目前最好的选择。

11.3.1、初识 systemd

使用 systemd 做服务管理时,需要了解一些基本知识:

  1. 了解 systemd 可管理哪些服务
  2. 了解 systemd 所管理服务的状态
  3. 了解 systemctl 管理服务的基本命令
  4. 学会编写、修改、看懂服务 Unit 配置文件

11.3.1.1、systemd 可管理哪些服务

操作系统使用 systemd 后,所有用户进程都是 systemd 的后代进程。

[root@arm64v8 ~]# pstree -ph | more
systemd(1)-+-agetty(804)
           |-agetty(16547)
           |-auditd(747)---{auditd}(748)
           |-chronyd(791)
           |-crond(784)
           |-dbus-daemon(775)---{dbus-daemon}(777)
           |-firewalld(811)---{firewalld}(950)
           |-irqbalance(773)
           |-master(1970)---qmgr(2002)
           |-polkitd(780)-+-{polkitd}(805)
           |              |-{polkitd}(806)
           |              |-{polkitd}(807)
           |              |-{polkitd}(808)
           |              `-{polkitd}(809)
           |-rsyslogd(1122)-+-{rsyslogd}(1127)
           |                `-{rsyslogd}(1128)
           |-sshd(1118)---sshd(16844)---bash(16849)-+-more(16881)
           |                                        `-pstree(16880)
           |-systemd-journal(561)
           |-systemd-logind(774)
           |-systemd-udevd(585)
           `-tuned(1119)-+-{tuned}(1653)
                         |-{tuned}(1654)
                         |-{tuned}(1666)
                         `-{tuned}(1696)
[root@arm64v8 ~]#

当某个进程不在某个绑定终端的 Shell 进程下,该进程必然是脱离终端脱离 Shell 的 Daemon 类进程或其子孙进程。

虽然从进程树关系来看,所有进程都直接或间接地受到 systemd 的管理,但是,并非所有 systemd 的子进程都受 Systemd Unit 管理单元的管理。

只有那些由 systemd 方式启动的服务进程(比如 systemctl 命令启动) 才受到 Systemd Unit 管理单元的监控和管理。为了简化描述,后面均直接以 systemd 管理 来描述受 systemd unit 管理单元的管理。

比如,用户可以通过下面两种方式启动 Nginx 服务进程:

[root@arm64v8 ~]# nginx                             # (1)
[root@arm64v8 ~]# systemctl start nginx.service     # (2)
  • systemd 只能监控、管理第 (2) 种方式启动的 nginx 服务。比如第一种方式启动的 nginx,无法使用 systemctl stop nginx 来停止。

所以,systemd 下的直系子进程可分为两类:受 systemd 管理的子进程和不受 systemd 管理的子进程。

11.3.1.2、systemd 管理服务的命令

启动、停止服务:

systemctl start Service_Name1 Service_Name2
systemctl stop  Service_Name

服务重载、重启相关操作:

# 重载服务:服务未运行时不做任何事
systemctl reload Service_Name

# 重启服务:服务已运行时重启之,服务未运行时启动之
systemctl restart Service_Name

# 服务已运行时重启之,未运行时不启动之
systemctl try-restart Service_Name

# 服务已运行时,如果支持reload,则reload,如果不支持则restart
# 服务未运行时,启动之
systemctl reload-or-restart Service_Name

# 服务已运行时,如果支持reload,则reload,如果不支持则restart
# 服务未运行时,不做任何事
systemctl reload-or-try-restart Service_Name

服务状态查看操作:

# 查看服务状态
systemctl status Service_Name

# 检查服务是否active: 服务是否已启动
# 至少一个服务active时,返回0,否则返回非0退出状态码
systemctl is-active Service_Name1 Service_Name2
systemctl --quiet is-active Service_Name  # 静默模式

# 检查服务是否failed: 服务启动命令退出状态码非0或启动超时
systemctl is-failed Service_Name

11.3.1.3、systemd 所管理服务的状态

当使用 systemd 管理服务时,有必要了解服务的各种状态信息。

使用 systemctl list-units --type=service 可以列出 Service Unit 的状态信息:

[root@arm64v8 ~]# systemctl list-units --type=service
UNIT                                LOAD   ACTIVE SUB     DESCRIPTION
auditd.service                      loaded active running Security Auditing Service
chronyd.service                     loaded active running NTP client/server
crond.service                       loaded active running Command Scheduler
......
serial-getty@ttyS0.service          loaded active running Serial Getty on ttyS0
sshd-keygen.service                 loaded active exited  OpenSSH Server Key Generation
sshd.service                        loaded active running OpenSSH server daemon
......
LOAD   = Reflects whether the unit definition was properly loaded.
ACTIVE = The high-level unit activation state, i.e. generalization of SUB.
SUB    = The low-level unit activation state, values depend on unit type.

41 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.

使用 systemctl status Service_Name 命令可以查看到服务的状态信息。

[root@arm64v8 ~]# systemctl status sshd
● sshd.service - OpenSSH server daemon
   Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2021-08-03 06:16:18 EDT; 1h 21min ago
     Docs: man:sshd(8)
           man:sshd_config(5)
 Main PID: 1118 (sshd)
   CGroup: /system.slice/sshd.service
           └─1118 /usr/sbin/sshd -D

......
[root@arm64v8 ~]#
  • 重点在 Active 行,它描述了服务的运行状态。上面 active 位置可能的状态值有:

    • active
    • inactive
    • activating 或 deactivating
    • reloading
    • failed

    如果一个服务已经启动成功,它将成功加入到 systemd 监控队列(job queue),此后它将受到 systemd 监控和管理,此时该服务的状态为 active。

    如果一个服务在启动过程中失败,比如负责服务启动的命令(ExecStart 指令所指定) 在启动时以非 0 状态码退出、服务启动过程耗时过久导致超时、进程崩溃等,此时该服务的状态将处于 failed 状态,表示加入监控队列失败,事实上它加入到了某个描述启动失败的队列中。

    当一个服务未启动,或者服务已停止时,该服务的状态将处于 inactive 状态。

    有些服务启动、停止可能会消耗一点时间,在启动或停止的过程中去查看服务的状态,看到的状态信息可能会是 activating 或 deactivating 或 reloading。

  • 括号中的 running 位置可能的值有很多:

    [root@kylinv10 ~]# systemctl --state=help
    Available unit load states:
    stub
    ......
    
    Available unit active states:
    active
    ......
    
    Available unit file states:
    enabled
    ......
    
    Available automount unit substates:
    dead
    ......
    
    Available mount unit substates:
    ......
    failed
    
    Available path unit substates:
    waiting
    ......
    [root@kylinv10 ~]# 

虽然有很多种状态,但用户通常只需关注其中几项常见的即可。并且这些状态的存在通常都依赖于第一项 active 状态,比如 active(running)、active(exited)。

running 状态表明被 systemd 监控的服务进程正在运行。

dead 状态表明被 systemd 监控的进程已经死了(终结了),systemd 识别到这种状态后就会将其从监控队列中释放并不再监控它,所以它的第一项状态也会随之变为 inactive。

exited 表明被 systemd 监控的服务进程已经退出了或者 systemd 找不到它本该要监控的进程,但是管理者 systemd 认为它还没有死,只是认为它暂时退出了,所以通常会结合 active 状态一起出现,即 active(exited)。

  • 这种状态表明了一种意愿,服务进程已经消失了,但依然认为服务进程是活动的且正被监控。
    • 例如,当服务配置文件中配置了 RemainAfterExit=true 时,服务在退出后会进入 active(exited) 状态,还有多种其它可能会进入这种状态,通常来说意味着服务启动不正常(比如配置文件错误),但并非一定代表服务是失败的,它仍然可能会正常提供服务。

auto-restart 表明服务正在被 systemd 自动重启中,当服务配置文件设置了 Restart 且符合 Restart 规则时 systemd 会自动重启服务。

11.3.2、systemd 服务配置文件

11.3.2.1、systemd service

Systemd Service 是 systemd 提供的用于管理服务启动、停止和相关操作的功能,它极大的简化了服务管理的配置过程,用户只需要配置几项指令即可。

相比于 SysV 的服务管理脚本,用户不需要去编写服务的启动、停止、重启、状态查看等等一系列复杂的脚本代码。

systemd service 是 systemd 所管理的其中一项内容。实际上,systemd service 是 Systemd Unit 的一种,除了 Service,systemd 还有其他几种类型的 unit,比如 socket、slice、scope、target 等等。在这里,暂时了解两项内容:

  • Service 类型,定义服务程序的启动、停止、重启等操作和进程相关属性
  • Target 类型,主要目的是对 Service(也可以是其它 Unit) 进行分组、归类,可以包含一个或多个 Service Unit(也可以是其它 Unit)

此外,Systemd 作为管家,还将一些功能集成到了 Systemd Service 中,个人觉得比较出彩的两个集成功能是:

  • 用户可以直接在 Service 配置文件中定义 CGroup 相关指令来对该服务程序做资源限制。在以前,对服务程序做 CGroup 资源控制的步骤是比较繁琐的
  • 用户可以选择 Journal 日志而非采用 rsyslog,这意味着用户可以不用单独去配置 rsyslog,而且可以直接通过 systemctl 或 journalctl 命令来查看某服务的日志信息。当然,该功能并不适用于所有情况,比如用户需要管理日志时

Systemd Service 还有其它一些特性,比如可以动态修改服务管理配置文件,比如可以并行启动非依赖的服务,从而加速开机过程,等等。例如,使用 systemd-analyze blame 可分析开机启动各服务占用的时长:

[root@arm64v8 ~]# systemd-analyze blame 
         32.216s kdump.service
          8.040s NetworkManager-wait-online.service
          5.341s network.service
          1.272s postfix.service
          1.127s dev-sda3.device
           975ms firewalld.service
           838ms tuned.service
           703ms NetworkManager.service
           611ms systemd-hwdb-update.service
           466ms sshd-keygen.service
           368ms systemd-fsck-root.service
           351ms plymouth-start.service
           343ms chronyd.service
           296ms nginx.service
           269ms systemd-vconsole-setup.service
           130ms polkit.service
           104ms systemd-logind.service
            98ms sshd.service

# 从内核启动开始至开机结束所花时间
[root@arm64v8 ~]# systemd-analyze time
Startup finished in 4.863s (kernel) + 2.738s (initrd) + 44.161s (userspace) = 51.763s
[root@arm64v8 ~]#

11.3.2.2、systemd 配置文件路径

systemd service 配置文件都以 .service 结尾,systemd 将服务管理配置文件统一隐藏在 /usr/lib/systemd/system 目录下,同时暴露一个可供用户存放服务配置文件的目录 /etc/systemd/system。

如果用户需要,可以将服务配置文件手动存放至用户配置目录 /etc/systemd/system 下。该目录下的服务配置文件可以是普通 .service 文件,也可以是链接至 /usr/lib/systemd/system 目录下服务配置文件的软链接。

# 位于/usr/lib/systemd/system下的服务配置文件
# 注意带有`@`符号的文件名,它有特殊意义
[root@arm64v8 ~]# ls /usr/lib/systemd/system -1 | tail
system-update.target
teamd@.service
timers.target
timers.target.wants
time-sync.target
tmp.mount
tuned.service
umount.target
user.slice
wpa_supplicant.service
[root@arm64v8 ~]#

下面这些目录(*.target.wants) 定义各种类型下需要运行的服务

[root@arm64v8 ~]# ls -1dF /etc/systemd/system/*
/etc/systemd/system/basic.target.wants/
/etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service@
/etc/systemd/system/default.target@
/etc/systemd/system/default.target.wants/
/etc/systemd/system/getty.target.wants/
/etc/systemd/system/local-fs.target.wants/
/etc/systemd/system/multi-user.target.wants/
/etc/systemd/system/network-online.target.wants/
/etc/systemd/system/sysinit.target.wants/
/etc/systemd/system/system-update.target.wants/
[root@arm64v8 ~]#
  • sysinit.target.wants 目录下的是系统初始化过程运行的服务
  • getty.target.wants 目录下的是开机后启动虚拟终端时运行的服务
  • multi-user.target.wants 目录下的是多用户模式(对应运行级别2、3、4) 下运行的服务
  • 这里不一一介绍,其实大部分可以见名知义。

/etc/systemd/system/multi-user.target.wants 下的服务配置文件,几乎都是软链接

[root@arm64v8 ~]# ls -l /etc/systemd/system/multi-user.target.wants/ | awk '{print $9,$10,$11}' | tail
crond.service -> /usr/lib/systemd/system/crond.service
firewalld.service -> /usr/lib/systemd/system/firewalld.service
irqbalance.service -> /usr/lib/systemd/system/irqbalance.service
kdump.service -> /usr/lib/systemd/system/kdump.service
postfix.service -> /usr/lib/systemd/system/postfix.service
remote-fs.target -> /usr/lib/systemd/system/remote-fs.target
rhel-configure.service -> /usr/lib/systemd/system/rhel-configure.service
rsyslog.service -> /usr/lib/systemd/system/rsyslog.service
sshd.service -> /usr/lib/systemd/system/sshd.service
tuned.service -> /usr/lib/systemd/system/tuned.service
[root@arm64v8 ~]#

11.3.2.3、systemd service 文件格式

一个 Systemd Service 的服务配置文件大概长这样:

[Unit]
Description = some descriptions
Documentation = man:xxx(8) man:xxx_config(5)
Requires = xxx1.target xxx2.target
After = yyy1.target yyy2.target

[Service]
Type = <TYPE>
ExecStart = <CMD_for_START>
ExecStop = <CMD_for_STOP>
ExecReload = <CMD_for_RELOAD>
Restart = <WHEN_TO_RESTART>
RestartSec = <TIME>

[Install]
WantedBy = xxx.target yyy.target

一个 .Service 配置文件分为三部分:

  1. Unit:定义该服务作为 Unit 角色时相关的属性
  2. Service:定义本服务相关的属性
  3. Install:定义本服务在设置服务开机自启动时相关的属性。换句话说,只有在 创建/移除 服务配置文件的软链接时,Install 段才会派上用场。这一配置段不是必须的,当未配置 [Install] 时,设置开机自启动或禁止开机自启动的操作将无任何效果

[Unit] 和 [Install] 段的配置指令都来自于 man systemd.unit,这些指令都用于描述作为 Unit 时的属性,[Service] 段则专属于 .Service 服务配置文件。

这里先介绍一些常见的 [Unit] 和 [Install] 相关的指令(虽然支持的配置指令很多,但只需熟悉几个即可),之后再专门介绍 Service 段落的配置指令。

[Unit] 段落指令

Unit指令 含义
Description Unit 的描述信息
Documentation 本 Unit 的 man 文档路径
After 本服务在哪些服务启动之后启动,仅定义启动顺序,不定义服务依赖关系,即使要求先启动的服务启动失败,本服务也依然会启动
Before 本服务在哪些服务启动之前启动,仅定义启动顺序,不定义服务依赖关系。通常用于定义在关机前要关闭的服务,如 Before=shutdown.target
Wants 本服务在哪些服务启动之后启动,定义服务依赖关系,不定义服务启动顺序。启动本服务时,如果被依赖服务未启动,则也会启动被依赖服务。如果被依赖服务启动失败,本服务不会受之影响,因此本服务会继续启动。如果未结合 After 使用,则本服务和被依赖服务同时启动。当配置在 [Install] 段落中时,systemctl enable 操作将会将本服务安装到对应的 .wants 目录下(在该目录下创建一个软链接),在开机自启动时,.wants 目录中的服务会被隐式添加至目标 Unit 的 Wants 指令后。
Requires 本服务在哪些服务启动之后启动,定义服务强依赖关系,不定义服务启动顺序。启动本服务时,如果被依赖服务未启动,则也会启动被依赖服务。如果结合了 After,当存在非 active 状态的被依赖服务时,本服务不会启动。且当被依赖服务被手动停止时,本服务也会被停止,但有例外。如果要保证两服务之间状态必须一致,使用 BindsTo 指令。当配置在 [Install] 段落中时,systemctl enable 操作将会将本服务安装到对应的 .requires 目录下(在该目录下创建一个软链接),在开机自启动时,.requires 目录中的服务会被隐式添加至目标 Unit 的 Requires 指令后。
Requisite 本服务在哪些服务启动之后启动,定义服务依赖关系,不定义服务启动顺序。启动本服务时,如果被依赖服务处于尚未启动状态,则不会主动去启动这些服务,所以本服务直接启动失败。该指令一般结合 After 一起使用,以便保证启动顺序。
BindsTo 绑定两个服务,两服务的状态保证一致。如服务 1 为 active,则本服务也一定为 active。
PartOf 本服务是其它服务的一部分,定义了单向的依赖关系,且只对 stop 和 restart 操作有效。当被依赖服务执行 stop 或 restart 操作时,本服务也会执行操作,但本服务执行这些操作,不会影响被依赖服务。一般用于组合 target 使用,比如 a.service 和 b.service 都配置 PartOf=c.target,那么 stop c 的时候,也会同时 stop a 和 b。
Conflicts 定义冲突的服务,本服务和被冲突服务的状态必须相反。当本服务要启动时,将会停止目标服务,当启动目标服务时,将会停止本服务。启动和停止的操作同时进行,所以,如果想要让本服务在目标服务启动之前就已经处于停止状态,则必须定义 After/Before。
OnFailure 当本服务处于 failed 时,将启动目标服务。如果本服务配置了 Restart 重启指令,则在耗尽重启次数之后,本服务才会进入 failed。有时候这是非常有用的,一个典型用法是本服务失败时调用定义了邮件发送功能的 service 来发送邮件,特别地,可以结合 systemd.timer 定时任务实现 cron 的 MAILTO 功能。
RefuseManualStart
RefuseManualStop
本服务不允许手动启动和手动停止,只能被依赖时的启动和停止,如果手动启动或停止,则会报错。有些特殊的服务非常关键,或者某服务作为一个大服务的一部分,为了保证安全,都可以使用该特性。例如,系统审计服务 auditd.service 中配置了不允许手动停止指令 RefuseManualStop,network.target 中配置了不允许手动启动指令 RefuseManualStart。
AllowIsolated 允许使用 systemctl isolate 切换到本服务,只配置在 target 中。一般来说,用户服务是绝不可能用到这一项的。
ConditionPathExists
AssertPathExists
要求给定的绝对路径文件已经存在,否则不做任何事(condition) 或进入 failed 状态(assert),可在路径前使用!表示条件取反,即不存在时才启动服务。
ConditionPathIsDirectory
AssertPathIsDirectory
如上,路径存在且是目录时启动。
ConditionPathIsReadWrite
AssertPathIsReadWrite
如上,路径存在且可读可写时启动。
ConditionFileIsExecutable
AssertFileIsExecutable
如上,路径存在且是可执行普通文件时启动。

对于自定义的服务配置文件来说,需要定义的常见指令包括 Description、After、Wants 及可能需要的条件判断类指令。所以,Unit 段落是非常简单的。

[Install] 段落指令

下面是 [Install] 段落相关的指令,它们只在 systemctl enable/disable 操作时有效。如果期望服务开机自启动,一般只配置一个 WantedBy 指令,如果不期望服务开机自启动,则 Install 段落通常省略。

Install 指令 含义
WantedBy 本服务设置开机自启动时,在被依赖目标的 .wants 目录下创建本服务的软链接。例如 WantedBy = multi-user.target 时,将在 /etc/systemd/multi-user.target.wants 目录下创建本服务的软链接。
RequiredBy 类似 WantedBy,但是是在 .requires 目录下创建软链接。
Alias 指定创建软链接时链接至本服务配置文件的别名文件。例如 reboot.target 中配置了 Alias=ctrl-alt-del.target,当执行 enable 时,将创建 /etc/systemd/system/ctrl-alt-del.service 软链接并指向 reboot.target。
DefaultInstance 当是一个模板服务配置文件时(即文件名为 Service_Name@.service),该指令指定该模板的默认实例。例如 trojan@.service 中配置了 DefaultInstall=server 时,systemctl enable trojan@.service 时将创建名为 trojan@server.service 的软链接。

例如,下面是 sshd 的服务配置文件 /usr/lib/systemd/system/sshd.service,只看 Unit 段落和 Install 段落,是否很简单。

[root@arm64v8 ~]# cat /usr/lib/systemd/system/sshd.service 
[Unit]
Description=OpenSSH server daemon
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target sshd-keygen.service
Wants=sshd-keygen.service

[Service]
......

[Install]
WantedBy=multi-user.target
[root@arm64v8 ~]#

[Service] 段配置

Systemd Service 配置文件中的 [Service] 段落可配置的指令很多,可配置在此段落中的指令来源有多处,包括:

  • man systemd.service 中描述的专属于 Service Unit 的指令,一般是定义服务进程启动、停止、重启等管理行为的指令。比如 ExecStart 指令指定服务启动时执行的命令。
  • man systemd.exec 中描述的指令,一般是定义服务进程的启动环境、运行环境的指令。例如指定工作目录、指定服务进程的运行用户、指定环境变量、指定服务 OOM 相关属性等。
  • man systemd.kill 中描述的指令,一般是定义终止服务进程相关的指令,比如终止服务时发送什么信号、服务进程没杀死时如何处理等。
  • man resource-control 中描述的指令,一般是定义服务进程关于资源控制相关的指令,即配置服务进程的 CGroup。比如该服务进程最多允许使用多少内存、使用哪些 CPU、最多占用多少百分比的 CPU 时间等。

例如,/usr/lib/systemd/system/rsyslog.service 文件的内容:

[Service]
EnvironmentFile=-/etc/sysconfig/rsyslog             # 来自systemd.exec
UMask=0066                                          # 来自systemd.exec
StandardOutput=null                                 # 来自systemd.exec

Type=notify                                         # 来自systemd.service
ExecStart=/usr/sbin/rsyslogd -n $SYSLOGD_OPTIONS    # 来自systemd.service
Restart=on-failure                                  # 来自systemd.service

再比如,想要限制一个服务最多允许使用 300M 内存(比如 512M 的 vps 主机运行一个比较耗内存的博客系统时,可设置内存使用限制),最多 30% CPU 时间:

[Service]
MemoryLimit=300M
CPUQuota=30%
ExecStart=xxx

此外还需要了解 systemd 的一项功能,systemctl set-property,它可以在线修改已启动服务的属性。例如

# 直接限制,且写入配置文件,所以下次启动服务也会被限制
[root@arm64v8 ~]# ll /etc/systemd/system/sshd.service.d/
total 0
[root@arm64v8 ~]# 
[root@arm64v8 ~]# systemctl set-property sshd.service CPUQuota=20% 
[root@arm64v8 ~]# systemctl set-property sshd.service MemoryLimit=100M 
[root@arm64v8 ~]# 
[root@arm64v8 ~]# ll /etc/systemd/system/sshd.service.d/
total 8
-rw-r--r--. 1 root root 23 Aug  3 08:58 50-CPUQuota.conf
-rw-r--r--. 1 root root 32 Aug  3 08:58 50-MemoryLimit.conf
[root@arm64v8 ~]#

# 直接限制,不写入配置文件,所以下次启动服务不会被限制
[root@arm64v8 ~]# systemctl set-property nginx MemoryLimit=100M --runtime

目前来说,可以不用过多关注来自其它位置的指令,应该给予重点关注的是来自 systemd.service 自身的指令,比如:

  • Type:指定服务的管理类型
  • ExecStart:指定启动服务时执行的命令行
  • ExecStop:指定停止服务时运行的命令
  • ExecReload:指定重载服务进程时运行的命令
  • Restart:指定 systemd 是否要自动重启服务进程以及什么情况下重启

特别是 Type 指令,它直接影响 [Service] 段中的多项配置方式。

  • simple
  • exec
  • forking
  • oneshot
  • dbus
  • notify
  • idle

如果配置的是服务进程,Type 的值很可能是 forking 或 simple,如果是普通命令的进程,Type 的值可能是 simple、oneshot。

而 dbus 类型一般情况下用不上,notify 要求服务程序中使用代码对 systemd notify 进行支持,所以多数情况下可能也用不上。

官方建议:尽量使用 simple 类型,不仅配置简单,而且性能俱佳。

11.3.2.4、systemd service:Type=forking

当使用 systemd 去管理一个长久运行的服务进程时,最常用的 Type 是 forking 类型。

使用 Type=forking 时,要求 ExecStart 启动的命令自身就是以 daemon 模式运行的。而以 daemon 模式运行的进程都有一个特性:总是会有一个瞬间退出的中间父进程。

例如,nginx 命令默认以 daemon 模式运行,所以可直接将其配置为 forking 类型:

[root@arm64v8 ~]# cp /usr/lib/systemd/system/nginx.service /usr/lib/systemd/system/nginx.service.backup
[root@arm64v8 ~]# cat /usr/lib/systemd/system/nginx.service
[Unit]
Description = Rewrite by Brinnatt@gmail.com for understanding Type=forking

[Service]
Type = forking
ExecStart = /usr/sbin/nginx
[root@arm64v8 ~]#
[root@arm64v8 ~]# systemctl daemon-reload
[root@arm64v8 ~]# systemctl start nginx.service
[root@arm64v8 ~]# systemctl status nginx.service
● nginx.service - Rewrite by Brinnatt@gmail.com for understanding Type=forking
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; static; vendor preset: disabled)
   Active: active (running) since Tue 2021-08-03 09:11:49 EDT; 6s ago
  Process: 18029 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
 Main PID: 18030 (nginx)
   Memory: 59.4M
   CGroup: /system.slice/nginx.service
           ├─18030 nginx: master process /usr/sbin/nginx
           ├─18031 nginx: worker process
           ├─18032 nginx: worker process
           ├─18033 nginx: worker process
           ├─18034 nginx: worker process
           ├─18035 nginx: worker process
           ├─18036 nginx: worker process
           ├─18037 nginx: worker process
           ├─18038 nginx: worker process
           ├─18039 nginx: worker process
           ├─18040 nginx: worker process
           ├─18041 nginx: worker process
           ├─18042 nginx: worker process
           ├─18043 nginx: worker process
           ├─18044 nginx: worker process
           ├─18045 nginx: worker process
           └─18046 nginx: worker process
......
[root@arm64v8 ~]#
  • 注意上面 status 报告的信息中,ExecStart 启动的 nginx 的进程 PID=18029,且该进程的状态是已退出,退出状态码为 0,这个进程是 daemon 类进程创建过程中瞬间退出的中间父进程。
  • 在 forking 类型中,该进程称为初始化进程。同时还有一行 Main PID: 18030 (nginx),这是 systemd 真正监控的 nginx 服务主进程,其 PID=18030,是 PID=18029 进程的子进程。

Type=forking 类型代表什么呢?要解释清楚该 type,需从进程创建开始说起。

systemd fork daemon

下面我们来梳理一下这个过程:

  1. systemd 作为用户空间第一个进程由内核加载。
  2. systemd 负责加载所有的 daemon 进程。
    1. 以 nginx 为例,在 nginx 服务进程启动时,由 pid=1 的 systemd 进程 fork() 一个子 systemd 进程(pid=18029)。
    2. 子 systemd 进程分支通过 systemd.exec 配置该子进程环境,然后使用 exec() 去调用 ExecStart 指定的 nginx 服务启动命令。
    3. exec() 调用程序时会替换当前进程,所以启动后的 nginx 服务进程(pid=18030) 将会替代子 systemd 进程(pid=18029),最终 nginx 服务进程自身成为 systemd(pid=1) 进程的子进程。

对于 Type=forking 来说,pid=1 的 systemd 进程 fork 出来的子进程正是瞬间退出的中间父进程,且 systemd 会在中间父进程退出后就认为服务启动成功,此时 systemd 可以立即去启动后续需要启动的服务。

如果 Type=forking 服务中的启动命令是一个前台命令会如何呢?比如将 sleep 配置为 forking 模式,将 nginx daemon off 配置为 forking 模式等。

答案是 systemd 会一直等待 ExecStart 启动的进程被当作中间父进程退出,而 systemd 加载前台进程不像加载 daemon 进程那样会先 fork() 一个瞬时父进程,所以就一直等待前台进程结束或超时。在等待过程中,systemctl start 会一直卡住,直到等待超时而失败,在此阶段中,systemctl status 将会查看到服务处于 activating 状态

注意:即使 systemctl start 会一直卡住,服务器依然可以处理外来请求,直到服务超时,activating 状态变成 failed。此时,服务器不能处理外来请求,如果系统管理员不理解,就会感到莫名其妙。

[root@arm64v8 ~]# cat /usr/lib/systemd/system/nginx.service
[Unit]
Description = Rewrite by Brinnatt@gmail.com for understanding Type=forking

[Service]
Type = forking
ExecStart = /usr/sbin/nginx -g 'daemon off;'
[root@arm64v8 ~]# 
[root@arm64v8 ~]# systemctl daemon-reload 
[root@arm64v8 ~]# systemctl start nginx.service
`卡住了`

# 复制一个会话终端
[root@arm64v8 ~]# systemctl status nginx
● nginx.service - Rewrite by Brinnatt@gmail.com for understanding Type=forking
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; static; vendor preset: disabled)
   Active: activating (start) since Tue 2021-08-03 09:37:43 EDT; 20s ago
  Control: 18118 (nginx)
   Memory: 43.7M
   CGroup: /system.slice/nginx.service
           ├─18118 nginx: master process /usr/sbin/nginx -g daemon off;
           ├─18119 nginx: worker process
           ├─18120 nginx: worker process
           ├─18121 nginx: worker process
           ├─18122 nginx: worker process
           ├─18123 nginx: worker process
           ├─18124 nginx: worker process
           ├─18125 nginx: worker process
           ├─18126 nginx: worker process
           ├─18127 nginx: worker process
           ├─18128 nginx: worker process
           ├─18129 nginx: worker process
           ├─18130 nginx: worker process
           ├─18131 nginx: worker process
           ├─18132 nginx: worker process
           ├─18133 nginx: worker process
           └─18134 nginx: worker process
......
[root@arm64v8 ~]#

当设置 Type=forking 时,有一个 GuessMainPID 指令其默认值为 yes,它表示 systemd 会通过一些算法去猜测Main PID,因为 Main PID 的父进程已瞬间退出,如果猜错了将导致服务启动异常,这也是有可能的。

好在,Type=forking 时的 systemd 提供了 PIDFile 指令( Type=forking 通常都会结合 PIDFile 指令),systemd 会从 PIDFile 指令所指定的 PID 文件中获取服务的主进程 PID。

例如,编写一个 nginx 的服务配置文件:

[root@arm64v8 ~]# cat /usr/lib/systemd/system/nginx.service
[Unit]
Description = Rewrite by Brinnatt@gmail.com for understanding Type=forking

[Service]
Type = forking
PIDFile = /run/nginx.pid
ExecStartPre = /usr/bin/rm -f /run/nginx.pid
ExecStart = /usr/sbin/nginx
ExecStartPost = /usr/bin/sleep 0.1
[root@arm64v8 ~]#

关于 PIDFile,有必要去了解一些注意事项,否则它们可能就会成为你的坑:

  • 首先,PIDFile 只适合在 Type=forking 模式下使用,其它时候没必要使用,因为其它类型的 Service 主进程的 PID 都是确定的。

    • systemd 推荐 PIDFile 指定的 PID 文件在 /run 目录下,所以,可能需要修改服务程序的配置文件,将其 PID 文件路径修改为 /run 目录之下,当然这并非必须。但有一点必须注意,PIDFile 指令的值要和服务程序的 PID 文件路径保持一致。

    例如 nginx 的相关配置:

    [root@arm64v8 ~]# grep -i "nginx.pid" /etc/nginx/nginx.conf
    pid /run/nginx.pid;
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# grep -i "nginx.pid" /usr/lib/systemd/system/nginx.service
    PIDFile = /run/nginx.pid
    ExecStartPre = /usr/bin/rm -f /run/nginx.pid
    [root@arm64v8 ~]#
  • 其次,systemd 会在中间父进程退出后立即读取这个 PID 文件,读取成功后就认为该服务已经启动成功。systemd 读取 PIDFile 的时候,服务主进程可能还未将 PID 写入到 PID 文件中,这时 systemd 将出现问题。

    • 所以,对于服务程序的开发人员来说,应尽早将主进程写入到 PID 文件中,比如可以在中间父进程 fork 完之后立即写入 PID 文件,然后再退出,而不是在 fork 出来的服务主进程内部由主进程负责写入。

    • 万一某个版本的 nginx 出现该问题,解决办法是使用 ExecStartPost=/usr/bin/sleep 0.1,让 systemd 在初始化进程(即中间父进程)退出之后耽搁 0.1 秒再继续向下执行,即推迟了 systemd 读取 PID 的过程,保证能让 systemd 从 PID 文件中读取到值。

    [root@arm64v8 ~]# cat /usr/lib/systemd/system/nginx.service
    [Unit]
    Description=The nginx HTTP and reverse proxy server
    After=network.target remote-fs.target nss-lookup.target
    
    [Service]
    Type=forking
    PIDFile=/run/nginx.pid
    # Nginx will fail to start if /run/nginx.pid already exists but has the wrong
    # SELinux context. This might happen when running nginx -t from the cmdline.
    # https://bugzilla.redhat.com/show_bug.cgi?id=1268621
    ExecStartPre=/usr/bin/rm -f /run/nginx.pid
    ExecStartPre=/usr/sbin/nginx -t
    ExecStart=/usr/sbin/nginx
    ExecStartPost=/usr/bin/sleep 0.1    # 加这一行
    ExecReload=/bin/kill -s HUP $MAINPID
    KillSignal=SIGQUIT
    TimeoutStopSec=5
    KillMode=process
    PrivateTmp=true
    
    [Install]
    WantedBy=multi-user.target
    [root@arm64v8 ~]#

最后,systemd 只会读 PIDFile 文件而不会写,也不会创建它。但是在停止服务的时候,systemd 会尝试删除 PID 文件。但是服务进程可能会异常终止,导致已终止的服务进程的 PID 文件仍然保留着,所以在使用 PIDFile 指令时,通常还会使用 ExecStartPre 指令来删除可能已经存在的 PID 文件。正如上面给出的 nginx 配置文件一样。

11.3.2.5、systemd service:Type=simple

Type=simple 是一种最常见的通过 systemd 服务系统运行用户自定义命令的类型,也是省略 Type 指令时的默认类型。适合那些在 shell 下运行在前台的命令。

例如,编写一个 /usr/lib/systemd/system/sleep.service 运行 sleep 进程:

[root@arm64v8 ~]# cat /usr/lib/systemd/system/sleep.service
[Unit]
Description = written by Brinnatt@gmail.com for testing Type=simple

[Service]
Type = simple
ExecStart = /usr/bin/sleep 10  # 命令必须使用绝对路径
[root@arm64v8 ~]#  

使用 daemon-reload 重载并启动该服务进程:

[root@arm64v8 ~]# systemctl daemon-reload 
[root@arm64v8 ~]# systemctl start sleep.service 
[root@arm64v8 ~]# systemctl status sleep.service
● sleep.service - written by Brinnatt@gmail.com for testing Type=simple
   Loaded: loaded (/usr/lib/systemd/system/sleep.service; static; vendor preset: disabled)
   Active: active (running) since Tue 2021-08-03 23:54:54 EDT; 6s ago
 Main PID: 22677 (sleep)
   Memory: 512.0K
   CGroup: /system.slice/sleep.service
           └─22677 /usr/bin/sleep 10
......
[root@arm64v8 ~]#

10 秒内,sleep 进程以 daemon 模式运行在后台,就像一个服务进程一样。10 秒之后,sleep 退出,于是 systemd 将该进程从监控队列中踢出。再次查看进程的状态将是 inactive:

[root@arm64v8 ~]# systemctl status sleep.service
● sleep.service - written by Brinnatt@gmail.com for testing Type=simple
   Loaded: loaded (/usr/lib/systemd/system/sleep.service; static; vendor preset: disabled)
   Active: inactive (dead)
......

上面的服务配置文件中的 ExecStart 指令指定启动本服务时执行的命令,即启动一个本该前台运行的 sleep 进程作为服务进程在后台运行。

注意:systemd service 的命令行中必须使用绝对路径,且只能编写单条命令(Type=oneshot时除外),如果要命令续行,可在尾部使用反斜线符号 \ 等。此外,命令行中支持部分类似 Shell 的特殊符号,但不支持重定向 > >> << <、管道 |、后台符号 &,具体可参考 man systemd.service 中 command line 段落的解释说明。

当 Type=simple 时,systemd 将会认为在 main service process 被 fork 之后 unit 就已经启动了;而服务配置文件中 ExecStart 指定的服务就是 main service process。

在这种模式下,如果 main sevice process 要为其它系统进程提供功能,那么沟通通道就应该在 service 启动之前建立起来;因为 systemd 就在创建 main service process 之后,在执行 service 的二进制程序之前,要继续启动下一批 units。

这就意味着使用 systemctl start 命令行启动的 Type=simple 服务,即便是 services 的二进制程序不能成功执行,也会报告成功。

  • 不能成功执行 services 二进制程序的情况,比如说 User= 不存在,services 二进制文件丢失等。

如果 simple 类型下 ExecStart 启动的命令本身就是以 daemon 模式运行的呢?

  • 其结果是 systemd 默认会立刻杀掉所有属于服务的进程。
  • 原因也很简单,daemon 类进程总是会有一个瞬间退出的中间父进程,而在 simple 类型下,systemd 所 fork 出来的子进程正是这个中间父进程,所以 systemd 会立即发现这个中间父进程的退出,于是杀掉其它所有服务进程。

例如,以运行 bash -c '(sleep 3000 &)' 的 simple 类型的服务,被 systemd 监控的 bash 进程会在启动 sleep 后立即退出,于是 systemd 会立即杀掉属于该服务的 sleep 进程。

[root@arm64v8 ~]# cat /usr/lib/systemd/system/sleep.service
[Unit]
Description = written by Brinnatt@gmail.com for testing Type=simple

[Service]
Type=simple
ExecStart=/usr/bin/bash -c '( /usr/bin/sleep 30 &)'
[root@arm64v8 ~]# 
[root@arm64v8 ~]# systemctl daemon-reload 
[root@arm64v8 ~]# systemctl start sleep.service 
[root@arm64v8 ~]# systemctl status sleep.service
● sleep.service - written by Brinnatt@gmail.com for testing Type=simple
   Loaded: loaded (/usr/lib/systemd/system/sleep.service; static; vendor preset: disabled)
   Active: inactive (dead)

再例如,nginx 命令默认是以 daemon 模式运行的,simple 类型下直接使用 nginx 命令启动服务,systemd 会立刻杀掉所有 nginx,即 nginx 无法启动成功。

[root@arm64v8 ~]# cat /usr/lib/systemd/system/nginx.service
[Unit]
Description = Rewrite by Brinnatt@gmail.com for understanding Type=forking

[Service]
Type = simple
ExecStart = /usr/sbin/nginx
[root@arm64v8 ~]# 
[root@arm64v8 ~]# systemctl daemon-reload 
[root@arm64v8 ~]# systemctl start nginx.service
[root@arm64v8 ~]# systemctl status nginx.service
● nginx.service - Rewrite by Brinnatt@gmail.com for understanding Type=forking
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; static; vendor preset: disabled)
   Active: inactive (dead)

但如果将 nginx 进程以非 daemon 模式运行,simple 类型的 nginx 服务将正常启动:

[root@arm64v8 ~]# cat /usr/lib/systemd/system/nginx.service
[Unit]
Description = Rewrite by Brinnatt@gmail.com for understanding Type=forking

[Service]
Type = simple
ExecStart = /usr/sbin/nginx -g 'daemon off;'
[root@arm64v8 ~]# 
[root@arm64v8 ~]# systemctl daemon-reload 
[root@arm64v8 ~]# systemctl start nginx.service
[root@arm64v8 ~]# systemctl status nginx.service
● nginx.service - Rewrite by Brinnatt@gmail.com for understanding Type=forking
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; static; vendor preset: disabled)
   Active: active (running) since Thu 2021-08-05 01:16:27 EDT; 7s ago
 Main PID: 26463 (nginx)
   Memory: 58.7M
   CGroup: /system.slice/nginx.service
           ├─26463 nginx: master process /usr/sbin/nginx -g daemon off;
           ├─26464 nginx: worker process
           ├─26465 nginx: worker process
           ├─26466 nginx: worker process
           ├─26467 nginx: worker process
           ├─26468 nginx: worker process
           ├─26469 nginx: worker process
           ├─26470 nginx: worker process
           ├─26471 nginx: worker process
           ├─26472 nginx: worker process
           ├─26473 nginx: worker process
           ├─26474 nginx: worker process
           ├─26475 nginx: worker process
           ├─26476 nginx: worker process
           ├─26477 nginx: worker process
           ├─26478 nginx: worker process
           └─26479 nginx: worker process
           ......

11.3.2.6、Systemd Service:其它 Type

除了 simple 和 forking 类型,还有 exec、oneshot、idle、notify 和 dbus 类型,这里不考虑 notify 和 dbus,剩下的 exec、oneshot 和 idle 都类似于 simple 类型。

  • simple:在 fork 出子 systemd 进程后,systemd 就认为该服务启动成功了
  • exec:在 fork 出子 systemd 进程且子 systemd 进程 exec() 调用 ExecStart 命令成功后,systemd 认为该服务启动成功
  • oneshot:在 ExecStart 命令执行完成退出后,systemd 才认为该服务启动成功
    • 因为服务进程退出后 systemd 才继续工作,所以在未配置 RemainAfterExit 指令时,oneshot 类型的服务永远无法出现 active 状态,它直接从启动状态到 activating 到 deactivating 再到 dead 状态
    • 当结合 RemainAfterExit 指令时,在服务进程退出后,systemd 会继续监控该 Unit,所以服务的状态为 active(exited),通过这个状态可以让用户知道,该服务曾经已经运行成功,而不是从未运行过
    • 通常来说,对于那些执行单次但无需长久运行的进程来说,可以采用 type=oneshot,比如启动 iptables,挂载文件系统的操作、关机或重启的服务等
  • idle:idle 的表现行为跟 simple 很像,但是 service 程序的实际执行会延迟,直到所有 active jobs 被派遣完成。这可以用来避免 shell 服务的输出与控制台上的状态输出相交叉。此类型仅用于改进控制台输出,很少会有人用。

通过分析 man systemd.service 官方文档,其中描述并不严谨,经过以上实验,只需要记住,针对 daemon yes 的程序使用 forking 类型;针对 daemon no 的程序使用 simple 类型。优先使用 simple 类型。

11.3.3、模板型服务配置文件

systemd service 支持简单的模板型 Unit 配置文件,在 Unit 配置文件中可以使用 %n %N %p %i... 等特殊符号进行占位,在 systemd 读取配置文件时会将这些特殊符号解析并替换成对应的值。

这些特殊符号的含义可参见 man systemd.unit。通常情况下只会使用到 %i 或 %I,其它特殊符号用到的机会较少。

  • 使用 %i %I 这两个特殊符号时,要求 Unit 的文件名以 @ 为后缀,即文件名格式为 Service_Name@.service。当使用 systemctl 管理这类服务时,@ 符号后面的字符会传参到 Unit 模板文件中的 %i 或 %I,官方称之为实例化。

有时候这是很实用的。比如有些程序既是服务端程序又是客户端程序,区分客户端和服务端的方式是使用不同配置文件。

假设用户想在一个机器上同时运行 trojan 程序的服务端和客户端,可编写如下 Unit 服务配置文件:

[root@arm64v8 ~]# cat /usr/lib/systemd/system/trojan@.service
[Unit]
Description = trojan to flee

[Service]
Type = forking
ExecStart = /usr/local/bin/trojan --config /etc/trojan/%i.conf

现在用户可以在 /etc/trojan 目录下同时提供服务程序的服务端配置文件和客户端配置文件。

/etc/trojan/server.conf
/etc/trojan/client.conf

如果要管理该主机上的服务端:

systemctl start trojan@server.service
systemctl status trojan@server.service
systemctl stop trojan@server.service
systemctl enable trojan@server.service
systemctl disable trojan@server.service

如果要管理该主机上的客户端:

systemctl start trojan@client.service
systemctl status trojan@client.service
systemctl stop trojan@client.service
systemctl enable trojan@client.service
systemctl disable trojan@client.service

注意:Systemd 在运行服务时,总是会先尝试找到一个完全匹配的 Unit 文件,如果没有找到,才会尝试选择匹配模板。比如 systemctl start trojan@server.service,systemd 会先去找完全匹配的 trojan@server.service 这个 Unit 文件;如果找不到,才会使用 trojan@.service 这个模板文件将服务实例化。

11.3.4、使用 target 组合多个服务

有些时候,一个大型服务可能由多个小服务组成。

比如 c 服务由 a.service 和 b.service 组成,因为组合了两个服务,所以 c 服务可以定义为 c.target。

a.service 内容:

[root@arm64v8 ~]# cat /usr/lib/systemd/system/a.service 
[Unit]
Description = a.service
PartOf = c.target
Before = c.target

[Service]
ExecStart = /usr/local/bin/pinga.sh
[root@arm64v8 ~]# 

b.service 内容:

[root@arm64v8 ~]# cat /usr/lib/systemd/system/b.service 
[Unit]
Description = b.service
PartOf = c.target
Before = c.target

[Service]
ExecStart = /usr/local/bin/pingb.sh
[root@arm64v8 ~]#

c.target 内容:

[root@arm64v8 ~]# cat /usr/lib/systemd/system/c.target 
[Unit]
Description = c.service, consists of a.service and b.service
After = a.service b.service
Wants = a.service b.service
[root@arm64v8 ~]#
[root@arm64v8 ~]# systemctl start c.target
[root@arm64v8 ~]# 
[root@arm64v8 ~]# systemctl status c.target a.service b.service
● c.target - c.service, consists of a.service and b.service
   Loaded: loaded (/usr/lib/systemd/system/c.target; static; vendor preset: disabled)
   Active: active since Thu 2021-08-05 22:40:52 EDT; 13s ago

Aug 05 22:40:52 arm64v8 systemd[1]: Reached target c.service, consists of a.service and...ce.
Aug 05 22:40:52 arm64v8 systemd[1]: Starting c.service, consists of a.service and b.service.

● a.service
   Loaded: loaded (/usr/lib/systemd/system/a.service; static; vendor preset: disabled)
   Active: active (running) since Thu 2021-08-05 22:40:52 EDT; 13s ago
 Main PID: 28788 (pinga.sh)
   Memory: 3.6M
   CGroup: /system.slice/a.service
           ├─28788 /bin/bash /usr/local/bin/pinga.sh
           └─28790 ping 10.60.20.74

Aug 05 22:40:52 arm64v8 systemd[1]: Started a.service.
Aug 05 22:40:52 arm64v8 systemd[1]: Starting a.service...

● b.service
   Loaded: loaded (/usr/lib/systemd/system/b.service; static; vendor preset: disabled)
   Active: active (running) since Thu 2021-08-05 22:40:52 EDT; 13s ago
 Main PID: 28789 (pingb.sh)
   Memory: 4.9M
   CGroup: /system.slice/b.service
           ├─28789 /bin/bash /usr/local/bin/pingb.sh
           └─28791 ping 10.60.20.75

Aug 05 22:40:52 arm64v8 systemd[1]: Started b.service.
Aug 05 22:40:52 arm64v8 systemd[1]: Starting b.service...
Hint: Some lines were ellipsized, use -l to show in full.
[root@arm64v8 ~]#

c 中配置 Wants 表示 a 和 b 先启动,但启动失败不会影响 c 的启动。如果要求 c.target 和 a.service、b.service 的启动状态一致,可将 Wants 替换成 Requires 或 BindsTo 指令。

PartOf 指令表明 a.service 和 b.service 是 c.target 的一部分,停止或重启 c.target 的同时,也会停止或重启 a 和 b。再加上 c.target 中配置了 Wants 指令(也可以改成 Requires 或 BindsTo),使得启动 c 的时候,a 和 b 也已经启动完成。

但是要注意,PartOf 是单向的,停止和重启 a 或 b 的时候不会影响 c。

11.3.5、systemd 开机自启任务

如果要让任务开机自启动,需将对应的 Unit 文件存放于 /etc/systemd/system 下。本文以 Service Unit 为例,但也支持让 path Unit、timer Unit 等类型的任务开机自启动。

11.3.5.1、systemd 服务开机自启

用户可以手动将服务配置文件存放至此路径,但更建议采用 systemd 系统提供的上层工具 systemctl 来操作。

# 将服务加入开机自启动
systemctl enable Service_Name

# 禁止服务开机自启动
systemctl disable Service_Name

# 查看服务是否开机自启动
systemctl is-enabled Service_Name

# 查看所有开机自启动服务
systemctl list-unit-files --type service | grep 'enabled'

使用 systemctl 命令时,可以指定服务名称,也可以指定服务对应的服务配置 unit 文件。

例如下面两条命令是等价的。

systemctl enable sshd          # 服务名
systemctl enable sshd.service  # 服务对应的unit文件

systemctl 的很多操作都具备幂等性,这意味着如果要操作的服务已经处于目标状态,则什么都不会做。

如果是未开机自启动的服务加入开机自启动呢?比如,拷贝 sshd 服务的配置文件,并将拷贝后的服务 sshd1 加入开机自启动:

[root@arm64v8 ~]# cp /usr/lib/systemd/system/{sshd,sshd1}.service

[root@arm64v8 ~]# systemctl enable sshd1
Created symlink from /etc/systemd/system/multi-user.target.wants/sshd1.service to /usr/lib/systemd/system/sshd1.service.

从结果可看到,systemctl 将服务加入开机自启动的操作,实际上是在 /etc/systemd/system 某个 target.wants 目录下创建服务配置文件的软链接文件。

[root@arm64v8 ~]# readlink /etc/systemd/system/multi-user.target.wants/sshd1.service 
/usr/lib/systemd/system/sshd1.service

显然,禁用服务开机自启动的操作是移除软链接。

[root@arm64v8 ~]# systemctl disable sshd1
Removed symlink /etc/systemd/system/multi-user.target.wants/sshd1.service.

最后,如果服务已经加入开机自启动,但想要再次加入(比如更新了 /usr/lib/systemd/system 下的服务配置文件),可在 enable 时加上 –force 选项:

[root@arm64v8 ~]# systemctl --force enable Service_Name

11.3.5.2、systemd 中自定义开机自启动命令/脚本

在 SysV 系统中,要让某个命令或某个脚本开机自启动,可以将命令或执行脚本的命令行写入 /etc/rc.local。

在 systemd 中,要让某个命令或某个脚本开机自启动,要么将其编写成一个开机自启动服务,要么通过 systemd 兼容的 /etc/rc.local。

下面是一个简单的让命令(脚本)开机自启动的配置文件:

[root@arm64v8 ~]# cat /usr/local/bin/ping.sh
#!/bin/bash
ping 10.60.20.72 >> /tmp/ping.txt
[root@arm64v8 ~]# 
[root@arm64v8 ~]# cat /usr/lib/systemd/system/ping.service 
[Unit]
Description = ping shell script
ConditionFileIsExecutable = /usr/local/bin/ping.sh  # 要求脚本具有可执行权限

[Service]
ExecStart = /usr/local/bin/ping.sh  # 指定要运行的命令、脚本

[Install]
WantedBy = multi-user.target    # 这段不能少

[root@arm64v8 ~]# 
[root@arm64v8 ~]# systemctl daemon-reload 
[root@arm64v8 ~]# systemctl enable ping.service
Created symlink from /etc/systemd/system/multi-user.target.wants/ping.service to /usr/lib/systemd/system/ping.service.
[root@arm64v8 ~]# systemctl start ping.service
[root@arm64v8 ~]#

如果要使用 /etc/rc.local 的方式呢?systemd 提供了 rc-local.service 服务来加载 /etc/rc.d/rc.local 文件中的命令。

[root@arm64v8 ~]# cat /usr/lib/systemd/system/rc-local.service 
#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.

# This unit gets pulled automatically into multi-user.target by
# systemd-rc-local-generator if /etc/rc.d/rc.local is executable.
[Unit]
Description=/etc/rc.d/rc.local Compatibility
ConditionFileIsExecutable=/etc/rc.d/rc.local
After=network.target

[Service]
Type=forking
ExecStart=/etc/rc.d/rc.local start
TimeoutSec=0
RemainAfterExit=yes
[root@arm64v8 ~]#
  • 这个文件缺少了 [Install] 段且没有 WantedBy,后面将会解释 Install 中的 WantedBy 表示设置该服务开机自启动时,该服务加入到哪个 运行级别 中启动。
  • 但这个文件的注释中说明了,如果 /etc/rc.d/rc.local 文件存在且具有可执行权限,则 systemd-rc-local-generator 将会自动添加到 multi-user.target 中,所以,即使没有 Install 和 WantedBy 也无关紧要。

另一方面需要注意,和 SysV 风格系统在系统启动的最后阶段运行 rc.local 不太一样,systemd 兼容的 rc.local 是在 network.target 即网络相关服务启动完成之后就启动的,这意味着 rc.local 可能在开机启动过程中较早的阶段就开始运行。

如果想要将命令加入到 /etc/rc.local 中实现开机自启动,直接写入该文件,并设置该文件可执行权限即可。

echo -e '#!/bin/bash\ndate +"%F %T" >/tmp/a.log' >>/etc/rc.d/rc.local
chmod +x /etc/rc.d/rc.local

11.3.6、systemd 运行级别

在 CentOS 6 及之前的版本中有运行级别的概念,Systemd 系统内没有直接定义运行级别的概念,但是通过 Target Unit 兼容模拟了运行级别。

可以查看 /usr/lib/systemd/system/ 下的一些 target 文件。为了节省篇幅,下面我列出了部分 target:

[root@arm64v8 ~]# ls -l /usr/lib/systemd/system/*.target | grep -o '/.*'

# /usr/lib/systemd/system/下定义的默认运行级别:graphical
/usr/lib/systemd/system/default.target -> graphical.target

# 紧急模式、救援模式、多用户模式和图形界面模式
/usr/lib/systemd/system/emergency.target
/usr/lib/systemd/system/rescue.target
/usr/lib/systemd/system/multi-user.target
/usr/lib/systemd/system/graphical.target

# 关机和重启相关操作
/usr/lib/systemd/system/ctrl-alt-del.target -> reboot.target
/usr/lib/systemd/system/shutdown.target
/usr/lib/systemd/system/poweroff.target
/usr/lib/systemd/system/reboot.target
/usr/lib/systemd/system/halt.target

# 运行级别0-6,注意多用户模式为multi-user.target
/usr/lib/systemd/system/runlevel0.target -> poweroff.target
/usr/lib/systemd/system/runlevel1.target -> rescue.target
/usr/lib/systemd/system/runlevel2.target -> multi-user.target
/usr/lib/systemd/system/runlevel3.target -> multi-user.target
/usr/lib/systemd/system/runlevel4.target -> multi-user.target
/usr/lib/systemd/system/runlevel5.target -> graphical.target
/usr/lib/systemd/system/runlevel6.target -> reboot.target
[root@arm64v8 ~]#

注意上面有一个 /usr/lib/systemd/system/default.target 指向 graphical.target,但它不一定就是默认运行级别,因为有可能 /etc/systemd/system 下也有一个 default.target。

[root@arm64v8 ~]# readlink /etc/systemd/system/default.target
/lib/systemd/system/multi-user.target
[root@arm64v8 ~]#
  • 因为 /etc/systemd/system 下的 unit 都是开机时加载的,所以 优先级 更高。这就意味着上面的示例默认运行级别为 multi-user.target,而非 graphical.target。

target 是如何模拟运行级别的呢?理解了运行级别的本质含义后,就很容易理解了。所谓运行级别,无非是定义几种系统的运行模式,在不同运行模式下,启动不同服务或执行不同程序。比如图形界面下会运行图形界面服务。

而 target 的主要作用是对服务进行分组、归类。所以,只需要定义几个代表不同运行级别的 target,并在不同的 target 中放入不同的服务程序即可(除了服务程序还可以包含其它的 Unit)。

target 又是如何对服务进行分组、归类的呢?作为初步了解,可在 /etc/systemd/system 中寻找答案。在此目录下,有一些 *.target.wants 目录,该目录定义了该 target 中包含了哪些 Unit,systemd 会在处理到对应 target 时会寻找 wants 后缀的目录,并加载启动该目录下的所有 Unit,这就是 target 对服务(及其它 Unit) 分组的方式。

[root@arm64v8 ~]# ls -1F /etc/systemd/system/
basic.target.wants/
dbus-org.fedoraproject.FirewallD1.service@
default.target@
default.target.wants/
getty.target.wants/
local-fs.target.wants/
multi-user.target.wants/
network-online.target.wants/
sshd.service.d/
sysinit.target.wants/
system-update.target.wants/
[root@arm64v8 ~]#

# 系统环境初始化相关服务
[root@arm64v8 ~]# ls -1 /etc/systemd/system/sysinit.target.wants/
rhel-autorelabel.service
rhel-domainname.service
rhel-import-state.service
rhel-loadmodules.service
[root@arm64v8 ~]#

# 多用户模式下开机自启动的服务
[root@arm64v8 ~]# ls -1 /etc/systemd/system/multi-user.target.wants/
auditd.service
chronyd.service
crond.service
firewalld.service
irqbalance.service
kdump.service
nginx@server.service
postfix.service
remote-fs.target
rhel-configure.service
rsyslog.service
sshd.service
tuned.service
[root@arm64v8 ~]#

之所以有这些 wants 目录,并且其中有一些 Unit 文件,是因为在 Service 配置文件(或其它 Unit) 中的 [Install] 段落使用了 WantedBy 指令。例如:

[root@arm64v8 ~]# cat /usr/lib/systemd/system/sshd.service
[Unit]
......

[Service]
......

[Install]
WantedBy=multi-user.target  # 关键句
[root@arm64v8 ~]#

当使用 systemctl enable Unit_Name 让 Unit_Name 开机自启动时,会寻找该 [Install] 中的 WantedBy 和 RequiredBy,并在对应的 /etc/systemd/system/xxx.target.wants 或 /etc/systemd/system/xxx.target.requires 目录下创建软链接。

如果 Service 配置文件中没有定义 WantedBy 和 RequiredBy,则 systemctl enable 操作不会有任何效果。

此外,可以在 target 配置文件内部使用 Wants、Requires 等表示依赖含义的指令来定义该 target 依赖哪些 Unit。

[root@arm64v8 ~]# cat /usr/lib/systemd/system/sysinit.target
[Unit]
Description=System Initialization
Documentation=man:systemd.special(7)
Conflicts=emergency.service emergency.target
Wants=local-fs.target swap.target           # 关键句
After=local-fs.target swap.target emergency.service emergency.target
[root@arm64v8 ~]#

.target 文件中 Wants 指令定义的更符合依赖的含义,而 .target.wants 目录更倾向于表明该 target 中归类了哪些要运行的服务。

比如负责系统环境初始化的 sysinit.target,其中的 Wants 指令定义了必须先运行且成功运行文件系统相关任务(local-fs.target 和 swap.target) 后才运行 sysinit.target,也就是开始启动 .target.wants 目录下的 Unit。

执行 systemctl list-units --type target 可以查看系统当前已经加载的所有 target,包括那些开机自启动过程中启动的。

[root@arm64v8 ~]# systemctl list-units --type target 
UNIT                  LOAD   ACTIVE SUB    DESCRIPTION
basic.target          loaded active active Basic System
c.target              loaded active active c.service, consists of a.service and b.service
cryptsetup.target     loaded active active Local Encrypted Volumes
getty.target          loaded active active Login Prompts
local-fs-pre.target   loaded active active Local File Systems (Pre)
local-fs.target       loaded active active Local File Systems
multi-user.target     loaded active active Multi-User System
network-online.target loaded active active Network is Online
network-pre.target    loaded active active Network (Pre)
network.target        loaded active active Network
paths.target          loaded active active Paths
remote-fs.target      loaded active active Remote File Systems
slices.target         loaded active active Slices
sockets.target        loaded active active Sockets
swap.target           loaded active active Swap
sysinit.target        loaded active active System Initialization
timers.target         loaded active active Timers

LOAD   = Reflects whether the unit definition was properly loaded.
ACTIVE = The high-level unit activation state, i.e. generalization of SUB.
SUB    = The low-level unit activation state, values depend on unit type.

17 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.
[root@arm64v8 ~]#

除了上面展示的 target,在 /usr/lib/systemd/system 目录下还有很多 target。而且,只要用户想要对一类 Unit 进行分组归类,那么也可以自己定义 target。

但需要明确的是,target 可分为两类:

  1. 可直接切换的 target(模拟运行级别)
  2. 不可直接切换的 target

切换是什么意思?比如从当前的运行级别 3 切换到运行级别 5,将会启动运行级别 5 上的所有程序以及依赖程序,并停止当前已启动但运行级别 5 不需要的服务程序。这就是运行级别的切换,只是停止一些服务(或程序)、并启动另外一些服务而已。

切换 target 也一样,比如切换到 graphical.target 时,会启动目标 graphical.target 需要的所有服务,并停止当前已运行但目标 target 不需要的服务。

切换 target 的方式如下:

# 切换到对应的target
systemctl isolate Target_Name
# 如:
systemctl isolate default.target  # 切换到默认运行级别
systemctl isolate rescue.target   # 切换到救援模式

# 还支持如下命令
systemctl default
systemctl resuce
systemctl emergency
systemctl halt
systemctl poweroff
systemctl reboot

可查看或设置默认的运行级别:

systemctl get-default
systemctl set-default Target_Name

设置默认运行级别,实际上是创建 /etc/systemd/system/default.target 指向对应 target 配置文件的软链接。

比如:

[root@arm64v8 ~]# systemctl set-default multi-user.target 
Removed /etc/systemd/system/default.target.
Created symlink /etc/systemd/system/default.target → /usr/lib/systemd/system/multi-user.target.
[root@arm64v8 ~]#

target 是否可直接切换,取决于 target 配置文件中是否定义了 AllowIsolate=yes 指令。比如 multi-user.target 是模拟运行级别的 target,肯定允许直接切换,而 network.target 定义的是网络启动任务,肯定不可以直接切换。

[root@arm64v8 ~]# systemctl cat multi-user.target 
[Unit]
Description=Multi-User System
Documentation=man:systemd.special(7)
Requires=basic.target
Conflicts=rescue.service rescue.target
After=basic.target rescue.service rescue.target
AllowIsolate=yes
[root@arm64v8 ~]#

[root@arm64v8 ~]# systemctl show -p AllowIsolate network.target
AllowIsolate=no
[root@arm64v8 ~]#

11.3.7、systemd timer 定时任务

11.3.7.1、Cron 和 Systemd Timer

Linux 环境下,cron 是使用最广泛的定时任务工具,但它有一些不方便的地方。比如它默认:

  • 只支持分钟级别精度的定时任务
  • 定时规则太死板
  • 当调度到本次任务时,如果上次调度的任务仍在执行,无法阻止本次任务重复执行(需结合 flock)
  • 无法对定时任务可能消耗的大量资源做出限制
  • 不支持只执行一次的定时点的计划任务
  • 日志不直观,不方便调试任务

因为 cron 不原生支持以上功能,所以当有以上相关需求时,只能在要调度的命令层次上寻找解决方案。

systemd 系统中包含了 timer 计时器组件,timer 可以完全替代 cron + at,它具有以下特性:

  • 可精确到微妙级别,其支持的时间单位包括:
    • us(微秒)、ms(毫秒)、s(秒)、m(分)、h(时)、d(日)、w(周)
    • 类似 cron 定义时间的方式(某年某月某日某时某分某秒以及时间段)
  • 可对定时任务做资源限制
  • 可替代 cron 和 at 工具,且支持比 cron 更加灵活丰富的定时规则
  • 不会重复执行定时任务
    • 如果触发定时任务时发现上次触发的任务还未执行完,那么本次触发的任务不会执行
    • 而且 systemd 启动服务的操作具有幂等性,如果服务正在运行,启动操作将不做任何事,所以,甚至可以疯狂到每秒或每几秒启动一次服务,免去判断进程是否存在的过程
  • 集成到 journal 日志,方便调试任务,方便查看任务调度情况

11.3.7.2、systemd timer 示例:每 3 秒运行一次

使用 systemd timer 定时任务时,需要同时编写两个文件:

  • 编写一个以 .timer 为后缀的 Systemd Unit,该文件描述定时任务如何定时
  • 编写一个以 .service 为后缀的 Systemd Service Unit,该文件描述定时任务要执行的操作

这两个文件名称通常保持一致(除了后缀部分),它们可以放在:

  • /etc/systemd/system 目录下
  • /usr/lib/systemd/system 目录下
  • ~/.config/systemd/user 目录下,表示定义用户定时任务,用户登录后才触发

例如:

/usr/lib/systemd/system/foo.service
/usr/lib/systemd/system/foo.timer

/etc/systemd/system/foo.service
/etc/systemd/system/foo.timer

~/.config/systemd/user/foo.timer
~/.config/systemd/user/foo.service

假设定义一个每 3 秒执行一次的任务,该任务用于检测页面是否正常,对应命令为 curl -s -o /dev/null -w '%{http_code}' 'https://github.com',其结果为访问页面时响应的 HTTP 状态码。

先编写对应服务配置文件:

[root@arm64v8 ~]# cat /usr/lib/systemd/system/curl_github.service
[Unit]
Description = Written by Brinnatt@gmail.com for testing timer

[Service]
ExecStart = /usr/bin/curl -s -o /dev/null -w '%{http_code}' 'https://github.com'
[root@arm64v8 ~]#

因为命令每次调用都只执行一次且快速退出,所以 Service 中使用了默认的 Type=simple。当然,也可以使用 Type=oneshot。

再编写定时器配置文件:

[root@arm64v8 ~]# cat /usr/lib/systemd/system/curl_github.timer
[Unit]
Description = Written by Brinnatt@gmail.com for testing timer, curl per 3s

[Timer]
OnActiveSec = 1s
OnUnitInactiveSec = 3s
AccuracySec = 1us
RandomizedDelaySec = 0

[Install]
WantedBy = timers.target
[root@arm64v8 ~]#

再执行如下命令即可让定时器生效:

[root@arm64v8 ~]# systemctl daemon-reload 
[root@arm64v8 ~]# systemctl start curl_github.timer  # 启动定时器

显然,还支持如下命令来管理定时器:

systemctl status xxx.timer
systemctl stop xxx.timer
systemctl restart xxx.timer

# 和WantedBy的值有关,若WantedBy=timers.target,则本命令多余
systemctl enable xxx.timer

回头来分析一下定时器配置文件中涉及到的指令。

首先是该文件 [Install] 段中的最后一行 WantedBy=timers.target,它表示在开机时会自动启动该定时器,之所以会开机自动执行这些 timers 定时计划,是因为在 basic.target 中定义了 timer.target 依赖。

[root@arm64v8 ~]# systemctl list-dependencies --reverse timers.target | head -2
timers.target
● └─basic.target
[root@arm64v8 ~]#

再看 Timer 段中定义定时器属性的指令。

  • OnActiveSec 表示从该定时器启动(即systemctl start xxx.timer) 之后,多长时间触发定时器对应的任务,即执行对应的 Service 服务。本例是启动定时器后 1 秒,开始第一次执行任务单元 curl_github.service。
  • OnUnitInactiveSec 表示从上一次任务单元退出后,多长时间再次触发定时器对应的任务。比如在本例中,表示的含义是每次 curl_github.service 执行完成(即页面检测完成后退出)后 3 秒,再次触发该任务。
  • 剩余两个指令 AccuracySec 和 RandomizedDelaySec,稍后再详细解释。因为在解释它们之前,需要学会观察定时任务的执行情况。

11.3.7.3、定时任务的执行时间点

使用 systemctl list-timers 可以列出当前已经生效的定时器(即如果不停止它,则迟早会触发对应的定时任务)。它会按照下次要执行的时间点先后进行排序,最快要执行的任务在最前面。

[root@arm64v8 ~]# systemctl list-timers --no-pager
NEXT                         LEFT          LAST                         PASSED       UNIT                         ACTIVATES
Fri 2021-08-06 00:36:40 EDT  1min 44s ago  Fri 2021-08-06 00:36:40 EDT  1min 44s ago curl_github.timer            curl_github.service
Fri 2021-08-06 02:37:58 EDT  1h 59min left Thu 2021-08-05 02:37:58 EDT  22h ago      systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
n/a                          n/a           Tue 2021-08-03 09:58:56 EDT  2 days ago   systemd-readahead-done.timer systemd-readahead-done.service

3 timers listed.
Pass --all to see loaded but inactive timers, too.
[root@arm64v8 ~]#
  • NEXT 表示下一次要触发定时任务的时间点
  • LEFT 表示现在距离下次执行任务还剩多长时间(已经确定了下一次执行的时间点),或者显示最近一次执行任务已经过去了多长时间(还不确定下一次执行的时间点),稍后解释了 AccuracySec 和 RandomizedDelaySec 就知道为什么会有这两种表示方式
  • LAST 表示上次触发定时任务的时间点
  • PASSED 表示距离上一次执行任务已经过去多久
  • UNIT 表示是哪个定时器
  • ACTIVATES 表示该定时器所触发的任务

虽然上面的含义都比较清晰,但是想要理解透彻,还真不容易。

不过,还有其它观察定时任务执行情况的方式。由于 systemd service 默认集成了 journald 日志系统,命令的标准输出和标准错误都会输出到 journal 日志中。

比如,可以使用 systemctl status xxx.service 观察定时器对应任务的执行状况,即每次执行任务的时间点以及定时任务执行过程中的标准输出、标准错误信息。

[root@arm64v8 ~]# systemctl status curl_github.service  # 注意是.service不是.timer
● curl_github.service - Written by Brinnatt@gmail.com for testing timer
   Loaded: loaded (/usr/lib/systemd/system/curl_github.service; static; vendor preset: disabled)
   Active: active (running) since Fri 2021-08-06 12:42:42 CST; 1min 32s ago
 Main PID: 12172 (curl)
   CGroup: /system.slice/curl_github.service
           └─12172 /usr/bin/curl -s -o /dev/null -w %{http_code} https://github.com

Aug 06 12:42:42 arm64v8 systemd[1]: Started Written by Brinnatt@gmail.com for testing timer.
Aug 06 12:42:42 arm64v8 systemd[1]: Starting Written by Brinnatt@gmail.com for testing ......
Hint: Some lines were ellipsized, use -l to show in full.
[root@arm64v8 ~]#

还可以使用 journalctl 工具来查看定时任务的日志信息:

# 查看指定服务的所有journal日志信息
# xxx.service是定时任务的名称
journalctl -u xxx.service

# 实时监控尾部日志,类似tail -f
journalctl -f -u xxx.service

# 显示指定时间段内的日志
# --since:从指定时间点内开始的日志
# --until:到指定时间点为止的日志
journalctl -u xxx.service --since="2021-08-06 12:56:23"
journalctl -u xxx.service --since="60s ago"
journalctl -u xxx.service --since="1min ago"
journalctl -u curl_github.service --since="-120s"

例如:

[root@arm64v8 ~]# journalctl -u curl_github.service --since="-30s"
-- Logs begin at Tue 2021-08-03 21:58:10 CST, end at Fri 2021-08-06 12:59:36 CST. --
Aug 06 12:59:08 arm64v8 systemd[1]: Started Written by Brinnatt@gmail.com for testing timer.
Aug 06 12:59:08 arm64v8 systemd[1]: Starting Written by Brinnatt@gmail.com for testing timer.
Aug 06 12:59:14 arm64v8 curl[12355]: 200
Aug 06 12:59:17 arm64v8 systemd[1]: Started Written by Brinnatt@gmail.com for testing timer.
Aug 06 12:59:17 arm64v8 systemd[1]: Starting Written by Brinnatt@gmail.com for testing timer.
Aug 06 12:59:25 arm64v8 curl[12359]: 200
Aug 06 12:59:28 arm64v8 systemd[1]: Started Written by Brinnatt@gmail.com for testing timer.
Aug 06 12:59:28 arm64v8 systemd[1]: Starting Written by Brinnatt@gmail.com for testing timer.
Aug 06 12:59:36 arm64v8 curl[12363]: 200

从结果可以看出,在 Aug 06 12:59:08、Aug 06 12:59:17 都执行了 page_test 任务。

11.3.7.4、AccuracySec 和 RandomizedDelaySec

AccuracySec 表示任务推迟执行的延迟范围,即从每次指定要执行任务的精确时间点到延迟时间段内的一个随机时间点启动任务。使用这种延迟,主要是为了避免 systemd 频繁触发定时器事件而频繁唤醒 CPU,从而让一定时间段内附近的定时任务可以集中在这个时间段内启动。

  • 比如说有多个定时任务都设置了每分钟执行一次,那么在每分钟时间段内,systemd 可能在这一分钟的第 10 秒执行一个定时任务,第 11 秒执行下一个定时任务,第 12 秒再执行下一个,依次类推。
  • 所以,AccuracySec 的目的便是将一定范围内的定时器事件合并到这个时间段内的同一个时间点上,然后一次性触发所有在范围内的定时器任务。

例如:

# 定时器启动后,再过10分钟第一次触发定时任务
OnActiveSec=10m
# 每次执行完任务后,再过15分钟后再次触发定时任务
OnUnitInactiveSec=15m
# 触发事件后,允许推迟0-10分钟再执行被触发的任务
AccuracySec=10m

所以,以上指令的效果是:

  • 启动定时器后的 10m - 20m 内的任一时间点触发第一次定时任务
  • 之后每隔 15m - 25m 再次触发定时任务

AccuracySec 的默认值为 1 分钟,所以如果不定义 AccuracySec 的话,即使用户期待的是每秒触发一次定时任务,但事实却是会在 1s - 61s 时间段内的一个随机时间点触发一次定时任务。

但是,触发定时任务的时间点并不表示这是执行任务的时间点。触发了定时任务,还需要根据 RandomizedDelaySec 的值来决定何时执行定时任务。

RandomizedDelaySec 指定触发定时任务后还需延迟一个指定范围内的随机时长才执行任务。该指令默认值为 0,表示触发后立即执行任务。

使用 RandomizedDelaySec,主要是为了在一个时间范围内分散大量被同时触发的定时任务,从而避免这些定时任务集中在同一时间点执行而 CPU 争抢。

可见,AccuracySec 和 RandomizedDelaySec 的目的是相反的:

  • 前者让指定范围内的定时器集中在同一个时间点一次性触发它们的定时任务
  • 后者让触发的、将要被执行的任务均匀分散在一个时间段范围内

根据以上描述,如果用户想要让定时任务非常精确度地执行,需要将它们设置的足够小。例如:

AccuracySec = 1ms        # 定时器到点就触发定时任务
RandomizedDelaySec = 0   # 定时任务一触发就立刻执行任务

11.3.7.5、systemd timer 支持的单调定时规则

systemd timer 还支持几种其它的定时器规则。

定时器指令 含义
OnBootSec 从开机启动后,即从内核开始运行算起,多长时间触发定时器对应任务
OnStartupSec 从 systemd 启动后,即内核启动 init 进程算起,多长时间触发定时器对应任务
OnActiveSec 从该定时器启动后,多长时间触发定时器对应的任务
OnUnitInactiveSec 从上次任务单元退出后,多长时间再次触发定时器对应的任务
OnUnitActiveSec 从上次触发的任务开始执行(状态达到 active) 算起,多长时间再次触发定时器对应的任务
(1) 当 Service 文件中 Type=oneshot,这类任务不会出现 active 状态,除非配置了 RemainAfterExit 指令(参考 man systemd.service)
(2) 这个定时器用的不如 OnUnitInactiveSec 多,因为这个定时器是以启动时间为基准的,有可能下次触发任务时,上次任务还没有执行完成,systemd 会忽略下次任务

其中 OnBootSec 和 OnStartupSec 比较特殊,因为定时器自身的启动比这两个时间点要晚,如果定时器配置文件中以这两个指令为定时任务的触发基准,可能会出现超期现象。

  • 比如某定时器设置 OnBootSec=1s,但如果从启动内核到启动定时器已经过了 2s,那么这个定时任务就超期了。
  • 好在,systemd 会对这两个特殊的指令特殊对待,如果这类定时任务超期了,将立即执行定时任务实现补救。

但对其它三个指令定义的定时器,超期了就超期了,不会再尝试去补救。也就是说,即使过了有效期,这两类定时任务还是有效的,而其它定时任务则失效。

事实上,这几个定时器指令都是单调定时器,即:这些任务的触发时机,总是以某个时间点为基准单调增加的。

11.3.7.6、更为灵活的定时规则:OnCalendar

cron 定时任务支持 * * * * * 来定义定时任务,这 5 个位置分别表示分 时 日 月 周。

前面已经介绍的 systemd timer 的定时规则已经能够实现只执行一次和每隔多久执行一次的定时规则。下面要介绍的 OnCalendar 基于日历的定时规则完全可以胜任 cron 的定时规则。

例如:

OnCalendar = Thu,Fri 2012-*-1,5 11:12:13
  • 这表示 2012 年每个月的 1 或 5 号的 11 点 12 分 13 秒,同时要求是周四或周五。

systemd timer 可识别的时间单位包括以下几种:

  • 微秒级单位:usec, us, µs
  • 毫秒级单位:msec, ms
  • 秒级单位(省略单位时的默认单位):seconds, second, sec, s
  • 分钟级单位:minutes, minute, min, m
  • 小时级单位:hours, hour, hr, h
  • 天的单位:days, day, d
  • 周的单位:weeks, week, w
    • 使用周单位时,必须使用三字母表示法或英文全称,如 Fri、Sun、Monday
  • 月的单位:months, month, M
  • 年的单位(一年以365.25天算):years, year, y

多个时间单位可结合使用,且时间的出现顺序无关。

例如下面的时间单位都是有效的:

2 h              --> 2小时
2hours           --> 2小时
48hr             --> 48小时
1y 12month       --> 1年12个月,即2年
55s500ms         --> 55秒+500毫秒
300ms20s 5day    --> 5天+20秒+300毫秒,顺序无关

在定时器里,还会经常用到表示某年某月某日、某时某分某秒的时间戳格式。systemd 内部的标准时间戳格式为:

Fri 2012-11-23 11:12:13
Fri 2012-11-23 11:12:13 UTC

涉及到时区格式,可以设计得更复杂,但实际上没有必要,要么不带时区即默认本地,要么只带 UTC。

最前面的周几可以省略,但如果不省略,则必须只能使用三字母表示法或英文全称,即合理的周几符号包括:

Monday      Mon
Tuesday     Tue
Wednesday   Wed
Thursday    Thu
Friday      Fri
Saturday    Sat
Sunday      Sun

年-月-日时:分:秒 二者可省其一,但不可全省。若省前者,则表示使用当前日期,若省后者则表示使用00:00:00

时:分:秒 可以省略 :秒,相当于使用 :00

年-月-日 中的 可以省略为2位数字表示,相当于 20xx,但强烈建议不要使用这种方式。

如果指定的 星期年-月-日(即使此部分已被省略) 与实际不相符,那么该时间戳无效。

还可以使用一些时间戳关键字:now, today, yesterday, tomorrow

还可以使用一些相对时间表示法:时长加上 + 前缀或者 ' left' 后缀(注意有空格),表示以此时间为基准向未来前进指定的时长,时长加上 - 前缀或者 ' ago' 后缀(注意有空格),表示以此时间为基准向过去倒退指定的时长。

最后,时长加上 @ 前缀表示相对于UNIX时间原点(1970-01-01 00:00:00 UTC) 之后多长时间。

以下都是有效时间:

# 假设当前时间为2012-11-23 18:15:22,时区为UTC+8,例如“TZ=:Asia/Shanghai”):
  Fri 2012-11-23 11:12:13 → Fri 2012-11-23 11:12:13
      2012-11-23 11:12:13 → Fri 2012-11-23 11:12:13
  2012-11-23 11:12:13 UTC → Fri 2012-11-23 19:12:13
               2012-11-23 → Fri 2012-11-23 00:00:00
                 12-11-23 → Fri 2012-11-23 00:00:00
                 11:12:13 → Fri 2012-11-23 11:12:13
                    11:12 → Fri 2012-11-23 11:12:00
                      now → Fri 2012-11-23 18:15:22
                    today → Fri 2012-11-23 00:00:00
                today UTC → Fri 2012-11-23 16:00:00
                yesterday → Fri 2012-11-22 00:00:00
                 tomorrow → Fri 2012-11-24 00:00:00
tomorrow Pacific/Auckland → Thu 2012-11-23 19:00:00
                 +3h30min → Fri 2012-11-23 21:45:22
                      -5s → Fri 2012-11-23 18:15:17
                11min ago → Fri 2012-11-23 18:04:22
              @1395716396 → Tue 2014-03-25 03:59:56

OnCalendar 指令使用基于日历的定时规则,基于日历的格式是对 systemd 标准时间戳的扩展:在标准时间戳的基础上,可以使用一些额外的语法。这些额外的语法包括:

  • 可使用 , 列出离散值,可使用 .. 表示一个范围

  • 对于 年-月-日时:分:秒 这两部分的每个子部分:

    • 可使用 * 表示匹配任意值
    • 可使用 /N(N是整数) 后缀表示每隔 N 个单位,特别地 /1 表示每次增加一个单位
  • 对于 年-月-日 部分,可使用 月~日 替代 月-日 表示一个月中的倒数第 N 天

  • 对于 ,可使用小数表示更高精度,最高精度为 6 位小数

  • 以下特殊表达式可以作为较长的规范化形式的简写:

      minutely → *-*-* *:*:00
        hourly → *-*-* *:00:00
         daily → *-*-* 00:00:00
       monthly → *-*-01 00:00:00
        weekly → Mon *-*-* 00:00:00
        yearly → *-01-01 00:00:00
     quarterly → *-01,04,07,10-01 00:00:00
    semiannually → *-01,07-01 00:00:00
    • minutely:每分钟
    • hourly:每小时
    • daily:每天
    • monthly:每月
    • weekly:每周
    • yearly:每年
    • quarterly:每季度
    • semiannually:每半年

有效时间戳及其规范化形式的示例:

  Sat,Thu,Mon..Wed,Sat..Sun → Mon..Thu,Sat,Sun *-*-* 00:00:00
      Mon,Sun 12-*-* 2,1:23 → Mon,Sun 2012-*-* 01,02:23:00
                    Wed *-1 → Wed *-*-01 00:00:00
           Wed..Wed,Wed *-1 → Wed *-*-01 00:00:00
                 Wed, 17:48 → Wed *-*-* 17:48:00
Wed..Sat,Tue 12-10-15 1:2:3 → Tue..Sat 2012-10-15 01:02:03
                *-*-7 0:0:0 → *-*-07 00:00:00
                      10-15 → *-10-15 00:00:00
        monday *-12-* 17:00 → Mon *-12-* 17:00:00
  Mon,Fri *-*-3,1,2 *:30:45 → Mon,Fri *-*-01,02,03 *:30:45
       12,14,13,12:20,10,30 → *-*-* 12,13,14:10,20,30:00
            12..14:10,20,30 → *-*-* 12..14:10,20,30:00
  mon,fri *-1/2-1,3 *:30:45 → Mon,Fri *-01/2-01,03 *:30:45
             03-05 08:05:40 → *-03-05 08:05:40
                   08:05:40 → *-*-* 08:05:40
                      05:40 → *-*-* 05:40:00
     Sat,Sun 12-05 08:05:40 → Sat,Sun *-12-05 08:05:40
           Sat,Sun 08:05:40 → Sat,Sun *-*-* 08:05:40
           2003-03-05 05:40 → 2003-03-05 05:40:00
 05:40:23.4200004/3.1700005 → *-*-* 05:40:23.420000/3.170001
             2003-02..04-05 → 2003-02..04-05 00:00:00
       2003-03-05 05:40 UTC → 2003-03-05 05:40:00 UTC
                 2003-03-05 → 2003-03-05 00:00:00
                      03-05 → *-03-05 00:00:00
                     hourly → *-*-* *:00:00
                      daily → *-*-* 00:00:00
                  daily UTC → *-*-* 00:00:00 UTC
                    monthly → *-*-01 00:00:00
                     weekly → Mon *-*-* 00:00:00
    weekly Pacific/Auckland → Mon *-*-* 00:00:00 Pacific/Auckland
                     yearly → *-01-01 00:00:00
                   annually → *-01-01 00:00:00
                      *:2/3 → *-*-* *:02/3:00

当不明确一个基于日历表示法的时间时,可使用神器 systemd-analyize calendar 命令来分析(早期 systemd 版本不支持 calendar 子命令)。

例如:

[root@kylinv10 ~]# systemd-analyze calendar Sat,Mon..Wed
  Original form: Sat,Mon..Wed               
Normalized form: Mon..Wed,Sat *-*-* 00:00:00
    Next elapse: Sat 2021-08-07 00:00:00 CST
       (in UTC): Fri 2021-08-06 16:00:00 UTC
       From now: 9h left                    
[root@kylinv10 ~]#

[root@kylinv10 ~]# systemd-analyze calendar "*-05~07/1"
  Original form: *-05~07/1                  
Normalized form: *-05~07/1 00:00:00         
    Next elapse: Wed 2022-05-25 00:00:00 CST
       (in UTC): Tue 2022-05-24 16:00:00 UTC
       From now: 9 months 17 days left      
[root@kylinv10 ~]#

[root@kylinv10 ~]# systemd-analyze calendar "Mon *-05~07/1"
  Original form: Mon *-05~07/1              
Normalized form: Mon *-05~07/1 00:00:00     
    Next elapse: Mon 2022-05-30 00:00:00 CST
       (in UTC): Sun 2022-05-29 16:00:00 UTC
       From now: 9 months 22 days left      
[root@kylinv10 ~]#

11.3.7.7、Unit 和 Persistent

[Timer] 段中还可以使用 Unit 指令和 Persistent 指令。

  • Unit=xxx.service:默认情况下,a.timer 对应要执行的任务文件是 a.service,使用 Unit 指令可以明确指定触发定时任务事件时要执行的文件
  • Persistent=yes/no:只在使用了 OnCalendar 时有效,默认值 no。设置 yes 时,会将上次执行任务的时间点保存在磁盘上,使得定时器再次被启动时,可以立即判断是否要执行丢失的任务
    • 以空文件方式保存,以该空文件的 atime/mtime/ctime 信息记录执行任务的时间点
    • 文件保存路径:/var/lib/systemd/timers,或 ~/.local/share/systemd/(用户级定时器保存路径)
    • 可删除这些时间戳文件,使得不会立即触发丢失的任务

比如下面的定时任务表示每天凌晨执行任务。

OnCalendar = 00:00
Persistent = yes

因为使用了 Persistent,所以每次执行完任务后都会将本次执行的时间点记录在磁盘文件中,如果在 23:59:50 时遇到一次重启耽搁 1 分钟,那么在重启成功后会立即执行该任务。

如果 Persistent=no,则在重启后不会立即执行任务,而是等到下一个凌晨才执行任务。

11.3.7.8、systemd timer 用户级定时任务

用户级定时器在用户登录后开始启动,用户退出时(所有使用该用户启动的终端的会话都断开) 停止。

用户级定时器要求将 .timer 和对应的 .service 定义在 ~/.config/systemd/user/ 目录下。如果使用了 OnCalendar 和 Persisten 指令,时间戳文件保存在 ~/.local/share/systemd/ 目录下。

例如:

[root@kylinv10 ~]# mkdir -p ~/.config/systemd/user
[root@kylinv10 ~]# cat ~/.config/systemd/user/test.service
[Unit]
Description = current time when task is been executed

[Service]
ExecStart = /bin/bash -c '/usr/bin/date +"%%T" >>/tmp/a.log'

[root@kylinv10 ~]# 
[root@kylinv10 ~]# cat ~/.config/systemd/user/test.timer
[Unit]
Description = user login timer

[Timer]
AccuracySec = 1ms
RandomizedDelaySec = 0
OnCalendar = *:*:*

[Install]
WantedBy = timer.target
[root@kylinv10 ~]#

再启动用户定时器:

[root@kylinv10 ~]# systemctl --user daemon-reload 
[root@kylinv10 ~]# systemctl --user start test.timer 
[root@kylinv10 ~]# systemctl --user list-timers 
NEXT                         LEFT       LAST                         PASSED    UNIT       AC>
Fri 2021-08-06 15:01:57 CST  510ms left Fri 2021-08-06 15:01:56 CST  488ms ago test.timer te>

1 timers listed.
Pass --all to see loaded but inactive timers, too.

查看定时器触发的任务状态:

[root@kylinv10 ~]# systemctl --user status test.service

停止定时器:

[root@kylinv10 ~]# systemctl --user stop test.timer

11.3.7.9、systemd 临时定时任务

systemd-run 命令支持定时器类选项,所以通过 systemd-run 可以启动临时的定时任务。systemd-run 支持的定时器选项有:

  • –on-boot
  • –on-startup
  • –on-unit-active
  • –on-unit-inactive
  • –on-active
  • –on-calendar

此外还支持 --timer-property 选项定义 [Timer] 中的指令。

例如:定时 30 秒,执行一次任务后自动退出

# 执行完该命令后,再过 30 秒执行 sed,精确触发
[root@kylinv10 ~]# cat /tmp/testfile 
#
[root@kylinv10 ~]# systemd-run --on-active=30 --timer-property=AccuracySec=100ms sed -i "/^#$/a \# hello world" /tmp/testfile
Running timer as unit: run-r8bfd007edb734b399027a2b1441a09a7.timer
Will run service as unit: run-r8bfd007edb734b399027a2b1441a09a7.service
[root@kylinv10 ~]#

# 30秒后
[root@kylinv10 ~]# cat /tmp/testfile
#
# hello world

例如:每隔 2 秒,执行一次任务

[root@kylinv10 ~]# systemd-run --on-calendar="*:*:1/2" --timer-property="AccuracySec=1us" --timer-property="RandomizedDelaySec=0" sed -i "/^#$/a \# hello world" /tmp/testfile
Running timer as unit: run-r0235cfa3d8b34083ab5c5f68f5f7bf29.timer
Will run service as unit: run-r0235cfa3d8b34083ab5c5f68f5f7bf29.service
[root@kylinv10 ~]# 
[root@kylinv10 ~]# wc -l /tmp/testfile
3 /tmp/testfile
[root@kylinv10 ~]# wc -l /tmp/testfile
4 /tmp/testfile
[root@kylinv10 ~]# wc -l /tmp/testfile
5 /tmp/testfile
[root@kylinv10 ~]#

执行完成后,会报告所执行的 timer unit 和 service unit,可通过这个值来查看状态或管理它们。例如,停止这个临时定时器:

[root@kylinv10 ~]# systemctl stop run-r0235cfa3d8b34083ab5c5f68f5f7bf29.timer

注意:上面两个例子使用 sed 是为了达到实验效果,千万要小心,使用 systemd-run 命令生成的 service 默认是使用 Type=simple 类型,如果想要自定义类型请使用 -u 选项指定 service 文件。

11.3.7.10、限制定时任务的资源使用量

有些定时任务可能会消耗大量资源,比如执行 rsync 的定时任务、执行数据库备份的定时任务,等等,它们可能会消耗网络带宽,消耗 IO 带宽,消耗 CPU 等资源。

想要控制这些定时任务的资源使用量也非常简单,因为真正执行任务的是 .service,而 Service 配置文件中可以轻松地配置一些资源控制指令或直接使用 Slice 定义的 CGroup。这些资源控制类的指令可参考 man systemd.resource-control。

例如,直接在 [Service] 中定义资源控制指令:

[Service]
Type=simple
MemoryLimit=20M
ExecStart=/usr/bin/backup.sh

又或者让 Service 使用定义好的 Slice:

[Service]
ExecStart=/usr/bin/backup.sh
Slice=backup.slice

其中 backup.slice 的内容为:

[root@kylinv10 ~]# cat /usr/lib/systemd/system/backup.slice
[Unit]
Description=Limited resources Slice
DefaultDependencies=no
Before=slices.target

[Slice]
CPUQuota=50%
MemoryLimit=20M
标签云