Linux systemd 用法详解

注: 本文为 systemd 系列相关合辑。


在 Linux 中确定运行的是哪种初始化系统

作者: Arindam 译者: LCTT geekpi | 2022-11-15 18:30

可以通过以下方式确定 Linux 发行版中是否正在运行 systemd 或其它初始化系统。

首个进程在你启动 Linux 发行版时开始运行,它称为初始化进程 init(初始化initialization的缩写)。它的进程标识符为 1(即 pid=1)。基于 Unix 的系统中的所有进程和应用程序都是这个初始化进程的后代。

根据功能和特性,存在不同类型的初始化进程。例如,systemd、Runit、OpenRC、sysVinit 等。其中,systemd 是最流行和最现代的一种,被包括 Ubuntu 和 Fedora 在内的所有现代 Linux 发行版使用和采用。

与传统的基于 Unix 的初始化系统相比,systemd 及其性能一直存在争议。但这就是另外一个话题了。

让我们看看如何确定在 Linux 发行版中运行的是 systemd 还是其它初始化系统。

systemd 还是其它初始化系统?

不幸的是,没有直接的命令可以找到它。你可以从初始化进程追溯它,它基本上是到 /sbin/init 的符号链接,即 pid=1。

使用 strings 命令打印嵌入在二进制文件 /sbin/init 中的文本并使用以下命令搜索 init

strings /sbin/init | grep init
示例 1

在下面的输出中,它是一个运行 Debian(Peppermint OS)的 sysVinit 系统。如你所见,它清楚地显示了 init 进程名称。

strings /sbin/init | grep init

显示使用 sysVinit 而不是 systemd 的示例

显示使用 sysVinit 而不是 systemd 的示例

如果在上述同一个系统中找 systemd,那么不会有任何结果。因此,你可以得出结论,你正在运行 sysVinit 而不是 systemd。

示例 2

如果你在 systemd 系统中运行上述命令,你可以在输出的第一行轻松看到 systemd 及其版本。

strings /sbin/init | grep systemd

显示它使用 systemd 的示例

显示它使用 systemd 的示例

示例 3

你也可以尝试使用 pstree 命令打印进程树,它应该会显示第一个进程名称。它应该是 systemdinit,如下例所示。

pstree

pstree 显示使用 systemd

pstree 显示使用 systemd

pstree 显示使用 init

pstree 显示使用 init

这就好了。这样你就可以轻松找出你的发行版是使用 systemd 还是其他的。


检查 Linux 系统是否使用 systemd

作者: Sagar Sharma 译者: LCTT geekpi | 2024-01-17 08:08

不知道在使用哪个初始化系统?以下是方法。

每个主流 Linux 发行版(包括 Ubuntu、Fedora、openSUSE 和 Arch)默认都使用 systemd。但是有 许多非 systemd 发行版,例如使用轻量级 runit 来获得更好性能的 Void Linux 或主要使用 sysvinit 的 Devuan。

当你尝试遵循某些教程或文档并且其中包含特定于 systemd 或某些其他初始化服务的命令时,就会出现问题。

这时,你必须检查你的 Linux 系统使用的是 systemd 还是其他系统。

一种方法是检查 PID 为 1 的进程(毕竟,初始化系统是 Linux 系统上运行的第一个进程)。

ps 1

但它的输出可能会产生误导,因为它经常显示 /sbin/init,这只是实际的初始化进程的软链接。

如果你 跟随该符号链接,就可以获取初始化系统信息。有两种方法:

  • 使用 stat 命令
  • 使用 readlink 命令

那么让我们从第一个开始。

📋 这些方法在 6 个初始化系统中进行了测试:Systemd、OpenRC、SysVinit、Busybox、runit 和 s6。

方法 1:使用 stat 命令检查 systemd 是否正在使用

以下是如何使用 stat 命令来了解你正在使用的初始化系统:

stat /sbin/init

如果你使用的是 systemd 支持的发行版,那么它将显示以下输出:

img

但是,如果你使用 systemd 之外的其他任何东西,它将显示初始化系统的名称,但是如果你使用的是 SysVinit,它只会显示 init 而不是 sysvinit

SysVinit only displays "init" instead of sysvinit

SysVinit only displays “init” instead of sysvinit

方法 2:使用 readlink 命令检查初始化系统

与之前的方法不同,当你使用 readlink 命令时,它只会打印初始化系统的名称。

因此,如果你想知道你是否正在使用 Systemd,只需使用以下命令:

readlink /sbin/init

img

如果你使用 OpenRC 初始化访问,那么它将显示以下输出:

img

但如果你使用 SysVinit,那么它会显示以下输出:

img

我为你写的小“脚本”

另一种方法是检查 /run/systemd/system 目录是否存在。

最简单的找出方法是在终端中 使用 if-else bash 命令,它将检查你是否正在运行由 systemd 驱动的发行版:

if [ -d /run/systemd/system ]; then echo "System is running systemd"; else echo "System is not running systemd"; fi

img
我希望本指南对你有所帮助。


理解 systemd 启动时在做什么

作者: David Both 译者: LCTT YungeGuo | 2021-08-26 11:02

systemd 启动过程提供的重要线索可以在问题出现时助你一臂之力。

在本系列的第一篇文章《学着爱上 systemd》,我考察了 systemd 的功能和架构,以及围绕 systemd 作为古老的 SystemV 初始化程序和启动脚本的替代品的争论。在这第二篇文章中,我将开始探索管理 Linux 启动序列的文件和工具。我会解释 systemd 启动序列、如何更改默认的启动目标(即 SystemV 术语中的运行级别)、以及在不重启的情况下如何手动切换到不同的目标。

我还将考察两个重要的 systemd 工具。第一个 systemctl 命令是和 systemd 交互、向其发送命令的基本方式。第二个是 journalctl,用于访问 systemd 日志,后者包含了大量系统历史数据,比如内核和服务的消息(包括指示性信息和错误信息)。

务必使用一个非生产系统进行本文和后续文章中的测试和实验。你的测试系统需要安装一个 GUI 桌面(比如 Xfce、LXDE、Gnome、KDE 或其他)。

上一篇文章中我写道计划在这篇文章创建一个 systemd 单元并添加到启动序列。由于这篇文章比我预期中要长,这些内容将留到本系列的下一篇文章。

使用 systemd 探索 Linux 的启动

在观察启动序列之前,你需要做几件事情得使引导和启动序列开放可见。正常情况下,大多数发行版使用一个开机动画或者启动画面隐藏 Linux 启动和关机过程中的显示细节,在基于 Red Hat 的发行版中称作 Plymouth 引导画面。这些隐藏的消息能够向寻找信息以排除程序故障、或者只是学习启动序列的系统管理员提供大量有关系统启动和关闭的信息。你可以通过 GRUB(大统一引导加载器Grand Unified Boot Loader)配置改变这个设置。

主要的 GRUB 配置文件是 /boot/grub2/grub.cfg ,但是这个文件在更新内核版本时会被覆盖,你不会想修改它的。相反,应该修改用于改变 grub.cfg 默认设置的 /etc/default/grub 文件。

首先看一下当前未修改的 /etc/default/grub 文件的版本:

[root@testvm1 ~]# cd /etc/default ; cat grub
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="resume=/dev/mapper/fedora_testvm1-swap rd.lvm.
lv=fedora_testvm1/root rd.lvm.lv=fedora_testvm1/swap rd.lvm.lv=fedora_
testvm1/usr rhgb quiet"
GRUB_DISABLE_RECOVERY="true"
[root@testvm1 default]#

GRUB 文档 的第 6 章列出了 /etc/default/grub 文件的所有可用项,我只关注下面的部分:

  • 我将 GRUB 菜单倒计时的秒数 GRUB_TIMEOUT,从 5 改成 10,以便在倒计时达到 0 之前有更多的时间响应 GRUB 菜单。
  • GRUB_CMDLINE_LINUX 列出了引导阶段传递给内核的命令行参数,我删除了其中的最后两个参数。其中的一个参数 rhgb 代表 “红帽图形化引导Red Hat Graphical Boot”,在内核初始化阶段显示一个小小的 Fedora 图标动画,而不是显示引导阶段的信息。另一个参数 quiet,屏蔽显示记录了启动进度和发生错误的消息。系统管理员需要这些信息,因此我删除了 rhgbquiet。如果引导阶段发生了错误,屏幕上显示的信息可以指向故障的原因。

更改之后,你的 GRUB 文件将会像下面一样:

[root@testvm1 default]# cat grub
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="resume=/dev/mapper/fedora_testvm1-swap rd.lvm.
lv=fedora_testvm1/root rd.lvm.lv=fedora_testvm1/swap rd.lvm.lv=fedora_
testvm1/usr"
GRUB_DISABLE_RECOVERY="false"
[root@testvm1 default]#

grub2-mkconfig 程序使用 /etc/default/grub 文件的内容生成 grub.cfg 配置文件,从而改变一些默认的 GRUB 设置。grub2-mkconfig 输出到 STDOUT,你可以使用程序的 -o 参数指明数据流输出的文件,不过使用重定向也同样简单。执行下面的命令更新 /boot/grub2/grub.cfg 配置文件:

[root@testvm1 grub2]# grub2-mkconfig > /boot/grub2/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-4.18.9-200.fc28.x86_64
Found initrd image: /boot/initramfs-4.18.9-200.fc28.x86_64.img
Found linux image: /boot/vmlinuz-4.17.14-202.fc28.x86_64
Found initrd image: /boot/initramfs-4.17.14-202.fc28.x86_64.img
Found linux image: /boot/vmlinuz-4.16.3-301.fc28.x86_64
Found initrd image: /boot/initramfs-4.16.3-301.fc28.x86_64.img
Found linux image: /boot/vmlinuz-0-rescue-7f12524278bd40e9b10a085bc82dc504
Found initrd image: /boot/initramfs-0-rescue-7f12524278bd40e9b10a085bc82dc504.img
done
[root@testvm1 grub2]#

重新启动你的测试系统查看本来会隐藏在 Plymouth 开机动画之下的启动信息。但是如果你没有关闭开机动画,又需要查看启动信息的话又该如何操作?或者你关闭了开机动画,而消息流过的速度太快,无法阅读怎么办?(实际情况如此。)

有两个解决方案,都涉及到日志文件和 systemd 日志 —— 两个都是你的好伙伴。你可以使用 less 命令查看 /var/log/messages 文件的内容。这个文件包含引导和启动信息,以及操作系统执行正常操作时生成的信息。你也可以使用不加任何参数的 journalctl 命令查看 systemd 日志,包含基本相同的信息:

[root@testvm1 grub2]# journalctl
-- Logs begin at Sat 2020-01-11 21:48:08 EST, end at Fri 2020-04-03 08:54:30 EDT. --
Jan 11 21:48:08 f31vm.both.org kernel: Linux version 5.3.7-301.fc31.x86_64 (mockbuild@bkernel03.phx2.fedoraproject.org) (gcc version 9.2.1 20190827 (Red Hat 9.2.1-1) (GCC)) #1 SMP Mon Oct >
Jan 11 21:48:08 f31vm.both.org kernel: Command line: BOOT_IMAGE=(hd0,msdos1)/vmlinuz-5.3.7-301.fc31.x86_64 root=/dev/mapper/VG01-root ro resume=/dev/mapper/VG01-swap rd.lvm.lv=VG01/root rd>
Jan 11 21:48:08 f31vm.both.org kernel: x86/fpu: Supporting XSAVE feature 0x001: 'x87 floating point registers'
Jan 11 21:48:08 f31vm.both.org kernel: x86/fpu: Supporting XSAVE feature 0x002: 'SSE registers'
Jan 11 21:48:08 f31vm.both.org kernel: x86/fpu: Supporting XSAVE feature 0x004: 'AVX registers'
Jan 11 21:48:08 f31vm.both.org kernel: x86/fpu: xstate_offset[2]:  576, xstate_sizes[2]:  256
Jan 11 21:48:08 f31vm.both.org kernel: x86/fpu: Enabled xstate features 0x7, context size is 832 bytes, using 'standard' format.
Jan 11 21:48:08 f31vm.both.org kernel: BIOS-provided physical RAM map:
Jan 11 21:48:08 f31vm.both.org kernel: BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
Jan 11 21:48:08 f31vm.both.org kernel: BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
Jan 11 21:48:08 f31vm.both.org kernel: BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
Jan 11 21:48:08 f31vm.both.org kernel: BIOS-e820: [mem 0x0000000000100000-0x00000000dffeffff] usable
Jan 11 21:48:08 f31vm.both.org kernel: BIOS-e820: [mem 0x00000000dfff0000-0x00000000dfffffff] ACPI data
Jan 11 21:48:08 f31vm.both.org kernel: BIOS-e820: [mem 0x00000000fec00000-0x00000000fec00fff] reserved
Jan 11 21:48:08 f31vm.both.org kernel: BIOS-e820: [mem 0x00000000fee00000-0x00000000fee00fff] reserved
Jan 11 21:48:08 f31vm.both.org kernel: BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
Jan 11 21:48:08 f31vm.both.org kernel: BIOS-e820: [mem 0x0000000100000000-0x000000041fffffff] usable
Jan 11 21:48:08 f31vm.both.org kernel: NX (Execute Disable) protection: active
Jan 11 21:48:08 f31vm.both.org kernel: SMBIOS 2.5 present.
Jan 11 21:48:08 f31vm.both.org kernel: DMI: innotek GmbH VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006
Jan 11 21:48:08 f31vm.both.org kernel: Hypervisor detected: KVM
Jan 11 21:48:08 f31vm.both.org kernel: kvm-clock: Using msrs 4b564d01 and 4b564d00
Jan 11 21:48:08 f31vm.both.org kernel: kvm-clock: cpu 0, msr 30ae01001, primary cpu clock
Jan 11 21:48:08 f31vm.both.org kernel: kvm-clock: using sched offset of 8250734066 cycles
Jan 11 21:48:08 f31vm.both.org kernel: clocksource: kvm-clock: mask: 0xffffffffffffffff max_cycles: 0x1cd42e4dffb, max_idle_ns: 881590591483 ns
Jan 11 21:48:08 f31vm.both.org kernel: tsc: Detected 2807.992 MHz processor
Jan 11 21:48:08 f31vm.both.org kernel: e820: update [mem 0x00000000-0x00000fff] usable ==> reserved
Jan 11 21:48:08 f31vm.both.org kernel: e820: remove [mem 0x000a0000-0x000fffff] usable
<snip>

由于数据流可能长达几十万甚至几百万行,我在这里截断了它。(我的主要工作站上列出的日志长度是 1,188,482 行。)请确保是在你的测试系统尝试的这个命令。如果系统已经运行了一段时间 —— 即使重启过很多次 —— 还是会显示大量的数据。查看这些日志数据,因为它包含了很多信息,在进行问题判断时可能非常有用。了解这个数据文件在正常的引导和启动过程中的模样,可以帮助你在问题出现时定位问题。

我将在本系列之后的文章讨论 systemd 日志、journalctl 命令、以及如何整理输出的日志数据来寻找更详细的信息。

内核被 GRUB 加载到内存后,必须先将自己从压缩后的文件中解压出来,才能执行任何有意义的操作。解压自己后,内核开始运行,加载 systemd 并转交控制权。

引导boot阶段到此结束,此时 Linux 内核和 systemd 正在运行,但是无法为用户执行任何生产性任务,因为其他的程序都没有执行,没有命令行解释器提供命令行,没有后台进程管理网络和其他的通信链接,也没有任何东西能够控制计算机执行生产功能。

现在 systemd 可以加载所需的功能性单元以便将系统启动到选择的目标运行状态。

目标

一个 systemd 目标target代表一个 Linux 系统当前的或期望的运行状态。与 SystemV 启动脚本十分类似,目标定义了系统运行必须存在的服务,以及处于目标状态下必须激活的服务。图表 1 展示了使用 systemd 的 Linux 系统可能的运行状态目标。就像在本系列的第一篇文章以及 systemd 启动的手册页(man bootup)所看到的一样,有一些开启不同必要服务的其他中间目标,包括 swap.targettimers.targetlocal-fs.target 等。一些目标(像 basic.target)作为检查点使用,在移动到下一个更高级的目标之前保证所有需要的服务已经启动并运行。

除非开机时在 GRUB 菜单进行更改,systemd 总是启动 default.targetdefault.target 文件是指向真实的目标文件的符号链接。对于桌面工作站,default.target 通常是 graphical.target,等同于 SystemV 的运行等级 5。对于服务器,默认目标多半是 multi-user.target,就像 SystemV 的运行等级 3。emergency.target 文件类似单用户模式。目标和服务service都是一种 systemd 单元。

下面的图表,包含在本系列的上一篇文章中,比较了 systemd 目标和古老的 SystemV 启动运行等级。为了向后兼容,systemd 提供了 systemd 目标别名,允许脚本和系统管理员使用像 init 3 一样的 SystemV 命令改变运行等级。当然,SystemV 命令被转发给 systemd 进行解释和执行。

< 如显示不全,请左右滑动 >

systemd 目标SystemV 运行级别目标别名描述
default.target这个目标通常是一个符号链接,作为 multi-user.targetgraphical.target 的别名。systemd 总是用 default.target 启动系统。default.target** 不能作为halt.targetpoweroff.targetreboot.target` 的别名。
graphical.target5runlevel5.target带有 GUI 的 multi-user.target
4runlevel4.target未使用。运行等级 4 和 SystemV 的运行等级 3 一致,可以创建这个目标并进行定制,用于启动本地服务,而不必更改默认的 multi-user.target
multi-user.target3runlevel3.target运行所有的服务,但是只有命令行界面(CLI) 。
2runlevel2.target多用户,没有 NFS,但是运行其他所有的非 GUI 服务
rescue.target1runlevel1.target一个基本的系统,包括挂载文件系统,但是只运行最基础的服务,以及一个主控制台上的用于救援的命令行解释器。
emergency.targetS单用户模式 —— 没有服务运行;文件系统没有挂载。这是最基础级的操作模式,只有一个运行在主控制台的用于紧急情况的命令行解释器,供用户和系统交互。
halt.target不断电的情况下停止系统
reboot.target6runlevel6.target重启
poweroff.target0runlevel0.target停止系统并关闭电源

每个目标在配置文件中都描述了一组依赖关系。systemd 启动需要的依赖,即 Linux 主机运行在特定功能级别所需的服务。加载目标配置文件中列出的所有依赖并运行后,系统就运行在那个目标等级。如果愿意,你可以在本系列的第一篇文章《学着爱上 systemd》中回顾 systemd 的启动序列和运行时目标。

探索当前的目标

许多 Linux 发行版默认安装一个 GUI 桌面界面,以便安装的系统可以像工作站一样使用。我总是从 Fedora Live USB 引导驱动器安装 Xfce 或 LXDE 桌面。即使是安装一个服务器或者其他基础类型的主机(比如用于路由器和防火墙的主机),我也使用 GUI 桌面的安装方式。

我可以安装一个没有桌面的服务器(数据中心的典型做法),但是这样不满足我的需求。原因不是我需要 GUI 桌面本身,而是 LXDE 安装包含了许多其他默认的服务器安装没有提供的工具,这意味着初始安装之后我需要做的工作更少。

但是,仅仅因为有 GUI 桌面并不意味着我要使用它。我有一个 16 端口的 KVM,可以用于访问我的大部分 Linux 系统的 KVM 接口,但我和它们交互的大部分交互是通过从我的主要工作站建立的远程 SSH 连接。这种方式更安全,而且和 graphical.target 相比,运行 multi-user.target 使用更少的系统资源。

首先,检查默认目标,确认是 graphical.target

[root@testvm1 ~]# systemctl get-default
graphical.target
[root@testvm1 ~]#

然后确认当前正在运行的目标,应该和默认目标相同。你仍可以使用老方法,输出古老的 SystemV 运行等级。注意,前一个运行等级在左边,这里是 N(意思是 None),表示主机启动后没有修改过运行等级。数字 5 是当前的目标,正如古老的 SystemV 术语中的定义:

[root@testvm1 ~]# runlevel
N 5
[root@testvm1 ~]#

注意,runlevel 的手册页指出运行等级已经被淘汰,并提供了一个转换表。

你也可以使用 systemd 方式,命令的输出有很多行,但确实用 systemd 术语提供了答案:

[root@testvm1 ~]# systemctl list-units --type target
UNIT                   LOAD   ACTIVE SUB    DESCRIPTION                
basic.target           loaded active active Basic System              
cryptsetup.target      loaded active active Local Encrypted Volumes    
getty.target           loaded active active Login Prompts              
graphical.target       loaded active active Graphical Interface        
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.target         loaded active active Network                    
nfs-client.target      loaded active active NFS client services        
nss-user-lookup.target loaded active active User and Group Name Lookups
paths.target           loaded active active Paths                      
remote-fs-pre.target   loaded active active Remote File Systems (Pre)  
remote-fs.target       loaded active active Remote File Systems        
rpc_pipefs.target      loaded active active rpc_pipefs.target          
slices.target          loaded active active Slices                    
sockets.target         loaded active active Sockets                    
sshd-keygen.target     loaded active active sshd-keygen.target        
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.
21 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.

上面列出了当前加载的和激活的目标,你也可以看到 graphical.targetmulti-user.targetmulti-user.target 需要在 graphical.target 之前加载。这个例子中,graphical.target 是激活的。

切换到不同的目标

切换到 multi-user.target 很简单:

[root@testvm1 ~]# systemctl isolate multi-user.target

显示器现在应该从 GUI 桌面或登录界面切换到了一个虚拟控制台。登录并列出当前激活的 systemd 单元,确认 graphical.target 不再运行:

[root@testvm1 ~]# systemctl list-units --type target

务必使用 runlevel 确认命令输出了之前的和当前的“运行等级”:

[root@testvm1 ~]# runlevel
5 3

更改默认目标

现在,将默认目标改为 multi-user.target,以便系统总是启动进入 multi-user.target,从而使用控制台命令行接口而不是 GUI 桌面接口。使用你的测试主机的根用户,切换到保存 systemd 配置的目录,执行一次快速列出操作:

[root@testvm1 ~]# cd /etc/systemd/system/ ; ll
drwxr-xr-x. 2 root root 4096 Apr 25  2018  basic.target.wants
&lt;snip&gt;
lrwxrwxrwx. 1 root root   36 Aug 13 16:23  default.target -> /lib/systemd/system/graphical.target
lrwxrwxrwx. 1 root root   39 Apr 25  2018  display-manager.service -> /usr/lib/systemd/system/lightdm.service
drwxr-xr-x. 2 root root 4096 Apr 25  2018  getty.target.wants
drwxr-xr-x. 2 root root 4096 Aug 18 10:16  graphical.target.wants
drwxr-xr-x. 2 root root 4096 Apr 25  2018  local-fs.target.wants
drwxr-xr-x. 2 root root 4096 Oct 30 16:54  multi-user.target.wants
&lt;snip&gt;
[root@testvm1 system]#

为了强调一些有助于解释 systemd 如何管理启动过程的重要事项,我缩短了这个列表。你应该可以在虚拟机看到完整的目录和链接列表。

default.target 项是指向目录 /lib/systemd/system/graphical.target 的符号链接(软链接),列出那个目录查看目录中的其他内容:

[root@testvm1 system]# ll /lib/systemd/system/ | less

你应该在这个列表中看到文件、目录、以及更多链接,但是专门寻找一下 multi-user.targetgraphical.target。现在列出 default.target(指向 /lib/systemd/system/graphical.target 的链接)的内容:

[root@testvm1 system]# cat default.target
#  SPDX-License-Identifier: LGPL-2.1+
#
#  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.
[Unit]
Description=Graphical Interface
Documentation=man:systemd.special(7)
Requires=multi-user.target
Wants=display-manager.service
Conflicts=rescue.service rescue.target
After=multi-user.target rescue.service rescue.target display-manager.service
AllowIsolate=yes
[root@testvm1 system]#

graphical.target 文件的这个链接描述了图形用户接口需要的所有必备条件。我会在本系列的下一篇文章至少探讨其中的一些选项。

为了使主机启动到多用户模式,你需要删除已有的链接,创建一个新链接指向正确目标。如果你的 PWD 不是 /etc/systemd/system,切换过去:

[root@testvm1 system]# rm -f default.target
[root@testvm1 system]# ln -s /lib/systemd/system/multi-user.target default.target

列出 default.target 链接,确认其指向了正确的文件:

[root@testvm1 system]# ll default.target
lrwxrwxrwx 1 root root 37 Nov 28 16:08 default.target -&gt; /lib/systemd/system/multi-user.target
[root@testvm1 system]#

如果你的链接看起来不一样,删除并重试。列出 default.target 链接的内容:

[root@testvm1 system]# cat default.target
#  SPDX-License-Identifier: LGPL-2.1+
#
#  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.
[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@testvm1 system]#

default.target(这里其实是指向 multi-user.target 的链接)其中的 [Unit] 部分现在有不同的必需条件。这个目标不需要有图形显示管理器。

重启,你的虚拟机应该启动到虚拟控制台 1 的控制台登录,虚拟控制台 1 在显示器标识为 tty1。现在你已经知道如何修改默认的目标,使用所需的命令将默认目标改回 graphical.target

首先检查当前的默认目标:

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

输入下面的命令直接切换到 graphical.target 和显示管理器的登录界面,不需要重启:

[root@testvm1 system]# systemctl isolate default.target

我不清楚为何 systemd 的开发者选择了术语 isolate 作为这个子命令。我的研究表明指的可能是运行指明的目标,但是“隔离”并终结其他所有启动该目标不需要的目标。然而,命令执行的效果是从一个运行的目标切换到另一个——在这个例子中,从多用户目标切换到图形目标。上面的命令等同于 SystemV 启动脚本和 init 程序中古老的 init 5 命令。

登录 GUI 桌面,确认能正常工作。

总结

本文探索了 Linux systemd 启动序列,开始探讨两个重要的 systemd 工具 systemctljournalctl,还说明了如何从一个目标切换到另一个目标,以及如何修改默认目标。

本系列的下一篇文章中将会创建一个新的 systemd 单元,并配置为启动阶段运行。下一篇文章还会查看一些配置选项,可以帮助确定某个特定的单元在序列中启动的位置,比如在网络启动运行后。

资源

关于 systemd 网络上有大量的信息,但大部分都简短生硬、愚钝、甚至令人误解。除了本文提到的资源,下面的网页提供了关于 systemd 启动更详细可靠的信息。

  • Fedora 项目有一个优质实用的 systemd 指南,几乎有你使用 systemd 配置、管理、维护一个 Fedora 计算机需要知道的一切。
  • Fedora 项目还有一个好用的 速查表,交叉引用了古老的 SystemV 命令和对应的 systemd 命令。
  • 要获取 systemd 的详细技术信息和创立的原因,查看 Freedesktop.org 的 systemd 描述。

学会爱上 systemd

作者: David Both 译者: LCTT messon007 | 2020-05-13 08:51

systemd 是所有进程之母,负责将 Linux 主机启动到可以做生产性任务的状态。

systemd(是的,全小写,即使在句子开头也是小写),是初始化程序(init)和 SystemV 初始化脚本的现代替代者。此外,它还有更多功能。

当我想到 init 和 SystemV 初始化时,像大多数系统管理员一样,我想到的是 Linux 的启动和关闭,而不是真正意义上的管理服务,例如在服务启动和运行后对其进行管理。像 init 一样,systemd 是所有进程之母,它负责使 Linux 主机启动到可以做生产性任务的状态。systemd 设定的一些功能比老的初始化程序要广泛得多,它要管理正在运行的 Linux 主机的许多方面,包括挂载文件系统、管理硬件、处理定时器以及启动和管理生产性主机所需的系统服务。

Linux 引导

Linux 主机从关机状态到运行状态的完整启动过程很复杂,但它是开放的并且是可知的。在详细介绍之前,我将简要介绍一下从主机硬件被上电到系统准备好用户登录的过程。大多数时候,“引导过程”被作为一个整体来讨论,但这是不准确的。实际上,完整的引导和启动过程包含三个主要部分:

  • ``硬件引导:初始化系统硬件
  • Linux 引导boot:加载 Linux 内核和 systemd
  • Linux 启动startup:systemd 为主机的生产性工作做准备

Linux 启动阶段始于内核加载了 init 或 systemd(取决于具体发行版使用的是旧的方式还是还是新的方式)之后。init 和 systemd 程序启动并管理所有其它进程,它们在各自的系统上都被称为“所有进程之母”。

将硬件引导与 Linux 引导及 Linux 启动区分开,并明确定义它们之间的分界点是很重要的。理解它们的差异以及它们每一个在使 Linux 系统进入生产状态所起的作用,才能够管理这些进程,并更好地确定大部分人所谓的“启动”问题出在哪里。

启动过程按照三步引导流程,使 Linux 计算机进入可进行生产工作的状态。当内核将主机的控制权转移到 systemd 时,启动环节开始。

systemd 之争

systemd 引起了系统管理员和其它负责维护 Linux 系统正常运行人员的广泛争议。在许多 Linux 系统中,systemd 接管了大量任务,这在某些开发者和sysadmins群体中引起了反对和不和谐。

SystemV 和 systemd 是执行 Linux 启动环节的两种不同的方法。SystemV 启动脚本和 init 程序是老的方法,而使用目标target的 systemd 是新方法。尽管大多数现代 Linux 发行版都使用较新的 systemd 进行启动、关机和进程管理,但仍有一些发行版未采用。原因之一是某些发行版维护者和系统管理员喜欢老的 SystemV 方法,而不是新的 systemd。

我认为两者都有其优势。

为何我更喜欢 SystemV

我更喜欢 SystemV,因为它更开放。使用 Bash 脚本来完成启动。内核启动 init 程序(这是一个编译后的二进制)后,init 启动 rc.sysinit 脚本,该脚本执行许多系统初始化任务。rc.sysinit 执行完后,init 启动 /etc/rc.d/rc 脚本,该脚本依次启动 /etc/rc.d/rcX.d 中由 SystemV 启动脚本定义的各种服务。其中 X 是待启动的运行级别号。

除了 init 程序本身之外,所有这些程序都是开放且易于理解的脚本。可以通读这些脚本并确切了解整个启动过程中发生的事情,但是我不认为有太多系统管理员真正做到这一点。每个启动脚本都被编了号,以便按特定顺序启动预期的服务。服务是串行启动的,一次只能启动一个服务。

systemd 是由 Red Hat 的 Lennart Poettering 和 Kay Sievers 开发的,它是一个由大型的、编译的二进制可执行文件构成的复杂系统,不访问其源码就无法理解。它是开源的,因此“访问其源代码”并不难,只是不太方便。systemd 似乎表现出对 Linux 哲学多个原则的重大驳斥。作为二进制文件,systemd 无法被直接打开供系统管理员查看或进行简单更改。systemd 试图做所有事情,例如管理正在运行的服务,同时提供明显比 SystemV 更多的状态信息。它还管理硬件、进程、进程组、文件系统挂载等。systemd 几乎涉足于现代 Linux 主机的每个方面,使它成为系统管理的一站式工具。所有这些都明显违反了“程序应该小,且每个程序都应该只做一件事并做好”的原则。

为何我更喜欢 systemd

我更喜欢用 systemd 作为启动机制,因为它会根据启动阶段并行地启动尽可能多的服务。这样可以加快整个的启动速度,使得主机系统比 SystemV 更快地到达登录屏幕。

systemd 几乎可以管理正在运行的 Linux 系统的各个方面。它可以管理正在运行的服务,同时提供比SystemV 多得多的状态信息。它还管理硬件、进程和进程组、文件系统挂载等。systemd 几乎涉足于现代 Linux 操作系统的每方面,使其成为系统管理的一站式工具。(听起来熟悉吧?)

systemd 工具是编译后的二进制文件,但该工具包是开放的,因为所有配置文件都是 ASCII 文本文件。可以通过各种 GUI 和命令行工具来修改启动配置,也可以添加或修改各种配置文件来满足特定的本地计算环境的需求。

真正的问题

你认为我不能喜欢两种启动系统吗?我能,我会用它们中的任何一个。

我认为,SystemV 和 systemd 之间大多数争议的真正问题和根本原因在于,在系统管理层面没有选择权。使用 SystemV 还是 systemd 已经由各种发行版的开发人员、维护人员和打包人员选择了(但有充分的理由)。由于 init 极端的侵入性,挖出并替换 init 系统会带来很多影响,会带来很多在发行版设计过程之外难以解决的后果。

尽管该选择实际上是为我而选的,但我的Linux主机能不能开机、能不能工作,这是我平时最关心的。作为最终用户,甚至是系统管理员,我主要关心的是我是否可以完成我的工作,例如写我的书和这篇文章,安装更新以及编写脚本来自动化所有事情。只要我能做我的工作,我就不会真正在意发行版中使用的启动系统。

在启动或服务管理出现问题时,我会在意。无论主机上使用哪种启动系统,我都足够了解如何沿着事件顺序来查找故障并进行修复。

替换SystemV

以前曾有过用更现代的东西替代 SystemV 的尝试。大约在两个版本中,Fedora 使用了一个叫作 Upstart 的东西来替换老化的 SystemV,但是它没有取代 init,也没有提供我所注意到的任何变化。由于 Upstart 并未对 SystemV 的问题进行任何显著的改变,所以在这个方向上的努力很快就被放弃了,转而使用 systemd。

尽管大部分 Linux 开发人员都认可替换旧的 SystemV 启动系统是个好主意,但许多开发人员和系统管理员并不喜欢 systemd。与其重新讨论人们在 systemd 中遇到的或曾经遇到过的所有所谓的问题,不如带你去看两篇好文章,尽管有些陈旧,但它们涵盖了大多数内容。Linux 内核的创建者 Linus Torvalds 对 systemd 似乎不感兴趣。在 2014 年 ZDNet 的一篇文章《Linus Torvalds 和其他人对 Linux 上的 systemd 的看法》中,Linus 清楚地表达了他的感受。

“实际上我对 systemd 本身没有任何特别强烈的意见。我对一些核心开发人员有一些问题,我认为他们在对待错误和兼容性方面过于轻率,而且我认为某些设计细节是疯狂的(例如,我不喜欢二进制日志),但这只是细节,不是大问题。”

如果你对 Linus 不太了解的话,我可以告诉你,如果他不喜欢某事,他是非常直言不讳的,很明确,而且相当明确的表示不喜欢。他解决自己对事物不满的方式已经被社会更好地接受了。

2013 年,Poettering 写了一篇很长的博客,他在文章驳斥了关于 systemd 的迷思,同时对创建 systemd 的一些原因进行了深入的剖析。这是一分很好的读物,我强烈建议你阅读。

systemd 任务

根据编译过程中使用的选项(不在本系列中介绍),systemd 可以有多达 69 个二进制可执行文件执行以下任务,其中包括:

  • systemd 程序以 1 号进程(PID 1)运行,并提供使尽可能多服务并行启动的系统启动能力,它额外加快了总体启动时间。它还管理关机顺序。
  • systemctl 程序提供了服务管理的用户接口。
  • 支持 SystemV 和 LSB 启动脚本,以便向后兼容。
  • 服务管理和报告提供了比 SystemV 更多的服务状态数据。
  • 提供基本的系统配置工具,例如主机名、日期、语言环境、已登录用户的列表,正在运行的容器和虚拟机、系统帐户、运行时目录及设置,用于简易网络配置、网络时间同步、日志转发和名称解析的守护进程。
  • 提供套接字管理。
  • systemd 定时器提供类似 cron 的高级功能,包括在相对于系统启动、systemd 启动时间、定时器上次启动时间的某个时间点运行脚本。
  • 它提供了一个工具来分析定时器规范中使用的日期和时间。
  • 能感知分层的文件系统挂载和卸载功能可以更安全地级联挂载的文件系统。
  • 允许主动的创建和管理临时文件,包括删除。
  • D-Bus 的接口提供了在插入或移除设备时运行脚本的能力。这允许将所有设备(无论是否可插拔)都被视为即插即用,从而大大简化了设备的处理。
  • 分析启动环节的工具可用于查找耗时最多的服务。
  • 它包括用于存储系统消息的日志以及管理日志的工具。

架构

这些以及更多的任务通过许多守护程序、控制程序和配置文件来支持。图 1 显示了许多属于 systemd 的组件。这是一个简化的图,旨在提供概要描述,因此它并不包括所有独立的程序或文件。它也不提供数据流的视角,数据流是如此复杂,因此在本系列文章的背景下没用。

图 1:systemd 的架构,作者 Shmuel Csaba Otto Traian (CC BY-SA 3.0)

图 1:systemd 的架构,作者 Shmuel Csaba Otto Traian (CC BY-SA 3.0)

如果要完整地讲解 systemd 就需要一本书。你不需要了解图 1 中的 systemd 组件是如何组合在一起的细节。只需了解支持各种 Linux 服务管理以及日志文件和日志处理的程序和组件就够了。但是很明显, systemd 并不是某些批评者所宣称的那样,它是一个单一的怪物。

作为 1 号进程的 systemd

systemd 是 1 号进程(PID 1)。它的一些功能,比老的 SystemV3 init 要广泛得多,用于管理正在运行的 Linux 主机的许多方面,包括挂载文件系统以及启动和管理 Linux 生产主机所需的系统服务。与启动环节无关的任何 systemd 任务都不在本文讨论范围之内(但本系列后面的一些文章将探讨其中的一些任务)。

首先,systemd 挂载 /etc/fstab 所定义的文件系统,包括所有交换文件或分区。此时,它可以访问位于 /etc 中的配置文件,包括它自己的配置文件。它使用其配置链接 /etc/systemd/system/default.target 来确定将主机引导至哪个状态或目标。default.target 文件是指向真实目标文件的符号链接。对于桌面工作站,通常是 graphical.target,它相当于 SystemV 中的运行级别 5。对于服务器,默认值更可能是 multi-user.target,相当于 SystemV 中的运行级别 3。emergency.target 类似于单用户模式。目标target和服务service是 systemd 的单元unit。

下表(图 2)将 systemd 目标与老的 SystemV 启动运行级别进行了比较。systemd 提供 systemd 目标别名以便向后兼容。目标别名允许脚本(以及许多系统管理员)使用 SystemV 命令(如 init 3)更改运行级别。当然,SystemV 命令被转发给 systemd 进行解释和执行。

< 如显示不全,请左右滑动 >

systemd 目标SystemV 运行级别目标别名描述
default.target此目标总是通过符号连接的方式成为 multi-user.targetgraphical.target 的别名。systemd 始终使用 default.target 来启动系统。default.target 绝不应该设为 halt.targetpoweroff.targetreboot.target 的别名。
graphic.target5runlevel5.target带有 GUI 的 multi-user.target
4runlevel4.target未用。在 SystemV 中运行级别 4 与运行级别 3 相同。可以创建并自定义此目标以启动本地服务,而无需更改默认的 multi-user.target
multi-user.target3runlevel3.target所有服务在运行,但仅有命令行界面(CLI)。
2runlevel2.target多用户,没有 NFS,其它所有非 GUI 服务在运行。
rescue.target1runlevel1.target基本系统,包括挂载文件系统,运行最基本的服务和主控制台的恢复 shell。
emergency.targetS单用户模式:没有服务运行;不挂载文件系统。这是最基本的工作级别,只有主控制台上运行的一个紧急 Shell 供用户与系统交互。
halt.target停止系统而不关闭电源。
reboot.target6runlevel6.target重启。
poweroff.target0runlevel0.target停止系统并关闭电源。

图 2:SystemV 运行级别与 systemd 目标和一些目标别名的比较

每个目标在其配置文件中都描述了一个依赖集。systemd 启动必须的依赖项,这些依赖项是运行 Linux 主机到特定功能级别所需的服务。当目标配置文件中列出的所有依赖项被加载并运行后,系统就在该目标级别运行了。在图 2 中,功能最多的目标位于表的顶部,从顶向下,功能逐步递减。

systemd 还会检查老的 SystemV init 目录,以确认是否存在任何启动文件。如果有,systemd 会将它们作为配置文件以启动它们描述的服务。网络服务是一个很好的例子,在 Fedora 中它仍然使用 SystemV 启动文件。

图 3(如下)是直接从启动手册页复制来的。它显示了 systemd 启动期间一般的事件环节以及确保成功启动的基本顺序要求。

                                       cryptsetup-pre.target
                                                   |
 (various low-level                                v
     API VFS mounts:                 (various cryptsetup devices...)
  mqueue, configfs,                                |    |
  debugfs, ...)                                    v    |
  |                                  cryptsetup.target  |
  |  (various swap                                 |    |    remote-fs-pre.target
  |   devices...)                                  |    |     |        |
  |    |                                           |    |     |        v
  |    v                       local-fs-pre.target |    |     |  (network file systems)
  |  swap.target                       |           |    v     v                 |
  |    |                               v           |  remote-cryptsetup.target  |
  |    |  (various low-level  (various mounts and  |             |              |
  |    |   services: udevd,    fsck services...)   |             |    remote-fs.target
  |    |   tmpfiles, random            |           |             |             /
  |    |   seed, sysctl, ...)          v           |             |            /
  |    |      |                 local-fs.target    |             |           /
  |    |      |                        |           |             |          /
  \____|______|_______________   ______|___________/             |         /
                              \ /                                |        /
                               v                                 |       /
                        sysinit.target                           |      /
                               |                                 |     /
        ______________________/|\_____________________           |    /
       /              |        |      |               \          |   /
       |              |        |      |               |          |  /
       v              v        |      v               |          | /
  (various       (various      |  (various            |          |/
   timers...)      paths...)   |   sockets...)        |          |
       |              |        |      |               |          |
       v              v        |      v               |          |
 timers.target  paths.target   |  sockets.target      |          |
       |              |        |      |               v          |
       v              \_______ | _____/         rescue.service   |
                              \|/                     |          |
                               v                      v          |
                           basic.target         rescue.target    |
                               |                                 |
                       ________v____________________             |
                      /              |              \            |
                      |              |              |            |
                      v              v              v            |
                  display-    (various system   (various system  |
              manager.service     services        services)      |
                      |         required for        |            |
                      |        graphical UIs)       v            v
                      |              |            multi-user.target
 emergency.service    |              |              |
         |            \_____________ | _____________/
         v                          \|/
 emergency.target                    v
                              graphical.target

图 3: systemd 启动图

sysinit.targetbasic.target 目标可以看作启动过程中的检查点。尽管 systemd 的设计目标之一是并行启动系统服务,但是某些服务和功能目标必须先启动,然后才能启动其它服务和目标。直到该检查点所需的所有服务和目标被满足后才能通过这些检查点。

sysinit.target 所依赖的所有单元都完成时,就会到达 sysinit.target。所有这些单元,包括挂载文件系统、设置交换文件、启动 Udev、设置随机数生成器种子、启动低层服务以及配置安全服务(如果一个或多个文件系统是加密的)都必须被完成,但在 sysinit.target 中,这些任务可以并行执行。

sysinit.target 启动了系统接近正常运行所需的所有低层服务和单元,它们也是进入 basic.target 所需的。

在完成 sysinit.target 之后,systemd 会启动实现下一个目标所需的所有单元。basic.target 通过启动所有下一目标所需的单元来提供一些额外功能。包括设置为各种可执行程序目录的路径、设置通信套接字和计时器之类。

最后,用户级目标 multi-user.targetgraphical.target 被初始化。要满足 graphical.target 的依赖必须先达到 multi-user.target。图 3 中带下划线的目标是通常的启动目标。当达到这些目标之一时,启动就完成了。如果 multi-user.target 是默认设置,那么你应该在控制台上看到文本模式的登录界面。如果 graphical.target 是默认设置,那么你应该看到图形的登录界面。你看到的具体的 GUI 登录界面取决于你的默认显示管理器。

引导手册页还描述并提供了引导到初始化 RAM 磁盘和 systemd 关机过程的图。

systemd 还提供了一个工具,该工具列出了完整的启动过程或指定单元的依赖项。单元是一个可控的 systemd 资源实体,其范围可以从特定服务(例如 httpd 或 sshd)到计时器、挂载、套接字等。尝试以下命令并滚动查看结果。

systemctl list-dependencies graphical.target

注意,这会完全展开使系统进入 graphical.target 运行模式所需的顶层目标单元列表。也可以使用 --all 选项来展开所有其它单元。

systemctl list-dependencies --all graphical.target

你可以使用 less 命令来搜索诸如 targetslicesocket 之类的字符串。

现在尝试下面的方法。

systemctl list-dependencies multi-user.target

systemctl list-dependencies rescue.target

systemctl list-dependencies local-fs.target

systemctl list-dependencies dbus.service

这个工具帮助我可视化我正用的主机的启动依赖细节。继续花一些时间探索一个或多个 Linux 主机的启动树。但是要小心,因为 systemctl 手册页包含以下注释:

“请注意,此命令仅列出当前被服务管理器加载到内存的单元。尤其是,此命令根本不适合用于获取特定单元的全部反向依赖关系列表,因为它不会列出被单元声明了但是未加载的依赖项。”

结尾语

即使在没有深入研究 systemd 之前,很明显能看出它既强大又复杂。显然,systemd 不是单一、庞大、独体且不可知的二进制文件。相反,它是由许多较小的组件和旨在执行特定任务的子命令组成。

本系列的下一篇文章将更详细地探讨 systemd 的启动,以及 systemd 的配置文件,更改默认的目标以及如何创建简单服务单元。

资源

互联网上有大量关于 systemd 的信息,但是很多都很简短、晦涩甚至是带有误导。除了本文提到的资源外,以下网页还提供了有关 systemd 启动的更详细和可靠的信息。

  • Fedora 项目有一个很好的实用的 systemd 指南。它有你需要知道的通过 systemd 来配置、管理和维护 Fedora 主机所需的几乎所有知识。

  • Fedora 项目还有一个不错的速记表,将老的 SystemV 命令与对比的 systemd 命令相互关联。

  • 有关 systemd 的详细技术信息及创建它的原因,请查看 Freedesktop.orgsystemd 描述


三件可以用 systemd 做的令人惊讶的事情

作者: Alan Smithee 译者: LCTT Xingyu.Wang | 2023-04-02 13:55

它不仅仅是为了让你的电脑启动得更快。

当 systemd 刚问世时,有很多关于它能加快启动时间的消息。这项功能对大多数人都有吸引力(对那些不重启的人来说就不那么重要了),所以在很多方面,这也是它今天仍然拥有的声誉。虽然 systemd 确实是在启动过程中并行启动服务起到了作用,但它的作用远不止于此。以下是你可能没有意识到 systemd 可以做的三件事,但你应该好好利用。

1、简化 Linux ps

如果你曾经使用过 ps,甚至只是 top 命令,那么你就会知道你的电脑一直都在运行数百个进程。有时,这正是你需要的信息,以便了解你的计算机或其用户在做什么。其他时候,你真正需要的是一个总体的概览。

systemd-cgtop 命令提供了一个基于控制组cgroup任务安排的计算机负载的简单视图。控制组 对现代 Linux 很重要,基本上是容器和 Kubernetes 的底层支持结构(这也是云计算可以扩展的原因),但它们也是家庭电脑上的有用结构。例如,从 systemd-cgtop 的输出中,你可以看到用户进程的负载,而不是系统进程:

Control Group               Proc+   %CPU   Memory  Input/s Output/s
/                             183    5.0     1.6G       0B     3.0M
user.slice                      4    2.8     1.1G       0B   174.7K
user.slice/user-1000.slice      4    2.8   968.2M       0B   174.7K
system.slice                   65    2.2     1.5G       0B     2.8M

你也可以只查看你的用户空间进程,或者查看用户空间进程和内核线程。

这绝不是对 topps 的替代,而是从一个不同的、独特的角度来观察你的系统。在运行容器时,它可能是至关重要的,因为容器使用控制组。

2、Linux 定时任务

Cron 是 Linux 的一个经典组件。当你想安排一些事情定期发生时,你会使用 Cron。它很可靠,而且相当好地集成到你的系统中。

问题是,Cron 并不了解有些计算机会被关闭。如果你有一个安排在午夜的 Cron 任务,但你每天在 23:59 关闭你的电脑,那么你的 Cron 任务就永远不会运行。Cron 没有任何工具可以检测到一夜之间错过了工作。

作为对这个问题的回答,有一个很好的 Anacron,但它不像 Cron 那样集成的好。要让 Anacron 运行,你需要做很多设置。

第二个选择是 systemd 计时器。和 Cron 一样,它也是内置的,可以随时使用。你需要写一个单元文件,这肯定比单行的 Crontab 条目多,但也很简单。例如,这里有一个单元文件,在开机 30 分钟后运行一个假想的备份脚本,但每天只运行一次。这可以确保我的电脑得到备份,并防止它每天尝试备份超过一次。

[Unit]
Description=Backup
Requires=myBackup.service
[Timer]
OnBootSec=30min
OnUnitActiveSec=1d
[Install]
WantedBy=timers.target

当然,你也可以干预并提示运行一个任务。多亏了 OnUnitActiveSec 指令,systemd 不会试图运行你手动激活的作业。

3、运行 Linux 容器

容器使启动一个复杂的服务变得非常容易。你可以在短短几分钟内运行一个 Mattermost 或 Discourse 服务器。在某些情况下,困难的部分是在你运行容器后管理和监控它们。Podman 使得管理它们变得容易,但是用什么来管理 Podman 呢?嗯,你可以使用 systemd

Podman 有一个内置的命令来生成单元文件,这样你的容器就可以被 systemd 管理和监控:

$ podman generate systemd --new --files --name example_pod

然后你所要做的就是启动服务:

$ systemctl --user start pod-example_pod.service

和其他服务一样,systemd 确保你的容器荚在任何情况下都能运行。它记录问题,你可以用 journalctl 和其他重要的日志来查看,你也可以用 systemd-cgtop 在控制组中监控它的活动。

它不是 Kubernetes 平台,但对于一两个容器来说,你只需要在可靠和可预测的基础上提供服务,Podman 和 systemd 是一对很棒的组合。

下载 systemd 电子书

systemd 的内容还有很多,你可以从作者 David Both 的新书《systemd 实用指南》中了解基础知识,以及很多实用的技巧。


使用 systemd 来管理启动项

作者: David Both 译者: LCTT tt67wq | 2021-05-18 11:00

了解 systemd 是怎样决定服务启动顺序,即使它本质上是个并行系统。

最近在设置 Linux 系统时,我想知道如何确保服务和其他单元的依赖关系在这些依赖于它们的服务和单元启动之前就已经启动并运行了。我需要更多 systemd 如何管理启动程序的相关知识,特别是在本质上是一个并行的系统中如何是决定服务启动顺序的。

你可能知道 SystemV(systemd 的前身,我在这个系列的 第一篇文章 中解释过)通过 Sxx 前缀命名启动脚本来决定启动顺序,xx 是一个 00-99 的数字。然后 SystemV 利用文件名来排序,然后按照所需的运行级别执行队列中每个启动脚本。

但是 systemd 使用单元文件来定义子程序,单元文件可由系统管理员创建或编辑,这些文件不仅可以用于初始化时也可以用于常规操作。在这个系列的 第三篇文章 中,我解释了如何创建一个挂载单元文件。在第五篇文章中,我解释了如何创建一种不同的单元文件 —— 在启动时执行一个程序的服务单元文件。你也可以修改单元文件中某些配置,然后通过 systemd 日志去查看你的修改在启动序列中的位置。

准备工作

先确认你已经在 /etc/default/grub 文件中的 GRUB_CMDLINE_LINUX= 这行移除了 rhgbquiet,如同我在这个系列的 第二篇文章 中展示的那样。这让你能够查看 Linux 启动信息流,你在这篇文章中部分实验中需要用到。

程序

在本教程中,你会创建一个简单的程序让你能够在主控台和后续的 systemd 日志中查看启动时的信息。

创建一个 shell 程序 /usr/local/bin/hello.sh 然后添加下述内容。你要确保执行结果在启动时是可见的,可以轻松的在 systemd 日志中找到它。你会使用一版携带一些方格的 “Hello world” 程序,这样它会非常显眼。为了确保这个文件是可执行的,且为了安全起见,它需要 root 的用户和组所有权和 700 权限

#!/usr/bin/bash
# Simple program to use for testing startup configurations
# with systemd.
# By David Both
# Licensed under GPL V2
#
echo "###############################"
echo "######### Hello World! ########"
echo "###############################"

在命令行中执行这个程序来检查它能否正常运行。

[root@testvm1 ~]# hello.sh
###############################
######### Hello World! ########
###############################
[root@testvm1 ~]#

这个程序可以用任意脚本或编译语言实现。hello.sh 程序可以被放在 Linux 文件系统层级结构(FHS)上的任意位置。我把它放在 /usr/local/bin 目录下,这样它可以直接在命令行中执行而不必在打命令的时候前面带上路径。我发现我创建的很多 shell 程序需要从命令行和其他工具(如 systemd)运行。

服务单元文件

创建服务单元文件 /etc/systemd/system/hello.service,写入下述内容。这个文件不一定是要可执行的,但是为了安全起见,它需要 root 的用户和组所有权和 644640 权限。

# Simple service unit file to use for testing
# startup configurations with systemd.
# By David Both
# Licensed under GPL V2
#
[Unit]
Description=My hello shell script
[Service]
Type=oneshot
ExecStart=/usr/local/bin/hello.sh
[Install]
WantedBy=multi-user.target

通过查看服务状态来确认服务单元文件能如期运行。如有任何语法问题,这里会显示错误。

[root@testvm1 ~]# systemctl status hello.service
● hello.service - My hello shell script
     Loaded: loaded (/etc/systemd/system/hello.service; disabled; vendor preset: disabled)
     Active: inactive (dead)
[root@testvm1 ~]#

你可以运行这类 “oneshot”(单发)类型的服务多次而不会有问题。此类服务适用于服务单元文件启动的程序是主进程,必须在 systemd 启动任何依赖进程之前完成的服务。

共有 7 种服务类型,你可以在 systemd.service(5) 的手册页上找到每一种(以及服务单元文件的其他部分)的详细解释。(你也可以在文章末尾的 [资料](file:///Users/xingyuwang/develop/TranslateProject-wxy/translated/tech/tmp.bYMHU00BHs#resources) 中找到更多信息。)

出于好奇,我想看看错误是什么样子的。所以我从 Type=oneshot 这行删了字母 “o”,现在它看起来是这样 Type=neshot,现在再次执行命令:

[root@testvm1 ~]# systemctl status hello.service
● hello.service - My hello shell script
     Loaded: loaded (/etc/systemd/system/hello.service; disabled; vendor preset: disabled)
     Active: inactive (dead)
May 06 08:50:09 testvm1.both.org systemd[1]: /etc/systemd/system/hello.service:12: Failed to parse service type, ignoring: neshot
[root@testvm1 ~]#

执行结果明确地告诉我错误在哪,这样解决错误变得十分容易。

需要注意的是即使在你将 hello.service 文件保存为它原来的形式之后,错误依然存在。虽然重启机器能消除这个错误,但你不必这么做,所以我去找了一个清理这类持久性错误的方法。我曾遇到有些错误需要 systemctl daemon-reload 命令来重置错误状态,但是在这个例子里不起作用。可以用这个命令修复的错误似乎总是有一个这样的声明,所以你知道要运行它。

然而,每次修改或新建一个单元文件之后执行 systemctl daemon-reload 确实是值得推荐的做法。它提醒 systemd 有修改发生,而且它可以防止某些与管理服务或单元相关的问题。所以继续去执行这条命令吧。

在修改完服务单元文件中的拼写错误后,一个简单的 systemctl restart hello.service 命令就可以清除错误。实验一下,通过添加一些其他的错误至 hello.service 文件来看看会得到怎样的结果。

启动服务

现在你已经准备好启动这个新服务,通过检查状态来查看结果。尽管你可能之前已经重启过,你仍然可以启动或重启这个单发服务任意次,因为它只运行一次就退出了。

继续启动这个服务(如下所示),然后检查状态。你的结果可能和我的有区别,取决于你做了多少试错实验。

[root@testvm1 ~]# systemctl start hello.service
[root@testvm1 ~]# systemctl status hello.service
● hello.service - My hello shell script
     Loaded: loaded (/etc/systemd/system/hello.service; disabled; vendor preset: disabled)
     Active: inactive (dead)
May 10 10:37:49 testvm1.both.org hello.sh[842]: ######### Hello World! ########
May 10 10:37:49 testvm1.both.org hello.sh[842]: ###############################
May 10 10:37:49 testvm1.both.org systemd[1]: hello.service: Succeeded.
May 10 10:37:49 testvm1.both.org systemd[1]: Finished My hello shell script.
May 10 10:54:45 testvm1.both.org systemd[1]: Starting My hello shell script...
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ###############################
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ######### Hello World! ########
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ###############################
May 10 10:54:45 testvm1.both.org systemd[1]: hello.service: Succeeded.
May 10 10:54:45 testvm1.both.org systemd[1]: Finished My hello shell script.
[root@testvm1 ~]#

从状态检查命令的输出中我们可以看到,systemd 日志表明 hello.sh 启动然后服务结束了。你也可以看到脚本的输出。该输出是根据服务的最近调用的日志记录生成的,试试看多启动几次这个服务,然后再看状态命令的输出就能理解我所说的。

你也应该直接查看日志内容,有很多种方法可以实现。一种办法是指定记录类型标识符,在这个例子中就是 shell 脚本的名字。它会展示前几次重启和当前会话的日志记录。如你所见,我已经为这篇文章做了挺长一段时间的研究测试了。

[root@testvm1 ~]# journalctl -t hello.sh
<剪去>
-- Reboot --
May 08 15:55:47 testvm1.both.org hello.sh[840]: ###############################
May 08 15:55:47 testvm1.both.org hello.sh[840]: ######### Hello World! ########
May 08 15:55:47 testvm1.both.org hello.sh[840]: ###############################
-- Reboot --
May 08 16:01:51 testvm1.both.org hello.sh[840]: ###############################
May 08 16:01:51 testvm1.both.org hello.sh[840]: ######### Hello World! ########
May 08 16:01:51 testvm1.both.org hello.sh[840]: ###############################
-- Reboot --
May 10 10:37:49 testvm1.both.org hello.sh[842]: ###############################
May 10 10:37:49 testvm1.both.org hello.sh[842]: ######### Hello World! ########
May 10 10:37:49 testvm1.both.org hello.sh[842]: ###############################
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ###############################
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ######### Hello World! ########
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ###############################
[root@testvm1 ~]#

为了定位 hello.service 单元的 systemd 记录,你可以在 systemd 中搜索。你可以使用 G+Enter 来翻页到日志记录 记录的末尾,然后用回滚来找到你感兴趣的日志。使用 -b 选项仅展示最近启动的记录。

[root@testvm1 ~]# journalctl -b -t systemd
<剪去>
May 10 10:37:49 testvm1.both.org systemd[1]: Starting SYSV: Late init script for live image....
May 10 10:37:49 testvm1.both.org systemd[1]: Started SYSV: Late init script for live image..
May 10 10:37:49 testvm1.both.org systemd[1]: hello.service: Succeeded.
May 10 10:37:49 testvm1.both.org systemd[1]: Finished My hello shell script.
May 10 10:37:50 testvm1.both.org systemd[1]: Starting D-Bus System Message Bus...
May 10 10:37:50 testvm1.both.org systemd[1]: Started D-Bus System Message Bus.

我拷贝了一些其他的日志记录,让你对你可能找到的东西有所了解。这条命令喷出了所有属于 systemd 的日志内容 —— 当我写这篇时是 109183 行。这是一个需要整理的大量数据。你可以使用页面的搜索功能,通常是 less 或者你可以使用内置的 grep 特性。-g( 或 --grep=)选项可以使用兼容 Perl 的正则表达式。

[root@testvm1 ~]# journalctl -b -t systemd -g "hello"
[root@testvm1 ~]# journalctl -b -t systemd -g "hello"
-- Logs begin at Tue 2020-05-05 18:11:49 EDT, end at Sun 2020-05-10 11:01:01 EDT. --
May 10 10:37:49 testvm1.both.org systemd[1]: Starting My hello shell script...
May 10 10:37:49 testvm1.both.org systemd[1]: hello.service: Succeeded.
May 10 10:37:49 testvm1.both.org systemd[1]: Finished My hello shell script.
May 10 10:54:45 testvm1.both.org systemd[1]: Starting My hello shell script...
May 10 10:54:45 testvm1.both.org systemd[1]: hello.service: Succeeded.
May 10 10:54:45 testvm1.both.org systemd[1]: Finished My hello shell script.
[root@testvm1 ~]#

你可以使用标准的 GNU grep 命令,但是这不会展示日志首行的元数据。

如果你只想看包含你的 hello 服务的日志记录,你可以指定时间来缩小范围。举个例子,我将在我的测试虚拟机上以 10:54:00 为开始时间,这是上述的日志记录开始的分钟数。注意 --since= 的选项必须加引号,这个选项也可以写成 -S "某个时间"

日期和时间可能在你的机器上有所不同,所以确保使用能匹配你日志中的时间的时间戳。

[root@testvm1 ~]# journalctl --since="2020-05-10 10:54:00"
May 10 10:54:35 testvm1.both.org audit: BPF prog-id=54 op=LOAD
May 10 10:54:35 testvm1.both.org audit: BPF prog-id=55 op=LOAD
May 10 10:54:45 testvm1.both.org systemd[1]: Starting My hello shell script...
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ###############################
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ######### Hello World! ########
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ###############################
May 10 10:54:45 testvm1.both.org systemd[1]: hello.service: Succeeded.
May 10 10:54:45 testvm1.both.org systemd[1]: Finished My hello shell script.
May 10 10:54:45 testvm1.both.org audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=hello comm="systemd" exe="/usr/lib/systemd"'
May 10 10:54:45 testvm1.both.org audit[1]: SERVICE_STOP pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=hello comm="systemd" exe="/usr/lib/systemd/"'
May 10 10:56:00 testvm1.both.org NetworkManager[840]: <error> [1589122560.0633] dhcp4 (enp0s3): error -113 dispatching events
May 10 10:56:00 testvm1.both.org NetworkManager[840]: <info>  [1589122560.0634] dhcp4 (enp0s3): state changed bound -> fail
<剪去>

since 选项跳过了指定时间点的所有记录,但在此时间点之后仍有大量你不需要的记录。你也可以使用 until 选项来裁剪掉你感兴趣的时间之后的记录。我想要事件发生时附近的一分钟,其他的都不用:

[root@testvm1 ~]# journalctl --since="2020-05-10 10:54:35" --until="2020-05-10 10:55:00"
-- Logs begin at Tue 2020-05-05 18:11:49 EDT, end at Sun 2020-05-10 11:04:59 EDT. --
May 10 10:54:35 testvm1.both.org systemd[1]: Reloading.
May 10 10:54:35 testvm1.both.org audit: BPF prog-id=27 op=UNLOAD
May 10 10:54:35 testvm1.both.org audit: BPF prog-id=26 op=UNLOAD
<剪去>
ay 10 10:54:35 testvm1.both.org audit: BPF prog-id=55 op=LOAD
May 10 10:54:45 testvm1.both.org systemd[1]: Starting My hello shell script...
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ###############################
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ######### Hello World! ########
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ###############################
May 10 10:54:45 testvm1.both.org systemd[1]: hello.service: Succeeded.
May 10 10:54:45 testvm1.both.org systemd[1]: Finished My hello shell script.
May 10 10:54:45 testvm1.both.org audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=hello comm="systemd" exe="/usr/lib/systemd>
May 10 10:54:45 testvm1.both.org audit[1]: SERVICE_STOP pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=hello comm="systemd" exe="/usr/lib/systemd/>
lines 1-46/46 (END

如果在这个时间段中仍然有大量的活动的话,你可以使用这些选项组合来进一步缩小结果数据流:

[root@testvm1 ~]# journalctl --since="2020-05-10 10:54:35" --until="2020-05-10 10:55:00" -t "hello.sh"
-- Logs begin at Tue 2020-05-05 18:11:49 EDT, end at Sun 2020-05-10 11:10:41 EDT. --
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ###############################
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ######### Hello World! ########
May 10 10:54:45 testvm1.both.org hello.sh[1380]: ###############################
[root@testvm1 ~]#

你的结果应该与我的相似。你可以从这一系列的实验中看出,这个服务运行的很正常。

重启 —— 还是走到这一步

到目前为止,你还没有重启过安装了服务的机器。所以现在重启吧,因为毕竟这个教程是关于启动阶段程序运行的情况。首先,你需要在启动序列中启用这个服务。

[root@testvm1 ~]# systemctl enable hello.service
Created symlink /etc/systemd/system/multi-user.target.wants/hello.service → /etc/systemd/system/hello.service.
[root@testvm1 ~]#

注意到这个软链接是被创建在 /etc/systemd/system/multi-user.target.wants 目录下的。这是因为服务单元文件指定了服务是被 multi-user.target 所“需要”的。

重启机器,确保能在启动阶段观察数据流,这样你能看到 “Hello world” 信息。等等……你看见了么?嗯,我看见了。尽管它很快被刷过去了,但是我确实看到 systemd 的信息显示它启动了 hello.service 服务。

看看上次系统启动后的日志。你可以使用页面搜索工具 less 来找到 “Hello” 或 “hello”。我裁剪了很多数据,但是留下了附近的日志记录,这样你就能感受到和你服务有关的日志记录在本地是什么样子的:

[root@testvm1 ~]# journalctl -b
<剪去>
May 10 10:37:49 testvm1.both.org systemd[1]: Listening on SSSD Kerberos Cache Manager responder socket.
May 10 10:37:49 testvm1.both.org systemd[1]: Reached target Sockets.
May 10 10:37:49 testvm1.both.org systemd[1]: Reached target Basic System.
May 10 10:37:49 testvm1.both.org systemd[1]: Starting Modem Manager...
May 10 10:37:49 testvm1.both.org systemd[1]: Starting Network Manager...
May 10 10:37:49 testvm1.both.org systemd[1]: Starting Avahi mDNS/DNS-SD Stack...
May 10 10:37:49 testvm1.both.org systemd[1]: Condition check resulted in Secure Boot DBX (blacklist) updater being skipped.
May 10 10:37:49 testvm1.both.org systemd[1]: Starting My hello shell script...
May 10 10:37:49 testvm1.both.org systemd[1]: Starting IPv4 firewall with iptables...
May 10 10:37:49 testvm1.both.org systemd[1]: Started irqbalance daemon.
May 10 10:37:49 testvm1.both.org audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=irqbalance comm="systemd" exe="/usr/lib/sy>"'
May 10 10:37:49 testvm1.both.org systemd[1]: Starting LSB: Init script for live image....
May 10 10:37:49 testvm1.both.org systemd[1]: Starting Hardware Monitoring Sensors...
<剪去>
May 10 10:37:49 testvm1.both.org systemd[1]: Starting NTP client/server...
May 10 10:37:49 testvm1.both.org systemd[1]: Starting SYSV: Late init script for live image....
May 10 10:37:49 testvm1.both.org systemd[1]: Started SYSV: Late init script for live image..
May 10 10:37:49 testvm1.both.org audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=livesys-late comm="systemd" exe="/usr/lib/>"'
May 10 10:37:49 testvm1.both.org hello.sh[842]: ###############################
May 10 10:37:49 testvm1.both.org hello.sh[842]: ######### Hello World! ########
May 10 10:37:49 testvm1.both.org hello.sh[842]: ###############################
May 10 10:37:49 testvm1.both.org systemd[1]: hello.service: Succeeded.
May 10 10:37:49 testvm1.both.org systemd[1]: Finished My hello shell script.
May 10 10:37:49 testvm1.both.org audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=hello comm="systemd" exe="/usr/lib/systemd>"'
May 10 10:37:49 testvm1.both.org audit[1]: SERVICE_STOP pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=hello comm="systemd" exe="/usr/lib/systemd/>
May 10 10:37:50 testvm1.both.org audit: BPF prog-id=28 op=LOAD
<剪去>

你可以看到 systemd 启动了 hello.service 单元,它执行了 hello.sh 脚本并将输出记录在日志中。如果你能在启动阶段抓到它,你也应该能看见,systemd 信息表明了它正在启动这个脚本,另外一条信息表明了服务成功。通过观察上面数据流中第一条 systemd 消息,你会发现 systemd 在到达基本的系统目标后很快就启动了你的服务。

但是我想看见信息在启动阶段也被打印出来。有一种方法可以做到:在 hello.service 文件的 [Service] 段中加入下述行:

StandardOutput=journal+console

现在 hello.service 文件看起来像这样:

# Simple service unit file to use for testing
# startup configurations with systemd.
# By David Both
# Licensed under GPL V2
#
[Unit]
Description=My hello shell script
[Service]
Type=oneshot
ExecStart=/usr/local/bin/hello.sh
StandardOutput=journal+console
[Install]
WantedBy=multi-user.target

加上这一行后,重启系统,并在启动过程中观察显示屏上滚动的数据流。你应该在它的小方框中看到信息。在启动序列完成后,你可以查看最近的启动日志,然后定位到你新服务的日志记录。

修改次序

现在你的服务已经可用了,你可以看看它在启动序列中哪个位置启动的,尝试下修改它。需要牢记的是 systemd 倾向于在每个主要目标(basic.targetmulti-user.targetgraphical.**target)中并行启动尽可能多的服务和其他的单元类型。你应该刚刚看过最近一次开机的日志记录,它应该和上面我的日志看上去类似。

注意,systemd 在它到达到基本系统目标(basic.target)后不久就启动了你的测试服务。这正是你在在服务单元文件的 WantedBy 行中指定的,所以它是对的。在你做出修改之前,列出 /etc/systemd/system/multi-user.target.wants 目录下的内容,你会看到一个指向服务单元文件的软链接。服务单元文件的 [Install] 段指定了哪一个目标会启动这个服务,执行 systemctl enable hello.service 命令会在适当的 targets.wants 路径下创建软链接。

hello.service -> /etc/systemd/system/hello.service

某些服务需要在 basic.target 阶段启动,其他则没这个必要,除非系统正在启动 graphical.target。这个实验中的服务不会在 basic.target 期间启动 —— 假设你直到 graphical.target 阶段才需要它启动。那么修改 WantedBy 这一行:

WantedBy=graphical.target

一定要先禁用 hello.service 再重新启用它,这样可以删除旧链接并且在 graphical.targets.wants 目录下创建一个新的链接。我注意到如果我在修改服务需要的目标之前忘记禁用该服务,我可以运行 systemctl disable 命令,链接将从两个 targets.wants 目录中删除。之后我只需要重新启用这个服务然后重启电脑。

启动 graphical.target 下的服务有个需要注意的地方,如果电脑启动到 multi-user.target 阶段,这个服务不会自动启动。如果这个服务需要 GUI 桌面接口,这或许是你想要的,但是它同样可能不是你想要的。

-o short-monotonic 选项来查看 graphical.targetmulti-user.target 的日志,展示内核启动几秒后的日志,精度为微秒级别:

[root@testvm1 ~]# journalctl -b -o short-monotonic

multi-user.target 的部分日志:

[   17.264730] testvm1.both.org systemd[1]: Starting My hello shell script...
[   17.265561] testvm1.both.org systemd[1]: Starting IPv4 firewall with iptables...
<剪去>
[   19.478468] testvm1.both.org systemd[1]: Starting LSB: Init script for live image....
[   19.507359] testvm1.both.org iptables.init[844]: iptables: Applying firewall rules: [  OK  ]
[   19.507835] testvm1.both.org hello.sh[843]: ###############################
[   19.507835] testvm1.both.org hello.sh[843]: ######### Hello World! ########
[   19.507835] testvm1.both.org hello.sh[843]: ###############################
<剪去>
[   21.482481] testvm1.both.org systemd[1]: hello.service: Succeeded.
[   21.482550] testvm1.both.org smartd[856]: Opened configuration file /etc/smartmontools/smartd.conf
[   21.482605] testvm1.both.org systemd[1]: Finished My hello shell script.

还有部分 graphical.target 的日志:

[   19.436815] testvm1.both.org systemd[1]: Starting My hello shell script...
[   19.437070] testvm1.both.org systemd[1]: Starting IPv4 firewall with iptables...
<剪去>
[   19.612614] testvm1.both.org hello.sh[841]: ###############################
[   19.612614] testvm1.both.org hello.sh[841]: ######### Hello World! ########
[   19.612614] testvm1.both.org hello.sh[841]: ###############################
[   19.629455] testvm1.both.org audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=hello comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success'
[   19.629569] testvm1.both.org audit[1]: SERVICE_STOP pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=hello comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success'
[   19.629682] testvm1.both.org systemd[1]: hello.service: Succeeded.
[   19.629782] testvm1.both.org systemd[1]: Finished My hello shell script.

尽管单元文件的 WantedBy 部分包含了 graphical.targethello.service 单元在启动后大约 19.5 或 19.6 秒后运行。但是 hello.servicemulti-user.target 中开始于 17.24 秒,在 graphical target 中开始于 19.43 秒。

这意味着什么呢?看看 /etc/systemd/system/default.target 这个链接。文件内容显示 systemd 先启动了默认目标 graphical.target,然后 graphical.target 触发了 multi-user.target

[root@testvm1 system]# cat default.target
#  SPDX-License-Identifier: LGPL-2.1+
#
#  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.
[Unit]
Description=Graphical Interface
Documentation=man:systemd.special(7)
Requires=multi-user.target
Wants=display-manager.service
Conflicts=rescue.service rescue.target
After=multi-user.target rescue.service rescue.target display-manager.service
AllowIsolate=yes
[root@testvm1 system]#

不管是用 graphical.target 还是 multi-user.target 启动服务,hello.service 单元都在启动后的 19.5 或 19.6 秒后启动。基于这个事实和日志结果(特别是使用单调输出的日志),你就知道这些目标是在并行启动。再看看日志中另外一件事:

[   28.397330] testvm1.both.org systemd[1]: Reached target Multi-User System.
[   28.397431] testvm1.both.org systemd[1]: Reached target Graphical Interface.

两个目标几乎是同时完成的。这是和理论一致的,因为 graphical.target 触发了 multi-user.target,在 multi-user.target 到达(即完成)之前它是不会完成的。但是 hello.service 比这个完成的早的多。

这一切表明,这两个目标几乎是并行启动的。如果你查看日志,你会发现各种目标和来自这类主要目标的服务大多是平行启动的。很明显,multi-user.target 没有必要在 graphical.target 启动前完成。所以,简单的使用这些主要目标来并不能很好地排序启动序列,尽管它在保证单元只在它们被 graphical.target 需要时启动这方面很有用。

在继续之前,把 hello.service 单元文件回滚至 WantedBy=multi-user.target(如果还没做的话)。

确保一个服务在网络运行后启动

一个常见的启动问题是保证一个单元在网络启动运行后再启动。Freedesktop.org 的文章《在网络启动后运行服务》中提到,目前没有一个真正的关于网络何时算作“启动”的共识。然而,这篇文章提供了三个选项,满足完全可用网络需求的是 network-online.target。需要注意的是 network.target 是在关机阶段使用的而不是启动阶段,所以它对你做有序启动方面没什么帮助。

在做出任何改变之前,一定要检查下日志,确认 hello.service 单元在网络可用之前可以正确启动。你可以在日志中查找 network-online.target 来确认。

你的服务并不真的需要网络服务,但是你可以把它当作是需要网络的。

因为设置 WantedBy=graphical.target 并不能保证服务会在网络启动可用后启动,所以你需要其他的方法来做到这一点。幸运的是,有个简单的方法可以做到。将下面两行代码加入 hello.service 单元文件的 [Unit] 段:

After=network-online.target                                                                            
Wants=network-online.target

两个字段都需要才能生效。重启机器,在日志中找到服务的记录:

[   26.083121] testvm1.both.org NetworkManager[842]: <info>  [1589227764.0293] device (enp0s3): Activation: successful, device activated.
[   26.083349] testvm1.both.org NetworkManager[842]: <info>  [1589227764.0301] manager: NetworkManager state is now CONNECTED_GLOBAL
[   26.085818] testvm1.both.org NetworkManager[842]: <info>  [1589227764.0331] manager: startup complete
[   26.089911] testvm1.both.org systemd[1]: Finished Network Manager Wait Online.
[   26.090254] testvm1.both.org systemd[1]: Reached target Network is Online.
[   26.090399] testvm1.both.org audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=NetworkManager-wait-online comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? termina>"'
[   26.091991] testvm1.both.org systemd[1]: Starting My hello shell script...
[   26.095864] testvm1.both.org sssd[be[implicit_files]][1007]: Starting up
[   26.290539] testvm1.both.org systemd[1]: Condition check resulted in Login and scanning of iSCSI devices being skipped.
[   26.291075] testvm1.both.org systemd[1]: Reached target Remote File Systems (Pre).
[   26.291154] testvm1.both.org systemd[1]: Reached target Remote File Systems.
[   26.292671] testvm1.both.org systemd[1]: Starting Notify NFS peers of a restart...
[   26.294897] testvm1.both.org systemd[1]: iscsi.service: Unit cannot be reloaded because it is inactive.
[   26.304682] testvm1.both.org hello.sh[1010]: ###############################
[   26.304682] testvm1.both.org hello.sh[1010]: ######### Hello World! ########
[   26.304682] testvm1.both.org hello.sh[1010]: ###############################
[   26.306569] testvm1.both.org audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=hello comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success'
[   26.306669] testvm1.both.org audit[1]: SERVICE_STOP pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=hello comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success'
[   26.306772] testvm1.both.org systemd[1]: hello.service: Succeeded.
[   26.306862] testvm1.both.org systemd[1]: Finished My hello shell script.
[   26.584966] testvm1.both.org sm-notify[1011]: Version 2.4.3 starting

这样证实了 hello.service 单元会在 network-online.target 之后启动。这正是你想要的。你可能也看见了 “Hello World” 消息在启动阶段出现。还需要注意的是,在启动时记录出现的时间戳比之前要晚了大约 6 秒。

定义启动序列的最好方法

本文章详细地探讨了 Linux 启动时 systemd 和单元文件以及日志的细节,并且发现了当错误被引入单元文件时候会发生什么。作为系统管理员,我发现这类实验有助于我理解程序或者服务出故障时的行为,并且在安全环境中有意破坏是一种学习的好方法。

文章中实验结果证明,仅将服务单元添加至 multi-user.target 或者 graphical.target 并不能确定它在启动序列中的位置。它仅仅决定了一个单元是否作为图形环境一部分启动。事实上,启动目标 multi-user.targetgraphical.target 和所有它们的 Wants 以及 Required 几乎是并行启动的。确保单元在特定位置启动的最好方法是确定它所依赖的单元,并将新单元配置成 WantAfter 它的依赖。


Systemd 定时器:三种使用场景

作者: Paul Brown 译者: LCTT David Dai | 2018-12-02 09:34

继续 systemd 教程,这些特殊的例子可以展示给你如何更好的利用 systemd 定时器单元。

在这个 systemd 系列教程中,我们已经在某种程度上讨论了 systemd 定时器单元。不过,在我们开始讨论 sockets 之前,我们先来看三个例子,这些例子展示了如何最佳化利用这些单元。

简单的类 cron 行为

我每周都要去收集 Debian popcon 数据,如果每次都能在同一时间收集更好,这样我就能看到某些应用程序的下载趋势。这是一个可以使用 cron 任务来完成的典型事例,但 systemd 定时器同样能做到:

# 类 cron 的 popcon.timer
[Unit]
Description= 这里描述了下载并处理 popcon 数据的时刻
[Timer]
OnCalendar= Thu *-*-* 05:32:07
Unit= popcon.service
[Install]
WantedBy= basic.target

实际的 popcon.service 会执行一个常规的 wget 任务,并没有什么特别之处。这里的新内容是 OnCalendar= 指令。这个指令可以让你在一个特定日期的特定时刻来运行某个服务。在这个例子中,Thu 表示 “在周四运行”,*-*-* 表示“具体年份、月份和日期无关紧要”,这些可以翻译成 “不管年月日,只在每周四运行”。

这样,你就设置了这个服务的运行时间。我选择在欧洲中部夏令时区的上午 5:30 左右运行,那个时候服务器不是很忙。

如果你的服务器关闭了,而且刚好错过了每周的截止时间,你还可以在同一个计时器中使用像 anacron 一样的功能。

# 具备类似 anacron 功能的 popcon.timer
[Unit]
Description= 这里描述了下载并处理 popcon 数据的时刻
[Timer]
Unit=popcon.service
OnCalendar=Thu *-*-* 05:32:07
Persistent=true
[Install]
WantedBy=basic.target

当你将 Persistent= 指令设为真值时,它会告诉 systemd,如果服务器在本该它运行的时候关闭了,那么在启动后就要立刻运行服务。这意味着,如果机器在周四凌晨停机了(比如说维护),一旦它再次启动后,popcon.service 将会立刻执行。在这之后,它的运行时间将会回到例行性的每周四早上 5:32.

到目前为止,就是这么简单直白。

延迟执行

但是,我们提升一个档次,来“改进”这个基于 systemd 的监控系统。你应该记得,当你接入摄像头的时候,系统就会开始拍照。假设你并不希望它在你安装摄像头的时候拍下你的脸。你希望将拍照服务的启动时间向后推迟一两分钟,这样你就有时间接入摄像头,然后走到画框外面。

为了完成这件事,首先你要更改 Udev 规则,将它指向一个定时器:

ACTION=="add", SUBSYSTEM=="video4linux", ATTRS{idVendor}=="03f0", 
ATTRS{idProduct}=="e207", TAG+="systemd", ENV{SYSTEMD_WANTS}="picchanged.timer", 
SYMLINK+="mywebcam", MODE="0666"

这个定时器看起来像这样:

# picchanged.timer
[Unit]
Description= 在摄像头接入的一分钟后,开始运行 picchanged
[Timer]
OnActiveSec= 1 m
Unit= picchanged.path
[Install]
WantedBy= basic.target

在你接入摄像头后,Udev 规则被触发,它会调用定时器。这个定时器启动后会等上一分钟(OnActiveSec= 1 m),然后运行 picchanged.path,它会监视主图片的变化picchanged.path 还会负责接触 webcan.service,这个实际用来拍照的服务。

在每天的特定时刻启停 Minetest 服务器

在最后一个例子中,我们认为你决定用 systemd 作为唯一的依赖。讲真,不管怎么样,systemd 差不多要接管你的生活了。为什么不拥抱这个必然性呢?

你有个为你的孩子设置的 Minetest 服务。不过,你还想要假装关心一下他们的教育和成长,要让他们做作业和家务活。所以你要确保 Minetest 只在每天晚上的一段时间内可用,比如五点到七点。

这个跟之前的“在特定时间启动服务”不太一样。写个定时器在下午五点启动服务很简单…:

# minetest.timer
[Unit]
Description= 在每天下午五点运行 minetest.service
[Timer]
OnCalendar= *-*-* 17:00:00
Unit= minetest.service
[Install]
WantedBy= basic.target

…可是编写一个对应的定时器,让它在特定时刻关闭服务,则需要更大剂量的横向思维。

我们从最明显的东西开始 —— 设置定时器:

# stopminetest.timer
[Unit]
Description= 每天晚上七点停止 minetest.service
[Timer]
OnCalendar= *-*-* 19:05:00
Unit= stopminetest.service
[Install]
WantedBy= basic.target

这里棘手的部分是如何去告诉 stopminetest.service 去 —— 你知道的 —— 停止 Minetest. 我们无法从 minetest.service 中传递 Minetest 服务器的 PID. 而且 systemd 的单元词汇表中也没有明显的命令来停止或禁用正在运行的服务。

我们的诀窍是使用 systemd 的 Conflicts= 指令。它和 systemd 的 Wants= 指令类似,不过它所做的事情正相反。如果你有一个 b.service 单元,其中包含一个 Wants=a.service 指令,在这个单元启动时,如果 a.service 没有运行,则 b.service 会运行它。同样,如果你的 b.service 单元中有一行写着 Conflicts= a.service,那么在 b.service 启动时,systemd 会停止 a.service.

这种机制用于两个服务在尝试同时控制同一资源时会发生冲突的场景,例如当两个服务要同时访问打印机的时候。通过在首选服务中设置 Conflicts=,你就可以确保它会覆盖掉最不重要的服务。

不过,你会在一个稍微不同的场景中来使用 Conflicts=. 你将使用 Conflicts= 来干净地关闭 minetest.service

# stopminetest.service
[Unit]
Description= 关闭 Minetest 服务
Conflicts= minetest.service
[Service]
Type= oneshot
ExecStart= /bin/echo "Closing down minetest.service"

stopminetest.service 并不会做特别的东西。事实上,它什么都不会做。不过因为它包含那行 Conflicts=,所以在它启动时,systemd 会关掉 minetest.service.

在你完美的 Minetest 设置中,还有最后一点涟漪:你下班晚了,错过了服务器的开机时间,可当你开机的时候游戏时间还没结束,这该怎么办?Persistent= 指令(如上所述)在错过开始时间后仍然可以运行服务,但这个方案还是不行。如果你在早上十一点把服务器打开,它就会启动 Minetest,而这不是你想要的。你真正需要的是一个确保 systemd 只在晚上五到七点启动 Minetest 的方法:

# minetest.timer
[Unit]
Description= 在下午五到七点内的每分钟都运行 minetest.service
[Timer]
OnCalendar= *-*-* 17..19:*:00
Unit= minetest.service
[Install]
WantedBy= basic.target

OnCalendar= *-*-* 17..19:*:00 这一行有两个有趣的地方:(1) 17..19 并不是一个时间点,而是一个时间段,在这个场景中是 17 到 19 点;以及,(2) 分钟字段中的 * 表示服务每分钟都要运行。因此,你会把它读做 “在下午五到七点间的每分钟,运行 minetest.service”

不过还有一个问题:一旦 minetest.service 启动并运行,你会希望 minetest.timer 不要再次尝试运行它。你可以在 minetest.service 中包含一条 Conflicts= 指令:

# minetest.service
[Unit]
Description= 运行 Minetest 服务器
Conflicts= minetest.timer
[Service]
Type= simple
User= <your user name>
ExecStart= /usr/bin/minetest --server
ExecStop= /bin/kill -2 $MAINPID
[Install]
WantedBy= multi-user.targe

上面的 Conflicts= 指令会保证在 minstest.service 成功运行后,minetest.timer 就会立即停止。

现在,启用并启动 minetest.timer

systemctl enable minetest.timersystemctl start minetest.timer

而且,如果你在六点钟启动了服务器,minetest.timer 会启用;到了五到七点,minetest.timer 每分钟都会尝试启动 minetest.service。不过,一旦 minetest.service 开始运行,systemd 会停止 minetest.timer,因为它会与 minetest.service “冲突”,从而避免计时器在服务已经运行的情况下还会不断尝试启动服务。

在首先启动某个服务时杀死启动它的计时器,这么做有点反直觉,但它是有效的。

总结

你可能会认为,有更好的方式来做上面这些事。我在很多文章中看到过“过度设计”这个术语,尤其是在用 systemd 定时器来代替 cron 的时候。

但是,这个系列文章的目的不是为任何具体问题提供最佳解决方案。它的目的是为了尽可能多地使用 systemd 来解决问题,甚至会到荒唐的程度。它的目的是展示大量的例子,来说明如何利用不同类型的单位及其包含的指令。我们的读者,也就是你,可以从这篇文章中找到所有这些的可实践范例。

尽管如此,我们还有一件事要做:下回中,我们会关注 sockets 和 targets,然后我们将完成对 systemd 单元的介绍。

你可以在 Linux 基金会和 edX 中,通过免费的 Linux 介绍课程中,学到更多关于 Linux 的知识。


使用 systemd 定时器代替 cron 作业

作者: David Both 译者: LCTT tt67wq | 2021-04-18 10:45

定时器提供了比 cron 作业更为细粒度的事件控制。

我正在致力于将我的 cron 作业迁移到 systemd 定时器上。我已经使用定时器多年了,但通常来说,我的学识只足以支撑我当前的工作。但在我研究 systemd 系列 的过程中,我发现 systemd 定时器有一些非常有意思的能力。

与 cron 作业类似,systemd 定时器可以在特定的时间间隔触发事件(shell 脚本和程序),例如每天一次或在一个月中的特定某一天(或许只有在周一生效),或在从上午 8 点到下午 6 点的工作时间内每隔 15 分钟一次。定时器也可以做到 cron 作业无法做到的一些事情。举个例子,定时器可以在特定事件发生后的一段时间后触发一段脚本或者程序去执行,例如开机、启动、上个任务完成,甚至于定时器调用的上个服务单元的完成的时刻。

操作系统维护的计时器

当在一个新系统上安装 Fedora 或者是任意一个基于 systemd 的发行版时,作为系统维护过程的一部分,它会在 Linux 宿主机的后台中创建多个定时器。这些定时器会触发事件来执行必要的日常维护任务,比如更新系统数据库、清理临时目录、轮换日志文件,以及更多其他事件。

作为示例,我会查看一些我的主要工作站上的定时器,通过执行 systemctl status *timer 命令来展示主机上的所有定时器。星号的作用与文件通配相同,所以这个命令会列出所有的 systemd 定时器单元。

[root@testvm1 ~]# systemctl status *timer
● mlocate-updatedb.timer - Updates mlocate database every day
     Loaded: loaded (/usr/lib/systemd/system/mlocate-updatedb.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Fri 2020-06-05 00:00:00 EDT; 15h left
   Triggers: ● mlocate-updatedb.service
Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Updates mlocate database every day.
● logrotate.timer - Daily rotation of log files
     Loaded: loaded (/usr/lib/systemd/system/logrotate.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Fri 2020-06-05 00:00:00 EDT; 15h left
   Triggers: ● logrotate.service
       Docs: man:logrotate(8)
             man:logrotate.conf(5)
Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Daily rotation of log files.
● sysstat-summary.timer - Generate summary of yesterday's process accounting
     Loaded: loaded (/usr/lib/systemd/system/sysstat-summary.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Fri 2020-06-05 00:07:00 EDT; 15h left
   Triggers: ● sysstat-summary.service
Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Generate summary of yesterday's process accounting.
● fstrim.timer - Discard unused blocks once a week
     Loaded: loaded (/usr/lib/systemd/system/fstrim.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Mon 2020-06-08 00:00:00 EDT; 3 days left
   Triggers: ● fstrim.service
       Docs: man:fstrim
Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Discard unused blocks once a week.
● sysstat-collect.timer - Run system activity accounting tool every 10 minutes
     Loaded: loaded (/usr/lib/systemd/system/sysstat-collect.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Thu 2020-06-04 08:50:00 EDT; 41s left
   Triggers: ● sysstat-collect.service
Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Run system activity accounting tool every 10 minutes.
● dnf-makecache.timer - dnf makecache --timer
     Loaded: loaded (/usr/lib/systemd/system/dnf-makecache.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Thu 2020-06-04 08:51:00 EDT; 1min 41s left
   Triggers: ● dnf-makecache.service
Jun 02 08:02:33 testvm1.both.org systemd[1]: Started dnf makecache –timer.
● systemd-tmpfiles-clean.timer - Daily Cleanup of Temporary Directories
     Loaded: loaded (/usr/lib/systemd/system/systemd-tmpfiles-clean.timer; static; vendor preset: disabled)
     Active: active (waiting) since Tue 2020-06-02 08:02:33 EDT; 2 days ago
    Trigger: Fri 2020-06-05 08:19:00 EDT; 23h left
   Triggers: ● systemd-tmpfiles-clean.service
       Docs: man:tmpfiles.d(5)
             man:systemd-tmpfiles(8)
Jun 02 08:02:33 testvm1.both.org systemd[1]: Started Daily Cleanup of Temporary Directories.

每个定时器至少有六行相关信息:

  • 定时器的第一行有定时器名字和定时器目的的简短介绍
  • 第二行展示了定时器的状态,是否已加载,定时器单元文件的完整路径以及预设信息。
  • 第三行指明了其活动状态,包括该定时器激活的日期和时间。
  • 第四行包括了该定时器下次被触发的日期和时间和距离触发的大概时间。
  • 第五行展示了被定时器触发的事件或服务名称。
  • 部分(不是全部)systemd 单元文件有相关文档的指引。我虚拟机上输出中有三个定时器有文档指引。这是一个很好(但非必要)的信息。
  • 最后一行是计时器最近触发的服务实例的日志条目。

你也许有一些不一样的定时器,取决于你的主机。

创建一个定时器

尽管我们可以解构一个或多个现有的计时器来了解其工作原理,但让我们创建我们自己的 服务单元 和一个定时器去触发它。为了保持简单,我们将使用一个相当简单的例子。当我们完成这个实验之后,就能更容易理解其他定时器的工作原理以及发现它们正在做什么。

首先,创建一个运行基础东西的简单的服务,例如 free 命令。举个例子,你可能想定时监控空余内存。在 /etc/systemd/system 目录下创建如下的 myMonitor.server 单元文件。它不需要是可执行文件:

# This service unit is for testing timer units
# By David Both
# Licensed under GPL V2
#
[Unit]
Description=Logs system statistics to the systemd journal
Wants=myMonitor.timer
[Service]
Type=oneshot
ExecStart=/usr/bin/free
[Install]
WantedBy=multi-user.target

这大概是你能创建的最简单的服务单元了。现在我们查看一下服务状态同时测试一下服务单元确保它和我们预期一样可用。

[root@testvm1 system]# systemctl status myMonitor.service
● myMonitor.service - Logs system statistics to the systemd journal
     Loaded: loaded (/etc/systemd/system/myMonitor.service; disabled; vendor preset: disabled)
     Active: inactive (dead)
[root@testvm1 system]# systemctl start myMonitor.service
[root@testvm1 system]#

输出在哪里呢?默认情况下,systemd 服务单元执行程序的标准输出(STDOUT)会被发送到系统日志中,它保留了记录供现在或者之后(直到某个时间点)查看。(在本系列的后续文章中,我将介绍系统日志的记录和保留策略)。专门查看你的服务单元的日志,而且只针对今天。-S 选项,即 --since 的缩写,允许你指定 journalctl 工具搜索条目的时间段。这并不代表你不关心过往结果 —— 在这个案例中,不会有过往记录 —— 如果你的机器以及运行了很长时间且堆积了大量的日志,它可以缩短搜索时间。

[root@testvm1 system]# journalctl -S today -u myMonitor.service
-- Logs begin at Mon 2020-06-08 07:47:20 EDT, end at Thu 2020-06-11 09:40:47 EDT. --
Jun 11 09:12:09 testvm1.both.org systemd[1]: Starting Logs system statistics to the systemd journal...
Jun 11 09:12:09 testvm1.both.org free[377966]:               total        used        free      shared  buff/cache   available
Jun 11 09:12:09 testvm1.both.org free[377966]: Mem:       12635740      522868    11032860        8016     1080012    11821508
Jun 11 09:12:09 testvm1.both.org free[377966]: Swap:       8388604           0     8388604
Jun 11 09:12:09 testvm1.both.org systemd[1]: myMonitor.service: Succeeded.
[root@testvm1 system]#

由服务触发的任务可以是单个程序、一组程序或者是一个脚本语言写的脚本。通过在 myMonitor.service 单元文件里的 [Service] 块末尾中添加如下行可以为服务添加另一个任务:

ExecStart=/usr/bin/lsblk

再次启动服务,查看日志检查结果,结果应该看上去像这样。你应该在日志中看到两条命令的结果输出:

Jun 11 15:42:18 testvm1.both.org systemd[1]: Starting Logs system statistics to the systemd journal...
Jun 11 15:42:18 testvm1.both.org free[379961]:               total        used        free      shared  buff/cache   available
Jun 11 15:42:18 testvm1.both.org free[379961]: Mem:       12635740      531788    11019540        8024     1084412    11812272
Jun 11 15:42:18 testvm1.both.org free[379961]: Swap:       8388604           0     8388604
Jun 11 15:42:18 testvm1.both.org lsblk[379962]: NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
Jun 11 15:42:18 testvm1.both.org lsblk[379962]: sda             8:0    0  120G  0 disk
Jun 11 15:42:18 testvm1.both.org lsblk[379962]: ├─sda1          8:1    0    4G  0 part /boot
Jun 11 15:42:18 testvm1.both.org lsblk[379962]: └─sda2          8:2    0  116G  0 part
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   ├─VG01-root 253:0    0    5G  0 lvm  /
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   ├─VG01-swap 253:1    0    8G  0 lvm  [SWAP]
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   ├─VG01-usr  253:2    0   30G  0 lvm  /usr
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   ├─VG01-tmp  253:3    0   10G  0 lvm  /tmp
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   ├─VG01-var  253:4    0   20G  0 lvm  /var
Jun 11 15:42:18 testvm1.both.org lsblk[379962]:   └─VG01-home 253:5    0   10G  0 lvm  /home
Jun 11 15:42:18 testvm1.both.org lsblk[379962]: sr0            11:0    1 1024M  0 rom
Jun 11 15:42:18 testvm1.both.org systemd[1]: myMonitor.service: Succeeded.
Jun 11 15:42:18 testvm1.both.org systemd[1]: Finished Logs system statistics to the systemd journal.

现在你知道了你的服务可以按预期工作了,在 /etc/systemd/system 目录下创建 myMonitor.timer 定时器单元文件,添加如下代码:

# This timer unit is for testing
# By David Both
# Licensed under GPL V2
#
[Unit]
Description=Logs some system statistics to the systemd journal
Requires=myMonitor.service
[Timer]
Unit=myMonitor.service
OnCalendar=*-*-* *:*:00
[Install]
WantedBy=timers.target

myMonitor.timer 文件中的 OnCalendar 时间格式,*-*-* *:*:00,应该会每分钟触发一次定时器去执行 myMonitor.service 单元。我会在文章的后面进一步探索 OnCalendar 设置。

到目前为止,在服务被计时器触发运行时观察与之有关的日志记录。你也可以跟踪计时器,跟踪服务可以让你接近实时的看到结果。执行 journalctl 时带上 -f 选项:

[root@testvm1 system]# journalctl -S today -f -u myMonitor.service
-- Logs begin at Mon 2020-06-08 07:47:20 EDT. --

执行但是不启用该定时器,看看它运行一段时间后发生了什么:

[root@testvm1 ~]# systemctl start myMonitor.service
[root@testvm1 ~]#

一条结果立即就显示出来了,下一条大概在一分钟后出来。观察几分钟日志,看看你有没有跟我发现同样的事情:

[root@testvm1 system]# journalctl -S today -f -u myMonitor.service
-- Logs begin at Mon 2020-06-08 07:47:20 EDT. --
Jun 13 08:39:18 testvm1.both.org systemd[1]: Starting Logs system statistics to the systemd journal...
Jun 13 08:39:18 testvm1.both.org systemd[1]: myMonitor.service: Succeeded.
Jun 13 08:39:19 testvm1.both.org free[630566]:               total        used        free      shared  buff/cache   available
Jun 13 08:39:19 testvm1.both.org free[630566]: Mem:       12635740      556604    10965516        8036     1113620    11785628
Jun 13 08:39:19 testvm1.both.org free[630566]: Swap:       8388604           0     8388604
Jun 13 08:39:18 testvm1.both.org systemd[1]: Finished Logs system statistics to the systemd journal.
Jun 13 08:39:19 testvm1.both.org lsblk[630567]: NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
Jun 13 08:39:19 testvm1.both.org lsblk[630567]: sda             8:0    0  120G  0 disk
Jun 13 08:39:19 testvm1.both.org lsblk[630567]: ├─sda1          8:1    0    4G  0 part /boot
Jun 13 08:39:19 testvm1.both.org lsblk[630567]: └─sda2          8:2    0  116G  0 part
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   ├─VG01-root 253:0    0    5G  0 lvm  /
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   ├─VG01-swap 253:1    0    8G  0 lvm  [SWAP]
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   ├─VG01-usr  253:2    0   30G  0 lvm  /usr
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   ├─VG01-tmp  253:3    0   10G  0 lvm  /tmp
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   ├─VG01-var  253:4    0   20G  0 lvm  /var
Jun 13 08:39:19 testvm1.both.org lsblk[630567]:   └─VG01-home 253:5    0   10G  0 lvm  /home
Jun 13 08:39:19 testvm1.both.org lsblk[630567]: sr0            11:0    1 1024M  0 rom
Jun 13 08:40:46 testvm1.both.org systemd[1]: Starting Logs system statistics to the systemd journal...
Jun 13 08:40:46 testvm1.both.org free[630572]:               total        used        free      shared  buff/cache   available
Jun 13 08:40:46 testvm1.both.org free[630572]: Mem:       12635740      555228    10966836        8036     1113676    11786996
Jun 13 08:40:46 testvm1.both.org free[630572]: Swap:       8388604           0     8388604
Jun 13 08:40:46 testvm1.both.org lsblk[630574]: NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
Jun 13 08:40:46 testvm1.both.org lsblk[630574]: sda             8:0    0  120G  0 disk
Jun 13 08:40:46 testvm1.both.org lsblk[630574]: ├─sda1          8:1    0    4G  0 part /boot
Jun 13 08:40:46 testvm1.both.org lsblk[630574]: └─sda2          8:2    0  116G  0 part
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   ├─VG01-root 253:0    0    5G  0 lvm  /
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   ├─VG01-swap 253:1    0    8G  0 lvm  [SWAP]
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   ├─VG01-usr  253:2    0   30G  0 lvm  /usr
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   ├─VG01-tmp  253:3    0   10G  0 lvm  /tmp
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   ├─VG01-var  253:4    0   20G  0 lvm  /var
Jun 13 08:40:46 testvm1.both.org lsblk[630574]:   └─VG01-home 253:5    0   10G  0 lvm  /home
Jun 13 08:40:46 testvm1.both.org lsblk[630574]: sr0            11:0    1 1024M  0 rom
Jun 13 08:40:46 testvm1.both.org systemd[1]: myMonitor.service: Succeeded.
Jun 13 08:40:46 testvm1.both.org systemd[1]: Finished Logs system statistics to the systemd journal.
Jun 13 08:41:46 testvm1.both.org systemd[1]: Starting Logs system statistics to the systemd journal...
Jun 13 08:41:46 testvm1.both.org free[630580]:               total        used        free      shared  buff/cache   available
Jun 13 08:41:46 testvm1.both.org free[630580]: Mem:       12635740      553488    10968564        8036     1113688    11788744
Jun 13 08:41:46 testvm1.both.org free[630580]: Swap:       8388604           0     8388604
Jun 13 08:41:47 testvm1.both.org lsblk[630581]: NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
Jun 13 08:41:47 testvm1.both.org lsblk[630581]: sda             8:0    0  120G  0 disk
Jun 13 08:41:47 testvm1.both.org lsblk[630581]: ├─sda1          8:1    0    4G  0 part /boot
Jun 13 08:41:47 testvm1.both.org lsblk[630581]: └─sda2          8:2    0  116G  0 part
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   ├─VG01-root 253:0    0    5G  0 lvm  /
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   ├─VG01-swap 253:1    0    8G  0 lvm  [SWAP]
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   ├─VG01-usr  253:2    0   30G  0 lvm  /usr
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   ├─VG01-tmp  253:3    0   10G  0 lvm  /tmp
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   ├─VG01-var  253:4    0   20G  0 lvm  /var
Jun 13 08:41:47 testvm1.both.org lsblk[630581]:   └─VG01-home 253:5    0   10G  0 lvm  /home
Jun 13 08:41:47 testvm1.both.org lsblk[630581]: sr0            11:0    1 1024M  0 rom
Jun 13 08:41:47 testvm1.both.org systemd[1]: myMonitor.service: Succeeded.
Jun 13 08:41:47 testvm1.both.org systemd[1]: Finished Logs system statistics to the systemd journal.

别忘了检查下计时器和服务的状态。

你在日志里大概至少注意到两件事。第一,你不需要特地做什么来让 myMonitor.service 单元中 ExecStart 触发器产生的 STDOUT 存储到日志里。这都是用 systemd 来运行服务的一部分功能。然而,它确实意味着你需要小心对待服务单元里面执行的脚本和它们能产生多少 STDOUT

第二,定时器并不是精确在每分钟的 :00 秒执行的,甚至每次执行的时间间隔都不是刚好一分钟。这是特意的设计,但是有必要的话可以改变这种行为(如果只是它挑战了你的系统管理员的敏感神经)。

这样设计的初衷是为了防止多个服务在完全相同的时刻被触发。举个例子,你可以用例如 Weekly,Daily 等时间格式。这些快捷写法都被定义为在某一天的 00:00:00 执行。当多个定时器都这样定义的话,有很大可能它们会同时执行。

systemd 定时器被故意设计成在规定时间附近随机波动的时间点触发,以避免同一时间触发。它们在一个时间窗口内半随机触发,时间窗口开始于预设的触发时间,结束于预设时间后一分钟。根据 systemd.timer 的手册页,这个触发时间相对于其他已经定义的定时器单元保持在稳定的位置。你可以在日志条目中看到,定时器在启动后立即触发,然后在每分钟后的 46 或 47 秒触发。

大部分情况下,这种概率抖动的定时器是没事的。当调度类似执行备份的任务,只需要它们在下班时间运行,这样是没问题的。系统管理员可以选择确定的开始时间来确保不和其他任务冲突,例如 01:05:00 这样典型的 cron 作业时间,但是有很大范围的时间值可以满足这一点。在开始时间上的一个分钟级别的随机往往是无关紧要的。

然而,对某些任务来说,精确的触发时间是个硬性要求。对于这类任务,你可以向单元文件的 Timer 块中添加如下声明来指定更高的触发时间跨度精确度(精确到微秒以内):

AccuracySec=1us

时间跨度可用于指定所需的精度,以及定义重复事件或一次性事件的时间跨度。它能识别以下单位:

  • usecusµs
  • msecms
  • secondssecondsecs
  • minutesminuteminm
  • hourshourhrh
  • daysdayd
  • weeksweekw
  • monthsmonthM(定义为 30.44 天)
  • yearsyeary(定义为 365.25 天)

所有 /usr/lib/systemd/system 中的定时器都指定了一个更宽松的时间精度,因为精准时间没那么重要。看看这些系统创建的定时器的时间格式:

[root@testvm1 system]# grep Accur /usr/lib/systemd/system/*timer
/usr/lib/systemd/system/fstrim.timer:AccuracySec=1h
/usr/lib/systemd/system/logrotate.timer:AccuracySec=1h
/usr/lib/systemd/system/logwatch.timer:AccuracySec=12h
/usr/lib/systemd/system/mlocate-updatedb.timer:AccuracySec=24h
/usr/lib/systemd/system/raid-check.timer:AccuracySec=24h
/usr/lib/systemd/system/unbound-anchor.timer:AccuracySec=24h
[root@testvm1 system]#

看下 /usr/lib/systemd/system 目录下部分定时器单元文件的完整内容,看看它们是如何构建的。

在本实验中不必让这个定时器在启动时激活,但下面这个命令可以设置开机自启:

[root@testvm1 system]# systemctl enable myMonitor.timer

你创建的单元文件不需要是可执行的。你同样不需要启用服务,因为它是被定时器触发的。如果你需要的话,你仍然可以在命令行里手动触发该服务单元。尝试一下,然后观察日志。

关于定时器精度、事件时间规格和触发事件的详细信息,请参见 systemd.timer 和 systemd.time 的手册页。

定时器类型

systemd 定时器还有一些在 cron 中找不到的功能,cron 只在确定的、重复的、具体的日期和时间触发。systemd 定时器可以被配置成根据其他 systemd 单元状态发生改变时触发。举个例子,定时器可以配置成在系统开机、启动后,或是某个确定的服务单元激活之后的一段时间被触发。这些被称为单调计时器。“单调”指的是一个持续增长的计数器或序列。这些定时器不是持久的,因为它们在每次启动后都会重置。

表格 1 列出了一些单调定时器以及每个定时器的简短定义,同时有 OnCalendar 定时器,这些不是单调的,它们被用于指定未来有可能重复的某个确定时间。这个信息来自于 systemd.timer 的手册页,有一些不重要的修改。

< 如显示不全,请左右滑动 >

定时器单调性定义
OnActiveSec=X定义了一个与定时器被激活的那一刻相关的定时器。
OnBootSec=X定义了一个与机器启动时间相关的计时器。
OnStartupSec=X定义了一个与服务管理器首次启动相关的计时器。对于系统定时器来说,这个定时器与 OnBootSec= 类似,因为系统服务管理器在机器启动后很短的时间后就会启动。当以在每个用户服务管理器中运行的单元进行配置时,它尤其有用,因为用户的服务管理器通常在首次登录后启动,而不是机器启动后。
OnUnitActiveSec=X定义了一个与将要激活的定时器上次激活时间相关的定时器。
OnUnitInactiveSec=X定义了一个与将要激活的定时器上次停用时间相关的定时器。
OnCalendar=定义了一个有日期事件表达式语法的实时(即时钟)定时器。查看 systemd.time(7) 的手册页获取更多与日历事件表达式相关的语法信息。除此以外,它的语义和 OnActiveSec= 类似。

Table 1: systemd 定时器定义

单调计时器可使用同样的简写名作为它们的时间跨度,即我们之前提到的 AccuracySec 表达式,但是 systemd 将这些名字统一转换成了秒。举个例子,比如你想规定某个定时器在系统启动后五天触发一次事件;它可能看起来像 OnBootSec=5d。如果机器启动于 2020-06-15 09:45:27,这个定时器会在 2020-06-20 09:45:27 或在这之后的一分钟内触发。

日历事件格式

日历事件格式是定时器在所需的重复时间触发的关键。我们开始看下一些 OnCalendar 设置一起使用的格式。

与 crontab 中的格式相比,systemd 及其计时器使用的时间和日历格式风格不同。它比 crontab 更为灵活,而且可以使用类似 at 命令的方式允许模糊的日期和时间。它还应该足够熟悉使其易于理解。

systemd 定时器使用 OnCalendar= 的基础格式是 DOW YYYY-MM-DD HH:MM:SS。DOW(星期几)是选填的,其他字段可以用一个星号(*)来匹配此位置的任意值。所有的日历时间格式会被转换成标准格式。如果时间没有指定,它会被设置为 00:00:00。如果日期没有指定但是时间指定了,那么下次匹配的时间可能是今天或者明天,取决于当前的时间。月份和星期可以使用名称或数字。每个单元都可以使用逗号分隔的列表。单元范围可以在开始值和结束值之间用 .. 指定。

指定日期有一些有趣的选项,波浪号(~)可以指定月份的最后一天或者最后一天之前的某几天。/ 可以用来指定星期几作为修饰符。

这里有几个在 OnCalendar 表达式中使用的典型时间格式例子。

日期事件格式描述
DOW YYYY-MM-DD HH:MM:SS
*-*-* 00:15:30每年每月每天的 0 点 15 分 30 秒
Weekly每个周一的 00:00:00
Mon *-*-* 00:00:00同上
Mon同上
Wed 2020-*-*2020 年每个周三的 00:00:00
Mon..Fri 2021-*-*2021 年的每个工作日(周一到周五)的 00:00:00
2022-6,7,8-1,15 01:15:002022 年 6、7、8 月的 1 到 15 号的 01:15:00
Mon *-05~03每年五月份的下个周一同时也是月末的倒数第三天
Mon..Fri *-08~04任何年份 8 月末的倒数第四天,同时也须是工作日
*-05~03/2五月末的倒数第三天,然后 2 天后再来一次。每年重复一次。注意这个表达式使用了波浪号(~)。
*-05-03/2五月的第三天,然后每两天重复一次直到 5 月底。注意这个表达式使用了破折号(-)。

Table 2: OnCalendar 事件时间格式例子

测试日历格式

systemd 提供了一个绝佳的工具用于检测和测试定时器中日历时间事件的格式。systemd-analyze calendar 工具解析一个时间事件格式,提供标准格式和其他有趣的信息,例如下次“经过”(即匹配)的日期和时间,以及距离下次触发之前大概时间。

首先,看看未来没有时间的日(注意 Next elapseUTC 的时间会根据你当地时区改变):

[student@studentvm1 ~]$ systemd-analyze calendar 2030-06-17
  Original form: 2030-06-17                
Normalized form: 2030-06-17 00:00:00        
    Next elapse: Mon 2030-06-17 00:00:00 EDT
       (in UTC): Mon 2030-06-17 04:00:00 UTC
       From now: 10 years 0 months left    
[root@testvm1 system]#

现在添加一个时间,在这个例子中,日期和时间是当作无关的部分分开解析的:

[root@testvm1 system]# systemd-analyze calendar 2030-06-17 15:21:16
  Original form: 2030-06-17                
Normalized form: 2030-06-17 00:00:00        
    Next elapse: Mon 2030-06-17 00:00:00 EDT
       (in UTC): Mon 2030-06-17 04:00:00 UTC
       From now: 10 years 0 months left    
  Original form: 15:21:16                  
Normalized form: *-*-* 15:21:16            
    Next elapse: Mon 2020-06-15 15:21:16 EDT
       (in UTC): Mon 2020-06-15 19:21:16 UTC
       From now: 3h 55min left              
[root@testvm1 system]#

为了把日期和时间当作一个单元来分析,可以把它们包在引号里。你在定时器单元里 OnCalendar= 时间格式中使用的时候记得把引号去掉,否则会报错:

[root@testvm1 system]# systemd-analyze calendar "2030-06-17 15:21:16"
Normalized form: 2030-06-17 15:21:16        
    Next elapse: Mon 2030-06-17 15:21:16 EDT
       (in UTC): Mon 2030-06-17 19:21:16 UTC
       From now: 10 years 0 months left    
[root@testvm1 system]#

现在我们测试下 Table2 里的例子。我尤其喜欢最后一个:

[root@testvm1 system]# systemd-analyze calendar "2022-6,7,8-1,15 01:15:00"
  Original form: 2022-6,7,8-1,15 01:15:00
Normalized form: 2022-06,07,08-01,15 01:15:00
    Next elapse: Wed 2022-06-01 01:15:00 EDT
       (in UTC): Wed 2022-06-01 05:15:00 UTC
       From now: 1 years 11 months left
[root@testvm1 system]#

让我们看一个例子,这个例子里我们列出了时间表达式的五个经过时间。

[root@testvm1 ~]# systemd-analyze calendar --iterations=5 "Mon *-05~3"
  Original form: Mon *-05~3                
Normalized form: Mon *-05~03 00:00:00      
    Next elapse: Mon 2023-05-29 00:00:00 EDT
       (in UTC): Mon 2023-05-29 04:00:00 UTC
       From now: 2 years 11 months left    
       Iter. #2: Mon 2028-05-29 00:00:00 EDT
       (in UTC): Mon 2028-05-29 04:00:00 UTC
       From now: 7 years 11 months left    
       Iter. #3: Mon 2034-05-29 00:00:00 EDT
       (in UTC): Mon 2034-05-29 04:00:00 UTC
       From now: 13 years 11 months left    
       Iter. #4: Mon 2045-05-29 00:00:00 EDT
       (in UTC): Mon 2045-05-29 04:00:00 UTC
       From now: 24 years 11 months left    
       Iter. #5: Mon 2051-05-29 00:00:00 EDT
       (in UTC): Mon 2051-05-29 04:00:00 UTC
       From now: 30 years 11 months left    
[root@testvm1 ~]#

这些应该为你提供了足够的信息去开始测试你的 OnCalendar 时间格式。systemd-analyze 工具可用于其他有趣的分析,我会在这个系列的下一篇文章来探索这些。

总结

systemd 定时器可以用于执行和 cron 工具相同的任务,但是通过按照日历和单调时间格式去触发事件的方法提供了更多的灵活性。

虽然你为此次实验创建的服务单元通常是由定时器调用的,你也可以随时使用 systemctl start myMonitor.service 命令去触发它。可以在一个定时器中编写多个维护任务的脚本;它们可以是 Bash 脚本或者其他 Linux 程序。你可以通过触发定时器来运行所有的脚本来运行服务,也可以按照需要执行单独的脚本。

我还没有看到任何迹象表明 cron 和 at 将被废弃。我希望这种情况不会发生,因为至少 at 在执行一次性调度任务的时候要比 systemd 定时器容易的多。


用于调度任务的 systemd 定时器

作者: Richard England 译者: LCTT Qian.Sun | 2021-06-25 17:57

systemd 提供定时器有一段时间了,定时器替代了 cron 功能,这一特性值得看看。本文将向你介绍在系统启动后如何使用 systemd 中的定时器来运行任务,并在此后重复运行。这不是对 systemd 的全面讨论,只是对此特性的一个介绍。

快速回顾:cron、anacron 与 systemd

cron 可以以几分钟到几个月或更长时间的粒度调度运行一个任务。设置起来相对简单,它只需要一个配置文件。虽然配置过程有些深奥,但一般用户也可以使用。

然而,如果你的系统在需要执行的时间没有运行,那么 cron 会失败。

anacron 克服了“系统没有运行”的问题。它确保任务将在你的系统再次启动时执行。虽然它旨在给管理员使用,但有些系统允许普通用户访问 anacron。

但是,anacron 的执行频率不能低于每天一次。

cron 和 anacron 都存在执行上下文一致性的问题。必须注意任务运行时有效的环境与测试时使用的环境完全相同。必须提供相同的 shell、环境变量和路径。这意味着测试和调试有时会很困难。

systemd 定时器提供了 cron 和 anacron 二者的优点,允许调度到分钟粒度。确保在系统再次运行时执行任务,即使在预期的执行时间内系统处于关闭状态。它对所有用户都可用。你可以在它将要运行的环境中测试和调试执行。

但是,它的配置更加复杂,至少需要两个配置文件。

如果你的 cron 和 anacron 配置可以很好地为你服务,那么可能没有理由改变。但是 systemd 至少值得研究,因为它可以简化任何当前的 cron/anacron 工作方式。

配置

systemd 定时器执行功能至少需要两个文件。这两个是“定时器单元timer unit”和“服务单元service unit”。(其执行的)“动作”不仅仅是简单的命令,你还需要一个“作业”文件或脚本来执行必要的功能。

定时器单元文件定义调度表,而服务单元文件定义执行的任务。有关的更多详细信息请参考 man systemd.timer 中提供的 .timer 单元。服务单元的详细信息可在 man systemd.service 中找到。

单元文件存放在几个位置(在手册页中有列出)。然而,对于普通用户来说,最容易找到的位置可能是 ~/.config/systemd/user。请注意,这里的 user 是字符串 user

示例

此示例是一个创建用户调度作业而不是(以 root 用户身份运行的)系统调度作业的简单示例。它将消息、日期和时间打印到文件中。

1、首先创建一个执行任务的 shell 脚本。在你的本地 bin 目录中创建它,例如在 ~/bin/schedule-test.sh 中。

创建文件:

touch ~/bin/schedule-test.sh

然后将以下内容添加到你刚刚创建的文件中:

#!/bin/sh
echo "This is only a test: $(date)" >> "$HOME/schedule-test-output.txt"

记住赋予你的 shell 脚本执行权限。

2、创建 .service 单元调用上面的脚本。在以下位置创建目录与文件:~/.config/systemd/user/schedule-test.service

[Unit]
Description=A job to test the systemd scheduler
[Service]
Type=simple
ExecStart=/home/<user>/bin/schedule-test.sh
[Install]
WantedBy=default.target

请注意 <user> 应该是你的家目录地址,但是单元文件路径名中的 user 实际上是字符串 user

ExecStart 应该提供一个没有变量的绝对地址。例外情况是,对于用户单元文件,你可以用 %h 替换 $HOME。换句话说,你可以使用:

ExecStart=%h/bin/schedule-test.sh

这仅用于用户单元文件,而不适用于系统服务,因为在系统环境中运行时 %h 总是返回 /root。其他特殊符号可在 man systemd.unitSPECIFIERS 中找到。因为它超出了本文的范围,所以这就是我们目前需要了解的关于特殊符号的全部内容。

3、创建一个 .timer 单元文件,该文件实际上调度你创建的 .service 单元文件。在 .service 单元文件相同位置创建它:~/.config/systemd/user/schedule-test.timer。请注意,文件名仅在扩展名上有所不同,例如一个是 .service,一个是 .timer

[Unit]
Description=Schedule a message every 1 minute
RefuseManualStart=no  # Allow manual starts
RefuseManualStop=no   # Allow manual stops
[Timer]
#Execute job if it missed a run due to machine being off
Persistent=true
#Run 120 seconds after boot for the first time
OnBootSec=120
#Run every 1 minute thereafter
OnUnitActiveSec=60
#File describing job to execute
Unit=schedule-test.service
[Install]
WantedBy=timers.target

请注意,这个 .timer 单元文件使用了 OnUnitActiveSec 来指定调度表。OnCalendar 选项更加灵活。例如:

# run on the minute of every minute every hour of every day
 OnCalendar=*-*-* *:*:00
# run on the hour of every hour of every day
 OnCalendar=*-*-* *:00:00
# run every day
 OnCalendar=*-*-* 00:00:00
# run 11:12:13 of the first or fifth day of any month of the year
# 2012, but only if that day is a Thursday or Friday
 OnCalendar=Thu,Fri 2012-*-1,5 11:12:13

有关 OnCalendar 的更多信息参见 [这里](https://www.freedesktop.org/software/systemd/man/systemd.time.html#Calendar Events)。

4、所有的部件都已就位,但你应该进行测试,以确保一切正常。首先,启用该用户服务:

$ systemctl --user enable schedule-test.service

这将导致类似如下的输出:

Created symlink /home/<user>/.config/systemd/user/default.target.wants/schedule-test.service → /home/<user>/.config/systemd/user/schedule-test.service.

现在执行测试工作:

$ systemctl --user start schedule-test.service

检查你的输出文件($HOME/schedule-test-output.txt),确保你的脚本运行正常。应该只有一个条目,因为我们还没有启动定时器。必要时进行调试。如果你需要更改 .service 单元文件,而不是更改它调用的 shell 脚本,请不要忘记再次启用该服务。

5、一旦作业正常运行,通过为服务启用、启动用户定时器来实时调度作业:

$ systemctl --user enable schedule-test.timer
$ systemctl --user start schedule-test.timer

请注意,你已经在上面的步骤 4 中启动、启用了服务,因此只需要为它启用、启动定时器。

enable 命令会产生如下输出:

Created symlink /home/<user>/.config/systemd/user/timers.target.wants/schedule-test.timer → /home/<user>/.config/systemd/user/schedule-test.timer.

start 命令将只是返回命令行界面提示符。

其他操作

你可以检查和监控服务。如果你从系统服务收到错误,下面的第一个命令特别有用:

$ systemctl --user status schedule-test
$ systemctl --user list-unit-files

手动停止服务:

$ systemctl --user stop schedule-test.service

永久停止并禁用定时器和服务,重新加载守护程序配置并重置任何失败通知:

$ systemctl --user stop schedule-test.timer
$ systemctl --user disable schedule-test.timer
$ systemctl --user stop schedule-test.service
$ systemctl --user disable schedule-test.service
$ systemctl --user daemon-reload
$ systemctl --user reset-failed

总结

本文以 systemd 定时器为出发点,但是 systemd 的内容远不止于此。这篇文章应该为你提供一个基础。你可以从 Fedora Magazine systemd 系列 开始探索更多。

参考

更多阅读:


在 systemd 中使用控制组管理资源

作者: David Both 译者: LCTT YungeGuo | 2021-10-14 11:46

控制组可以按照应用管理资源,而不是按照组成应用的单个进程。

作为一个系统管理员,没有事情比意外地耗尽计算资源让我更觉得沮丧。我曾不止一次填满了一个分区的所有可用磁盘空间、耗尽内存、以及没有足够的 CPU 时间在合理的时间内处理我的任务。资源管理是系统管理员最重要的工作之一。

资源管理的关键是保证所有的进程能够相对公平的访问需要的系统资源。资源管理还包括确保在需要时添加内存、硬盘驱动器空间、还有 CPU 处理能力;或者在无法添加时限制资源的使用。此外,应该阻止独占系统资源的用户,无论其是否有意。

系统管理员可以通过一些工具监控和管理不同的系统资源。例如,top 和类似的工具允许你监控内存、I/O、存储(磁盘、SSD 等)、网络、交换空间、CPU 的用量等。这些工具,尤其是那些以 CPU 为中心的工具,大部分基于以运行的进程为基本单位进行控制的模型。它们最多只是提供了一种方式来调整 nice 数字,从而修改优先级,或者杀死一个运行的进程。(要了解 nice 数字的信息,查看 使用 Glances 监控 Linux 和 Windows 主机)。

SystemV 环境中基于传统的资源管理的其他工具,由 /etc/security/limits.conf 文件和 /etc/security/limits.d 中的本地配置文件控制。资源可以按照用户或组以一种相对粗糙但实用的方式限制。可以管理的资源包括内存的各个方面、每日的总 CPU 时间、数据总量、优先级、nice 数字、并发登录的数量、进程数、文件大小的最大值等。

使用控制组管理进程

systemd 和 SystemV 之间的一个主要差异是管理进程的方式。SystemV 将每个进程视作一个独立的实体。systemd 将相关的进程集中到一个控制组,简写做 cgroup,并将控制组作为一个整体管理系统资源。这意味着资源能够基于应用管理,而不是由组成应用的各个进程来管理。

控制组的控制单元称作切片单元slice unit。切片是允许 systemd 以树状格式控制程序次序,从而简化管理的概念化。

查看控制组

我将从一些允许你查看不同类型控制组信息的命令开始。 systemctl status <service> 命令显示一个特定服务的切片信息,包括服务的切片。这个例子展示了 at 守护进程:

[root@testvm1 ~]# systemctl status atd.service
● atd.service - Deferred execution scheduler
     Loaded: loaded (/usr/lib/systemd/system/atd.service; enabled; vendor preset: enabled)
     Active: active (running) since Wed 2020-09-23 12:18:24 EDT; 1 day 3h ago
       Docs: man:atd(8)
   Main PID: 1010 (atd)
      Tasks: 1 (limit: 14760)
     Memory: 440.0K
        CPU: 5ms
     CGroup: /system.slice/atd.service
             └─1010 /usr/sbin/atd -f
Sep 23 12:18:24 testvm1.both.org systemd[1]: Started Deferred execution scheduler.
[root@testvm1 ~]#

这是一个我感到 systemd 比 SystemV 和旧的初始化程序更好用的原因的绝佳示例。这里的信息远比 SystemV 能够提供的丰富。CGroup 项包括的层级结构中,system.slice 是 systemd(PID 1),atd.service 在下一层,是 system.slice 的一部分。CGroup 项的第二行还显示了进程 ID(PID)和启动守护进程使用的命令。

systemctl 命令可以列出多个控制组项,--all 参数列出所有的切片,包括当前没有激活的切片:

[root@testvm1 ~]# systemctl -t slice --all
  UNIT                             LOAD   ACTIVE   SUB    DESCRIPTION                    
  -.slice                          loaded active   active Root Slice                      
  system-getty.slice               loaded active   active system-getty.slice              
  system-lvm2\x2dpvscan.slice      loaded active   active system-lvm2\x2dpvscan.slice    
  system-modprobe.slice            loaded active   active system-modprobe.slice          
  system-sshd\x2dkeygen.slice      loaded active   active system-sshd\x2dkeygen.slice    
  system-systemd\x2dcoredump.slice loaded inactive dead   system-systemd\x2dcoredump.slice
  system-systemd\x2dfsck.slice     loaded active   active system-systemd\x2dfsck.slice    
  system.slice                     loaded active   active System Slice                    
  user-0.slice                     loaded active   active User Slice of UID 0            
  user-1000.slice                  loaded active   active User Slice of UID 1000          
  user.slice                       loaded active   active User and Session Slice          
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.
11 loaded units listed.
To show all installed unit files use 'systemctl list-unit-files'.
[root@testvm1 ~]#

关于这个数据,第一个需要注意的是数据显示了 UID 0(root)和 UID 1000 的用户切片,UID 1000 是我登录的用户。这里列出了组成每个切片的切片部分,而不是服务。还说明了每个用户登录时都会为其创建一个切片,这为将一个用户的所有任务作为单个控制组项进行管理提供了一种方式。

探索控制组的层次结构

目前为止一切顺利,但是控制组是分层的,所有的服务单元作为其中一个控制组的成员运行。要查看这个层次结构很简单,使用一个旧命令和 systemd 的一个新命令即可。

ps 命令可以用于映射进程的和其所处的控制组层次。注意使用 ps 命令时需要指明想要的数据列。我大幅削减了下面命令的输出数量,但是试图保留足够的数据,以便你能够对自己系统上的输出有所感受:

[root@testvm1 ~]# ps xawf -eo pid,user,cgroup,args
    PID USER     CGROUP                      COMMAND
      2 root     -                           [kthreadd]
      3 root     -                            \_ [rcu_gp]
      4 root     -                            \_ [rcu_par_gp]
      6 root     -                            \_ [kworker/0:0H-kblockd]
      9 root     -                            \_ [mm_percpu_wq]
     10 root     -                            \_ [ksoftirqd/0]
     11 root     -                            \_ [rcu_sched]
     12 root     -                            \_ [migration/0]
     13 root     -                            \_ [cpuhp/0]
     14 root     -                            \_ [cpuhp/1]
<删节>
 625406 root     -                            \_ [kworker/3:0-ata_sff]
 625409 root     -                            \_ [kworker/u8:0-events_unbound]
      1 root     0::/init.scope              /usr/lib/systemd/systemd --switched-root --system --deserialize 30
    588 root     0::/system.slice/systemd-jo /usr/lib/systemd/systemd-journald
    599 root     0::/system.slice/systemd-ud /usr/lib/systemd/systemd-udevd
    741 root     0::/system.slice/auditd.ser /sbin/auditd
    743 root     0::/system.slice/auditd.ser  \_ /usr/sbin/sedispatch
    764 root     0::/system.slice/ModemManag /usr/sbin/ModemManager
    765 root     0::/system.slice/NetworkMan /usr/sbin/NetworkManager --no-daemon
    767 root     0::/system.slice/irqbalance /usr/sbin/irqbalance --foreground
    779 root     0::/system.slice/mcelog.ser /usr/sbin/mcelog --ignorenodev --daemon --foreground
    781 root     0::/system.slice/rngd.servi /sbin/rngd -f
    782 root     0::/system.slice/rsyslog.se /usr/sbin/rsyslogd -n
<删节>
    893 root     0::/system.slice/sshd.servi sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
   1130 root     0::/user.slice/user-0.slice  \_ sshd: root [priv]
   1147 root     0::/user.slice/user-0.slice  |   \_ sshd: root@pts/0
   1148 root     0::/user.slice/user-0.slice  |       \_ -bash
   1321 root     0::/user.slice/user-0.slice  |           \_ screen
   1322 root     0::/user.slice/user-0.slice  |               \_ SCREEN
   1323 root     0::/user.slice/user-0.slice  |                   \_ /bin/bash
 498801 root     0::/user.slice/user-0.slice  |                   |   \_ man systemd.resource-control
 498813 root     0::/user.slice/user-0.slice  |                   |       \_ less
   1351 root     0::/user.slice/user-0.slice  |                   \_ /bin/bash
 123293 root     0::/user.slice/user-0.slice  |                   |   \_ man systemd.slice
 123305 root     0::/user.slice/user-0.slice  |                   |       \_ less
   1380 root     0::/user.slice/user-0.slice  |                   \_ /bin/bash
 625412 root     0::/user.slice/user-0.slice  |                   |   \_ ps xawf -eo pid,user,cgroup,args
 625413 root     0::/user.slice/user-0.slice  |                   |   \_ less
 246795 root     0::/user.slice/user-0.slice  |                   \_ /bin/bash
 625338 root     0::/user.slice/user-0.slice  |                       \_ /usr/bin/mc -P /var/tmp/mc-root/mc.pwd.246795
 625340 root     0::/user.slice/user-0.slice  |                           \_ bash -rcfile .bashrc
   1218 root     0::/user.slice/user-1000.sl  \_ sshd: dboth [priv]
   1233 dboth    0::/user.slice/user-1000.sl      \_ sshd: dboth@pts/1
   1235 dboth    0::/user.slice/user-1000.sl          \_ -bash
<删节>
   1010 root     0::/system.slice/atd.servic /usr/sbin/atd -f
   1011 root     0::/system.slice/crond.serv /usr/sbin/crond -n
   1098 root     0::/system.slice/lxdm.servi /usr/sbin/lxdm-binary
   1106 root     0::/system.slice/lxdm.servi  \_ /usr/libexec/Xorg -background none :0 vt01 -nolisten tcp -novtswitch -auth /var/run/lxdm/lxdm-:0.auth
 370621 root     0::/user.slice/user-1000.sl  \_ /usr/libexec/lxdm-session
 370631 dboth    0::/user.slice/user-1000.sl      \_ xfce4-session
 370841 dboth    0::/user.slice/user-1000.sl          \_ /usr/bin/ssh-agent /bin/sh -c exec -l bash -c "/usr/bin/startxfce4"
 370911 dboth    0::/user.slice/user-1000.sl          \_ xfwm4 --display :0.0 --sm-client-id 2dead44ab-0b4d-4101-bca4-e6771f4a8ac2
 370930 dboth    0::/user.slice/user-1000.sl          \_ xfce4-panel --display :0.0 --sm-client-id 2ce38b8ef-86fd-4189-ace5-deec1d0e0952
 370942 dboth    0::/user.slice/user-1000.sl          |   \_ /usr/lib64/xfce4/panel/wrapper-2.0 /usr/lib64/xfce4/panel/plugins/libsystray.so 6 23068680 systr
ay Notification Area Area where notification icons appear
 370943 dboth    0::/user.slice/user-1000.sl          |   \_ /usr/lib64/xfce4/panel/wrapper-2.0 /usr/lib64/xfce4/panel/plugins/libpulseaudio-plugin.so 8 2306
8681 pulseaudio PulseAudio Plugin Adjust the audio volume of the PulseAudio sound system
 370944 dboth    0::/user.slice/user-1000.sl          |   \_ /usr/lib64/xfce4/panel/wrapper-2.0 /usr/lib64/xfce4/panel/plugins/libxfce4powermanager.so 9 2306
8682 power-manager-plugin Power Manager Plugin Display the battery levels of your devices and control the brightness of your display
 370945 dboth    0::/user.slice/user-1000.sl          |   \_ /usr/lib64/xfce4/panel/wrapper-2.0 /usr/lib64/xfce4/panel/plugins/libnotification-plugin.so 10 2
3068683 notification-plugin Notification Plugin Notification plugin for the Xfce panel
 370948 dboth    0::/user.slice/user-1000.sl          |   \_ /usr/lib64/xfce4/panel/wrapper-2.0 /usr/lib64/xfce4/panel/plugins/libactions.so 14 23068684 acti
ons Action Buttons Log out, lock or other system actions
 370934 dboth    0::/user.slice/user-1000.sl          \_ Thunar --sm-client-id 2cfc809d8-4e1d-497a-a5c5-6e4fa509c3fb --daemon
 370939 dboth    0::/user.slice/user-1000.sl          \_ xfdesktop --display :0.0 --sm-client-id 299be0608-4dca-4055-b4d6-55ec6e73a324
 370962 dboth    0::/user.slice/user-1000.sl          \_ nm-applet
<删节>

你可以使用 systemd-cgls 命令查看整个层次结构,这个命令不需要任何的复杂参数,更加简单。

我也大幅缩短了这个树状结构,但是保留了足够多的输出,以便你能够了解在自己的系统上执行这个命令时应该看到的数据总量和条目类型。我在我的一个虚拟机上执行了这个命令,输出大概有 200 行;我的主要工作站的输出大概有 250 行。

[root@testvm1 ~]# systemd-cgls
Control group /:
-.slice
├─user.slice
│ ├─user-0.slice
│ │ ├─session-1.scope
│ │ │ ├─  1130 sshd: root [priv]
│ │ │ ├─  1147 sshd: root@pts/0
│ │ │ ├─  1148 -bash
│ │ │ ├─  1321 screen
│ │ │ ├─  1322 SCREEN
│ │ │ ├─  1323 /bin/bash
│ │ │ ├─  1351 /bin/bash
│ │ │ ├─  1380 /bin/bash
│ │ │ ├─123293 man systemd.slice
│ │ │ ├─123305 less
│ │ │ ├─246795 /bin/bash
│ │ │ ├─371371 man systemd-cgls
│ │ │ ├─371383 less
│ │ │ ├─371469 systemd-cgls
│ │ │ └─371470 less
│ │ └─user@0.service …
│ │   ├─dbus-broker.service
│ │   │ ├─1170 /usr/bin/dbus-broker-launch --scope user
│ │   │ └─1171 dbus-broker --log 4 --controller 12 --machine-id 3bccd1140fca488187f8a1439c832f07 --max-bytes 100000000000000 --max-fds 25000000000000 --max->
│ │   ├─gvfs-daemon.service
│ │   │ └─1173 /usr/libexec/gvfsd
│ │   └─init.scope
│ │     ├─1137 /usr/lib/systemd/systemd --user
│ │     └─1138 (sd-pam)
│ └─user-1000.slice
│   ├─user@1000.service …
│   │ ├─dbus\x2d:1.2\x2dorg.xfce.Xfconf.slice
│   │ │ └─dbus-:1.2-org.xfce.Xfconf@0.service
│   │ │   └─370748 /usr/lib64/xfce4/xfconf/xfconfd
│   │ ├─dbus\x2d:1.2\x2dca.desrt.dconf.slice
│   │ │ └─dbus-:1.2-ca.desrt.dconf@0.service
│   │ │   └─371262 /usr/libexec/dconf-service
│   │ ├─dbus-broker.service
│   │ │ ├─1260 /usr/bin/dbus-broker-launch --scope user
│   │ │ └─1261 dbus-broker --log 4 --controller 11 --machine-id
<删节>
│   │ └─gvfs-mtp-volume-monitor.service
│   │   └─370987 /usr/libexec/gvfs-mtp-volume-monitor
│   ├─session-3.scope
│   │ ├─1218 sshd: dboth [priv]
│   │ ├─1233 sshd: dboth@pts/1
│   │ └─1235 -bash
│   └─session-7.scope
│     ├─370621 /usr/libexec/lxdm-session
│     ├─370631 xfce4-session
│     ├─370805 /usr/bin/VBoxClient --clipboard
│     ├─370806 /usr/bin/VBoxClient --clipboard
│     ├─370817 /usr/bin/VBoxClient --seamless
│     ├─370818 /usr/bin/VBoxClient --seamless
│     ├─370824 /usr/bin/VBoxClient --draganddrop
│     ├─370825 /usr/bin/VBoxClient --draganddrop
│     ├─370841 /usr/bin/ssh-agent /bin/sh -c exec -l bash -c "/usr/bin/startxfce4"
│     ├─370910 /bin/gpg-agent --sh --daemon --write-env-file /home/dboth/.cache/gpg-agent-info
│     ├─370911 xfwm4 --display :0.0 --sm-client-id 2dead44ab-0b4d-4101-bca4-e6771f4a8ac2
│     ├─370923 xfsettingsd --display :0.0 --sm-client-id 261b4a437-3029-461c-9551-68c2c42f4fef
│     ├─370930 xfce4-panel --display :0.0 --sm-client-id 2ce38b8ef-86fd-4189-ace5-deec1d0e0952
│     ├─370934 Thunar --sm-client-id 2cfc809d8-4e1d-497a-a5c5-6e4fa509c3fb --daemon
│     ├─370939 xfdesktop --display :0.0 --sm-client-id 299be0608-4dca-4055-b4d6-55ec6e73a324
<删节>
└─system.slice
  ├─rngd.service
  │ └─1650 /sbin/rngd -f
  ├─irqbalance.service
  │ └─1631 /usr/sbin/irqbalance --foreground
  ├─fprintd.service
  │ └─303383 /usr/libexec/fprintd
  ├─systemd-udevd.service
  │ └─956 /usr/lib/systemd/systemd-udevd
<删节>
  ├─systemd-journald.service
  │ └─588 /usr/lib/systemd/systemd-journald
  ├─atd.service
  │ └─1010 /usr/sbin/atd -f
  ├─system-dbus\x2d:1.10\x2dorg.freedesktop.problems.slice
  │ └─dbus-:1.10-org.freedesktop.problems@0.service
  │   └─371197 /usr/sbin/abrt-dbus -t133
  ├─sshd.service
  │ └─893 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
  ├─vboxservice.service
  │ └─802 /usr/sbin/VBoxService -f
  ├─crond.service
  │ └─1011 /usr/sbin/crond -n
  ├─NetworkManager.service
  │ └─765 /usr/sbin/NetworkManager --no-daemon
  ├─switcheroo-control.service
  │ └─787 /usr/libexec/switcheroo-control
 <删节>

这个树状视图显示了所有的用户和系统切片,以及每个控制组内正在运行的服务和程序。注意叫作 scope(范围)的单元,它将相关的程序组成一个管理单元,在上面列出的结果中就是 user-1000.sliceuser-1000.slice/session-7.scope 控制组包含了 GUI 桌面程序层次结构,以 LXDM 显示管理器会话和其所有的子任务开始,包括像 Bash 命令行解释器和 Thunar GUI 文件管理器之类的程序。

配置文件中不定义范围单元,而是作为启动相关程序组的结果程序化生成的。范围单元不创建或启动作为控制组的组成部分运行的进程。范围内的所有进程都是平等的,没有内部的层次结构。一个范围的生命周期在第一个进程创建时开始,在最后一个进程销毁时结束。

在你的桌面打开多个窗口,比如终端模拟器、LibreOffice、或者任何你想打开的,然后切换到一个可用的虚拟控制台,启动类似 topMidnight Commander 的程序。在主机运行 systemd-cgls 命令,留意整体的层次结构和范围单元。

systemd-cgls 命令提供的控制组层次结构表示(以及组成控制组单元的细节),比我见过的其他任何指令都要完整。和 ps 命令提供的输出相比,我喜欢 systemd-cgls 命令更简洁的树形表示。

其他资源

互联网上充斥着大量关于 systemd 的信息,但大部分都简短生硬、愚钝、甚至令人误解。除了本文提到的资源,下面的网页提供了关于 systemd 启动更详细可靠的信息。自从我开始这一系列的文章来反映我所做的研究以来,这个的列表已经变长了。


使用 restic 和 systemd 自动备份

作者: Link Dupont 译者: LCTT geekpi | 2019-05-19 23:19

及时备份很重要。即使在 Fedora Magazine 中,备份软件 也是一个常见的讨论话题。本文演示了如何仅使用 systemd 以及 restic 来自动备份。

有关 restic 的介绍,请查看我们的文章在 Fedora 上使用 restic 进行加密备份。然后继续阅读以了解更多详情。

为了自动创建快照以及清理数据,需要运行两个 systemd 服务。第一个运行备份命令的服务需要以常规频率运行。第二个服务负责数据清理。

如果你根本不熟悉 systemd,那么这是个很好的学习机会。查看 Magazine 上关于 systemd 的系列文章,从单元文件的这个入门开始:

如果你还没有安装 restic,请注意它在官方的 Fedora 仓库中。要安装它,请带上 sudo 运行此命令:

$ sudo dnf install restic

备份

首先,创建 ~/.config/systemd/user/restic-backup.service。将下面的文本复制并粘贴到文件中以获得最佳效果。

[Unit]
Description=Restic backup service
[Service]
Type=oneshot
ExecStart=restic backup --verbose --one-file-system --tag systemd.timer $BACKUP_EXCLUDES $BACKUP_PATHS
ExecStartPost=restic forget --verbose --tag systemd.timer --group-by "paths,tags" --keep-daily $RETENTION_DAYS --keep-weekly $RETENTION_WEEKS --keep-monthly $RETENTION_MONTHS --keep-yearly $RETENTION_YEARS
EnvironmentFile=%h/.config/restic-backup.conf

此服务引用环境文件来加载密钥(例如 RESTIC_PASSWORD)。创建 ~/.config/restic-backup.conf。复制并粘贴以下内容以获得最佳效果。此示例使用 BackBlaze B2 存储。请相应地调整 ID、密钥、仓库和密码值。

BACKUP_PATHS="/home/rupert"
BACKUP_EXCLUDES="--exclude-file /home/rupert/.restic_excludes --exclude-if-present .exclude_from_backup"
RETENTION_DAYS=7
RETENTION_WEEKS=4
RETENTION_MONTHS=6
RETENTION_YEARS=3
B2_ACCOUNT_ID=XXXXXXXXXXXXXXXXXXXXXXXXX
B2_ACCOUNT_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
RESTIC_REPOSITORY=b2:XXXXXXXXXXXXXXXXXX:/
RESTIC_PASSWORD=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

现在已安装该服务,请重新加载 systemd:systemctl -user daemon-reload。尝试手动运行该服务以创建备份:systemctl -user start restic-backup

因为该服务类型是一次性,它将运行一次并退出。验证服务运行并根据需要创建快照后,设置计时器以定期运行此服务。例如,要每天运行 restic-backup.service,请按如下所示创建 ~/.config/systemd/user/restic-backup.timer。再次复制并粘贴此文本:

[Unit]
Description=Backup with restic daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target

运行以下命令启用:

$ systemctl --user enable --now restic-backup.timer

清理

虽然主服务运行 forget 命令仅保留保留策略中的快照,但实际上并未从 restic 仓库中删除数据。 prune 命令检查仓库和当前快照,并删除与快照无关的所有数据。由于 prune 可能是一个耗时的过程,因此无需在每次运行备份时运行。这是第二个服务和计时器的场景。首先,通过复制和粘贴此文本来创建文件 ~/.config/systemd/user/restic-prune.service

[Unit]
Description=Restic backup service (data pruning)
[Service]
Type=oneshot
ExecStart=restic prune
EnvironmentFile=%h/.config/restic-backup.conf

与主 restic-backup.service 服务类似,restic-prune 也是一次性服务,并且可以手动运行。设置完服务后,创建 ~/.config/systemd/user/restic-prune.timer 并启用相应的计时器:

[Unit]
Description=Prune data from the restic repository monthly
[Timer]
OnCalendar=monthly
Persistent=true
[Install]
WantedBy=timers.target

就是这些了!restic 将会每日运行并按月清理数据。


使用 systemd 作为问题定位工具

作者: David Both 译者: LCTT tt67wq | 2021-06-02 21:43

虽然 systemd 并非真正的故障定位工具,但其输出中的信息为解决问题指明了方向。

没有人会认为 systemd 是一个故障定位工具,但当我的 web 服务器遇到问题时,我对 systemd 和它的一些功能的不断了解帮助我找到并规避了问题。

我遇到的问题是这样,我的服务器 yorktown 为我的家庭办公网络提供名称服务 、DHCP、NTP、HTTPD 和 SendMail 邮件服务,它在正常启动时未能启动 Apache HTTPD 守护程序。在我意识到它没有运行之后,我不得不手动启动它。这个问题已经持续了一段时间,我最近才开始尝试去解决它。

你们中的一些人会说,systemd 本身就是这个问题的原因,根据我现在了解的情况,我同意你们的看法。然而,我在使用 SystemV 时也遇到了类似的问题。(在本系列文章的 第一篇 中,我探讨了围绕 systemd 作为旧有 SystemV 启动程序和启动脚本的替代品所产生的争议。如果你有兴趣了解更多关于 systemd 的信息,也可以阅读 第二篇第三篇 文章。)没有完美的软件,systemd 和 SystemV 也不例外,但 systemd 为解决问题提供的信息远远多于 SystemV。

确定问题所在

找到这个问题根源的第一步是确定 httpd 服务的状态:

[root@yorktown ~]# systemctl status httpd
● httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
   Active: failed (Result: exit-code) since Thu 2020-04-16 11:54:37 EDT; 15min ago
     Docs: man:httpd.service(8)
  Process: 1101 ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND (code=exited, status=1/FAILURE)
 Main PID: 1101 (code=exited, status=1/FAILURE)
   Status: "Reading configuration..."
      CPU: 60ms
Apr 16 11:54:35 yorktown.both.org systemd[1]: Starting The Apache HTTP Server...
Apr 16 11:54:37 yorktown.both.org httpd[1101]: (99)Cannot assign requested address: AH00072: make_sock: could not bind to address 192.168.0.52:80
Apr 16 11:54:37 yorktown.both.org httpd[1101]: no listening sockets available, shutting down
Apr 16 11:54:37 yorktown.both.org httpd[1101]: AH00015: Unable to open logs
Apr 16 11:54:37 yorktown.both.org systemd[1]: httpd.service: Main process exited, code=exited, status=1/FAILURE
Apr 16 11:54:37 yorktown.both.org systemd[1]: httpd.service: Failed with result 'exit-code'.
Apr 16 11:54:37 yorktown.both.org systemd[1]: Failed to start The Apache HTTP Server.
[root@yorktown ~]#

这种状态信息是 systemd 的功能之一,我觉得比 SystemV 提供的任何功能都要有用。这里的大量有用信息使我很容易得出逻辑性的结论,让我找到正确的方向。我从旧的 chkconfig 命令中得到的是服务是否在运行,以及如果它在运行的话,进程 ID(PID)是多少。这可没多大帮助。

该状态报告中的关键条目显示,HTTPD 不能与 IP 地址绑定,这意味着它不能接受传入的请求。这表明网络启动速度不够快,因为 IP 地址还没有设置好,所以 HTTPD 服务还没有准备好与 IP 地址绑定。这是不应该发生的,所以我查看了我的网络服务的 systemd 启动配置文件;在正确的 afterrequires 语句下,所有这些似乎都没问题。下面是我服务器上的 /lib/systemd/system/httpd.service 文件:

# Modifying this file in-place is not recommended, because changes 
# will be overwritten during package upgrades.  To customize the 
# behaviour, run "systemctl edit httpd" to create an override unit.
# For example, to pass additional options (such as -D definitions) to 
# the httpd binary at startup, create an override unit (as is done by                             
# systemctl edit) and enter the following:                                           
#    [Service]
#    Environment=OPTIONS=-DMY_DEFINE             
[Unit]                                               
Description=The Apache HTTP Server
Wants=httpd-init.service
After=network.target remote-fs.target nss-lookup.target httpd-init.service
Documentation=man:httpd.service(8)
[Service]
Type=notify
Environment=LANG=C
ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND
ExecReload=/usr/sbin/httpd $OPTIONS -k graceful
# Send SIGWINCH for graceful stop
KillSignal=SIGWINCH
KillMode=mixed
PrivateTmp=true
[Install]
WantedBy=multi-user.target

httpd.service 单元文件明确规定,它应该在 network.targethttpd-init.service(以及其他)之后加载。我试着用 systemctl list-units 命令找到所有这些服务,并在结果数据流中搜索它们。所有这些服务都存在,应该可以确保在设置网络 IP 地址之前,httpd 服务没有加载。

第一个解决方案

在互联网上搜索了一下,证实其他人在 httpd 和其他服务也遇到了类似的问题。这似乎是由于其中一个所需的服务向 systemd 表示它已经完成了启动,但实际上它却启动了一个尚未完成的子进程。通过更多搜索,我想到了一个规避方法。

我搞不清楚为什么花了这么久才把 IP 地址分配给网卡。所以我想,如果我可以将 HTTPD 服务的启动推迟合理的一段时间,那么 IP 地址就会在那个时候分配。

幸运的是,上面的 /lib/systemd/system/httpd.service 文件提供了一些方向。虽然它说不要修改它,但是它还是指出了如何操作:使用 systemctl edit httpd 命令,它会自动创建一个新文件(/etc/systemd/system/httpd.service.d/override.conf)并打开 GNU Nano 编辑器(如果你对 Nano 不熟悉,一定要看一下 Nano 界面底部的提示)。

在新文件中加入以下代码并保存:

[root@yorktown ~]# cd /etc/systemd/system/httpd.service.d/
[root@yorktown httpd.service.d]# ll
total 4
-rw-r--r-- 1 root root 243 Apr 16 11:43 override.conf
[root@yorktown httpd.service.d]# cat override.conf
# Trying to delay the startup of httpd so that the network is
# fully up and running so that httpd can bind to the correct
# IP address
#
# By David Both, 2020-04-16
[Service]
ExecStartPre=/bin/sleep 30

这个覆盖文件的 [Service] 段有一行代码,将 HTTPD 服务的启动时间推迟了 30 秒。下面的状态命令显示了等待时间里的服务状态:

[root@yorktown ~]# systemctl status httpd
● httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
  Drop-In: /etc/systemd/system/httpd.service.d
           └─override.conf
           /usr/lib/systemd/system/httpd.service.d
           └─php-fpm.conf
   Active: activating (start-pre) since Thu 2020-04-16 12:14:29 EDT; 28s ago
     Docs: man:httpd.service(8)
Cntrl PID: 1102 (sleep)
    Tasks: 1 (limit: 38363)
   Memory: 260.0K
      CPU: 2ms
   CGroup: /system.slice/httpd.service
           └─1102 /bin/sleep 30
Apr 16 12:14:29 yorktown.both.org systemd[1]: Starting The Apache HTTP Server...
Apr 16 12:15:01 yorktown.both.org systemd[1]: Started The Apache HTTP Server.
[root@yorktown ~]#

这个命令显示了 30 秒延迟过后 HTTPD 服务的状态。该服务已经启动并正常运行。

[root@yorktown ~]# systemctl status httpd
● httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
  Drop-In: /etc/systemd/system/httpd.service.d
           └─override.conf
           /usr/lib/systemd/system/httpd.service.d
           └─php-fpm.conf
   Active: active (running) since Thu 2020-04-16 12:15:01 EDT; 1min 18s ago
     Docs: man:httpd.service(8)
  Process: 1102 ExecStartPre=/bin/sleep 30 (code=exited, status=0/SUCCESS)
 Main PID: 1567 (httpd)
   Status: "Total requests: 0; Idle/Busy workers 100/0;Requests/sec: 0; Bytes served/sec:   0 B/sec"
    Tasks: 213 (limit: 38363)
   Memory: 21.8M
      CPU: 82ms
   CGroup: /system.slice/httpd.service
           ├─1567 /usr/sbin/httpd -DFOREGROUND
           ├─1569 /usr/sbin/httpd -DFOREGROUND
           ├─1570 /usr/sbin/httpd -DFOREGROUND
           ├─1571 /usr/sbin/httpd -DFOREGROUND
           └─1572 /usr/sbin/httpd -DFOREGROUND
Apr 16 12:14:29 yorktown.both.org systemd[1]: Starting The Apache HTTP Server...
Apr 16 12:15:01 yorktown.both.org systemd[1]: Started The Apache HTTP Server.

我本来可以实验下更短的延迟时间是否也能奏效,但是我的系统并不用那么严格,所以我觉得不这样做。目前系统的工作状态很可靠,所以我很高兴。

因为我收集了所有这些信息,我将其作为 Bug1825554 报告给红帽 Bugzilla。我相信报告 Bug 比抱怨 Bug 更有有用。

更好的解决方案

把这个问题作为 bug 上报几天后,我收到了回复,表示 systemd 只是一个管理工具,如果 httpd 需要在满足某些要求之后被拉起,需要在单元文件中表达出来。这个回复指引我去查阅 httpd.service 的手册页。我希望我能早点发现这个,因为它是比我自己想出的更优秀的解决方案。这种方案明确的针对了前置目标单元,而不仅仅是随机延迟。

来自 httpd.service 手册页:

在启动时开启服务

httpd.servicehttpd.socket 单元默认是 禁用 的。为了在启动阶段开启 httpd 服务,执行:systemctl enable httpd.service。在默认配置中,httpd 守护进程会接受任何配置好的 IPv4 或 IPv6 地址的 80 口上的连接(如果安装了 mod_ssl,就会接受 443 端口上的 TLS 连接)。

如果 httpd 被配置成依赖任一特定的 IP 地址(比如使用 Listen 指令),该地址可能只在启动阶段可用,又或者 httpd 依赖其他服务(比如数据库守护进程),那么必须配置该服务,以确保正确的启动顺序。

例如,为了确保 httpd 在所有配置的网络接口配置完成之后再运行,可以创建一个带有以下代码段的 drop-in 文件(如上述):

[Unit]
After=network-online.target
Wants=network-online.target

我仍然觉得这是个 bug,因为在 httpd.conf 配置文件中使用 Listen 指令是很常见的,至少在我的经验中。我一直在使用 Listen 指令,即使在只有一个 IP 地址的主机上,在多个网卡和 IP 地址的机器上这显然也是有必要的。在 /usr/lib/systemd/system/httpd.service 默认配置文件中加入上述几行,对不使用 Listen 指令的不会造成问题,对使用 Listen 指令的则会规避这个问题。

同时,我将使用建议的方法。

下一步

本文描述了一个我在服务器上启动 Apache HTTPD 服务时遇到的一个问题。它指引你了解我在解决这个问题上的思路,并说明了我是如何使用 systemd 来协助解决问题。我也介绍了我用 systemd 实现的规避方法,以及我按照我的 bug 报告得到的更好的解决方案。

如我在开头处提到的那样,这有很大可能是一个 systemd 的问题,特别是 httpd 启动的配置问题。尽管如此,systemd 还是提供了工具让我找到了问题的可能来源,并制定和实现了规避方案。两种方案都没有真正令我满意地解决问题。目前,这个问题根源依旧存在,必须要解决。如果只是在 /usr/lib/systemd/system/httpd.service 文件中添加推荐的代码,那对我来说是可行的。

在这个过程中我发现了一件事,我需要了解更多关于定义服务启动顺序的知识。我会在下一篇文章中探索这个领域,即本系列的第五篇。

资源

网上有大量的关于 systemd 的参考资料,但是大部分都有点简略、晦涩甚至有误导性。除了本文中提到的资料,下列的网页提供了跟多可靠且详细的 systemd 入门信息。

  • Fedora 项目有一篇切实好用的 systemd 入门,它囊括了几乎所有你需要知道的关于如何使用 systemd 配置、管理和维护 Fedora 计算机的信息。
  • Fedora 项目也有一个不错的 备忘录,交叉引用了过去 SystemV 命令和 systemd 命令做对比。
  • 关于 systemd 的技术细节和创建这个项目的原因,请查看 Freedesktop.org 上的 systemd 描述

此外,还有一系列深度的技术文章,是由 systemd 的设计者和主要开发者 Lennart Poettering 为 Linux 系统管理员撰写的。这些文章写于 2010 年 4 月至 2011 年 9 月间,但它们现在和当时一样具有现实意义。关于 systemd 及其生态的许多其他好文章都是基于这些文章:


via:


Systemd 入门教程:命令篇

作者: 阮一峰

日期: 2016年3月 7日

Systemd 是 Linux 系统工具,用来启动守护进程,已成为大多数发行版的标准配置。

img

一、由来

历史上,Linux 的启动一直采用init进程。

下面的命令用来启动服务。

$ sudo /etc/init.d/apache2 start
# 或者
$ service apache2 start

这种方法有两个缺点。

一是启动时间长。init进程是串行启动,只有前一个进程启动完,才会启动下一个进程。

二是启动脚本复杂。init进程只是执行启动脚本,不管其他事情。脚本需要自己处理各种情况,这往往使得脚本变得很长。

二、Systemd 概述

Systemd 就是为了解决这些问题而诞生的。它的设计目标是,为系统的启动和管理提供一套完整的解决方案。

根据 Linux 惯例,字母d是守护进程(daemon)的缩写。 Systemd 这个名字的含义,就是它要守护整个系统。

img

(上图为 Systemd 作者 Lennart Poettering

使用了 Systemd,就不需要再用init了。Systemd 取代了initd,成为系统的第一个进程(PID 等于 1),其他进程都是它的子进程。

$ systemctl --version

上面的命令查看 Systemd 的版本。

Systemd 的优点是功能强大,使用方便,缺点是体系庞大,非常复杂。事实上,现在还有很多人反对使用 Systemd,理由就是它过于复杂,与操作系统的其他部分强耦合,违反"keep simple, keep stupid"的Unix 哲学

img

(上图为 Systemd 架构图)

三、系统管理

Systemd 并不是一个命令,而是一组命令,涉及到系统管理的方方面面。

3.1 systemctl

systemctl是 Systemd 的主命令,用于管理系统。

# 重启系统
$ sudo systemctl reboot

# 关闭系统,切断电源
$ sudo systemctl poweroff

# CPU停止工作
$ sudo systemctl halt

# 暂停系统
$ sudo systemctl suspend

# 让系统进入冬眠状态
$ sudo systemctl hibernate

# 让系统进入交互式休眠状态
$ sudo systemctl hybrid-sleep

# 启动进入救援状态(单用户状态)
$ sudo systemctl rescue

3.2 systemd-analyze

systemd-analyze命令用于查看启动耗时。

# 查看启动耗时
$ systemd-analyze                                                                                       

# 查看每个服务的启动耗时
$ systemd-analyze blame

# 显示瀑布状的启动过程流
$ systemd-analyze critical-chain

# 显示指定服务的启动流
$ systemd-analyze critical-chain atd.service

3.3 hostnamectl

hostnamectl命令用于查看当前主机的信息。

# 显示当前主机的信息
$ hostnamectl

# 设置主机名。
$ sudo hostnamectl set-hostname rhel7

3.4 localectl

localectl命令用于查看本地化设置。

# 查看本地化设置
$ localectl

# 设置本地化参数。
$ sudo localectl set-locale LANG=en_GB.utf8
$ sudo localectl set-keymap en_GB

3.5 timedatectl

timedatectl命令用于查看当前时区设置。

# 查看当前时区设置
$ timedatectl

# 显示所有可用的时区
$ timedatectl list-timezones                                                                                   

# 设置当前时区
$ sudo timedatectl set-timezone America/New_York
$ sudo timedatectl set-time YYYY-MM-DD
$ sudo timedatectl set-time HH:MM:SS

3.6 loginctl

loginctl命令用于查看当前登录的用户。

# 列出当前session
$ loginctl list-sessions

# 列出当前登录用户
$ loginctl list-users

# 列出显示指定用户的信息
$ loginctl show-user ruanyf

四、Unit

4.1 含义

Systemd 可以管理所有系统资源。不同的资源统称为 Unit(单位)。

Unit 一共分成12种。

  • Service unit:系统服务
  • Target unit:多个 Unit 构成的一个组
  • Device Unit:硬件设备
  • Mount Unit:文件系统的挂载点
  • Automount Unit:自动挂载点
  • Path Unit:文件或路径
  • Scope Unit:不是由 Systemd 启动的外部进程
  • Slice Unit:进程组
  • Snapshot Unit:Systemd 快照,可以切回某个快照
  • Socket Unit:进程间通信的 socket
  • Swap Unit:swap 文件
  • Timer Unit:定时器

systemctl list-units命令可以查看当前系统的所有 Unit 。

# 列出正在运行的 Unit
$ systemctl list-units

# 列出所有Unit,包括没有找到配置文件的或者启动失败的
$ systemctl list-units --all

# 列出所有没有运行的 Unit
$ systemctl list-units --all --state=inactive

# 列出所有加载失败的 Unit
$ systemctl list-units --failed

# 列出所有正在运行的、类型为 service 的 Unit
$ systemctl list-units --type=service

4.2 Unit 的状态

systemctl status命令用于查看系统状态和单个 Unit 的状态。

# 显示系统状态
$ systemctl status

# 显示单个 Unit 的状态
$ sysystemctl status bluetooth.service

# 显示远程主机的某个 Unit 的状态
$ systemctl -H root@rhel7.example.com status httpd.service

除了status命令,systemctl还提供了三个查询状态的简单方法,主要供脚本内部的判断语句使用。

# 显示某个 Unit 是否正在运行
$ systemctl is-active application.service

# 显示某个 Unit 是否处于启动失败状态
$ systemctl is-failed application.service

# 显示某个 Unit 服务是否建立了启动链接
$ systemctl is-enabled application.service

4.3 Unit 管理

对于用户来说,最常用的是下面这些命令,用于启动和停止 Unit(主要是 service)。

# 立即启动一个服务
$ sudo systemctl start apache.service

# 立即停止一个服务
$ sudo systemctl stop apache.service

# 重启一个服务
$ sudo systemctl restart apache.service

# 杀死一个服务的所有子进程
$ sudo systemctl kill apache.service

# 重新加载一个服务的配置文件
$ sudo systemctl reload apache.service

# 重载所有修改过的配置文件
$ sudo systemctl daemon-reload

# 显示某个 Unit 的所有底层参数
$ systemctl show httpd.service

# 显示某个 Unit 的指定属性的值
$ systemctl show -p CPUShares httpd.service

# 设置某个 Unit 的指定属性
$ sudo systemctl set-property httpd.service CPUShares=500

4.4 依赖关系

Unit 之间存在依赖关系:A 依赖于 B,就意味着 Systemd 在启动 A 的时候,同时会去启动 B。

systemctl list-dependencies命令列出一个 Unit 的所有依赖。

$ systemctl list-dependencies nginx.service

上面命令的输出结果之中,有些依赖是 Target 类型(详见下文),默认不会展开显示。如果要展开 Target,就需要使用--all参数。

$ systemctl list-dependencies --all nginx.service

五、Unit 的配置文件

5.1 概述

每一个 Unit 都有一个配置文件,告诉 Systemd 怎么启动这个 Unit 。

Systemd 默认从目录/etc/systemd/system/读取配置文件。但是,里面存放的大部分文件都是符号链接,指向目录/usr/lib/systemd/system/,真正的配置文件存放在那个目录。

systemctl enable命令用于在上面两个目录之间,建立符号链接关系。

$ sudo systemctl enable clamd@scan.service
# 等同于
$ sudo ln -s '/usr/lib/systemd/system/clamd@scan.service' '/etc/systemd/system/multi-user.target.wants/clamd@scan.service'

如果配置文件里面设置了开机启动,systemctl enable命令相当于激活开机启动。

与之对应的,systemctl disable命令用于在两个目录之间,撤销符号链接关系,相当于撤销开机启动。

$ sudo systemctl disable clamd@scan.service

配置文件的后缀名,就是该 Unit 的种类,比如sshd.socket。如果省略,Systemd 默认后缀名为.service,所以sshd会被理解成sshd.service

5.2 配置文件的状态

systemctl list-unit-files命令用于列出所有配置文件。

# 列出所有配置文件
$ systemctl list-unit-files

# 列出指定类型的配置文件
$ systemctl list-unit-files --type=service

这个命令会输出一个列表。

$ systemctl list-unit-files

UNIT FILE              STATE
chronyd.service        enabled
clamd@.service         static
clamd@scan.service     disabled

这个列表显示每个配置文件的状态,一共有四种。

  • enabled:已建立启动链接
  • disabled:没建立启动链接
  • static:该配置文件没有[Install]部分(无法执行),只能作为其他配置文件的依赖
  • masked:该配置文件被禁止建立启动链接

注意,从配置文件的状态无法看出,该 Unit 是否正在运行。这必须执行前面提到的systemctl status命令。

$ systemctl status bluetooth.service

一旦修改配置文件,就要让 SystemD 重新加载配置文件,然后重新启动,否则修改不会生效。

$ sudo systemctl daemon-reload
$ sudo systemctl restart httpd.service

5.3 配置文件的格式

配置文件就是普通的文本文件,可以用文本编辑器打开。

systemctl cat命令可以查看配置文件的内容。

$ systemctl cat atd.service

[Unit]
Description=ATD daemon

[Service]
Type=forking
ExecStart=/usr/bin/atd

[Install]
WantedBy=multi-user.target

从上面的输出可以看到,配置文件分成几个区块。每个区块的第一行,是用方括号表示的区别名,比如[Unit]。注意,配置文件的区块名和字段名,都是大小写敏感的。

每个区块内部是一些等号连接的键值对。

[Section]
Directive1=value
Directive2=value

. . .

注意,键值对的等号两侧不能有空格。

5.4 配置文件的区块

[Unit]区块通常是配置文件的第一个区块,用来定义 Unit 的元数据,以及配置与其他 Unit 的关系。它的主要字段如下。

  • Description:简短描述
  • Documentation:文档地址
  • Requires:当前 Unit 依赖的其他 Unit,如果它们没有运行,当前 Unit 会启动失败
  • Wants:与当前 Unit 配合的其他 Unit,如果它们没有运行,当前 Unit 不会启动失败
  • BindsTo:与Requires类似,它指定的 Unit 如果退出,会导致当前 Unit 停止运行
  • Before:如果该字段指定的 Unit 也要启动,那么必须在当前 Unit 之后启动
  • After:如果该字段指定的 Unit 也要启动,那么必须在当前 Unit 之前启动
  • Conflicts:这里指定的 Unit 不能与当前 Unit 同时运行
  • Condition...:当前 Unit 运行必须满足的条件,否则不会运行
  • Assert...:当前 Unit 运行必须满足的条件,否则会报启动失败

[Install]通常是配置文件的最后一个区块,用来定义如何启动,以及是否开机启动。它的主要字段如下。

  • WantedBy:它的值是一个或多个 Target,当前 Unit 激活时(enable)符号链接会放入/etc/systemd/system目录下面以 Target 名 + .wants后缀构成的子目录中
  • RequiredBy:它的值是一个或多个 Target,当前 Unit 激活时,符号链接会放入/etc/systemd/system目录下面以 Target 名 + .required后缀构成的子目录中
  • Alias:当前 Unit 可用于启动的别名
  • Also:当前 Unit 激活(enable)时,会被同时激活的其他 Unit

[Service]区块用来 Service 的配置,只有 Service 类型的 Unit 才有这个区块。它的主要字段如下。

  • Type:定义启动时的进程行为。它有以下几种值。
  • Type=simple:默认值,执行ExecStart指定的命令,启动主进程
  • Type=forking:以 fork 方式从父进程创建子进程,创建后父进程会立即退出
  • Type=oneshot:一次性进程,Systemd 会等当前服务退出,再继续往下执行
  • Type=dbus:当前服务通过D-Bus启动
  • Type=notify:当前服务启动完毕,会通知Systemd,再继续往下执行
  • Type=idle:若有其他任务执行完毕,当前服务才会运行
  • ExecStart:启动当前服务的命令
  • ExecStartPre:启动当前服务之前执行的命令
  • ExecStartPost:启动当前服务之后执行的命令
  • ExecReload:重启当前服务时执行的命令
  • ExecStop:停止当前服务时执行的命令
  • ExecStopPost:停止当其服务之后执行的命令
  • RestartSec:自动重启当前服务间隔的秒数
  • Restart:定义何种情况 Systemd 会自动重启当前服务,可能的值包括always(总是重启)、on-successon-failureon-abnormalon-aborton-watchdog
  • TimeoutSec:定义 Systemd 停止当前服务之前等待的秒数
  • Environment:指定环境变量

Unit 配置文件的完整字段清单,请参考官方文档

六、Target

启动计算机的时候,需要启动大量的 Unit。如果每一次启动,都要一一写明本次启动需要哪些 Unit,显然非常不方便。Systemd 的解决方案就是 Target。

简单说,Target 就是一个 Unit 组,包含许多相关的 Unit 。启动某个 Target 的时候,Systemd 就会启动里面所有的 Unit。从这个意义上说,Target 这个概念类似于"状态点",启动某个 Target 就好比启动到某种状态。

传统的init启动模式里面,有 RunLevel 的概念,跟 Target 的作用很类似。不同的是,RunLevel 是互斥的,不可能多个 RunLevel 同时启动,但是多个 Target 可以同时启动。

# 查看当前系统的所有 Target
$ systemctl list-unit-files --type=target

# 查看一个 Target 包含的所有 Unit
$ systemctl list-dependencies multi-user.target

# 查看启动时的默认 Target
$ systemctl get-default

# 设置启动时的默认 Target
$ sudo systemctl set-default multi-user.target

# 切换 Target 时,默认不关闭前一个 Target 启动的进程,
# systemctl isolate 命令改变这种行为,
# 关闭前一个 Target 里面所有不属于后一个 Target 的进程
$ sudo systemctl isolate multi-user.target

Target 与 传统 RunLevel 的对应关系如下。

Traditional runlevel      New target name     Symbolically linked to...

Runlevel 0           |    runlevel0.target -> poweroff.target
Runlevel 1           |    runlevel1.target -> rescue.target
Runlevel 2           |    runlevel2.target -> multi-user.target
Runlevel 3           |    runlevel3.target -> multi-user.target
Runlevel 4           |    runlevel4.target -> multi-user.target
Runlevel 5           |    runlevel5.target -> graphical.target
Runlevel 6           |    runlevel6.target -> reboot.target

它与init进程的主要差别如下。

(1)默认的 RunLevel(在/etc/inittab文件设置)现在被默认的 Target 取代,位置是/etc/systemd/system/default.target,通常符号链接到graphical.target(图形界面)或者multi-user.target(多用户命令行)。

(2)启动脚本的位置,以前是/etc/init.d目录,符号链接到不同的 RunLevel 目录 (比如/etc/rc3.d/etc/rc5.d等),现在则存放在/lib/systemd/system/etc/systemd/system目录。

(3)配置文件的位置,以前init进程的配置文件是/etc/inittab,各种服务的配置文件存放在/etc/sysconfig目录。现在的配置文件主要存放在/lib/systemd目录,在/etc/systemd目录里面的修改可以覆盖原始设置。

七、日志管理

Systemd 统一管理所有 Unit 的启动日志。带来的好处就是,可以只用journalctl一个命令,查看所有日志(内核日志和应用日志)。日志的配置文件是/etc/systemd/journald.conf

journalctl功能强大,用法非常多。

# 查看所有日志(默认情况下 ,只保存本次启动的日志)
$ sudo journalctl

# 查看内核日志(不显示应用日志)
$ sudo journalctl -k

# 查看系统本次启动的日志
$ sudo journalctl -b
$ sudo journalctl -b -0

# 查看上一次启动的日志(需更改设置)
$ sudo journalctl -b -1

# 查看指定时间的日志
$ sudo journalctl --since="2012-10-30 18:17:16"
$ sudo journalctl --since "20 min ago"
$ sudo journalctl --since yesterday
$ sudo journalctl --since "2015-01-10" --until "2015-01-11 03:00"
$ sudo journalctl --since 09:00 --until "1 hour ago"

# 显示尾部的最新10行日志
$ sudo journalctl -n

# 显示尾部指定行数的日志
$ sudo journalctl -n 20

# 实时滚动显示最新日志
$ sudo journalctl -f

# 查看指定服务的日志
$ sudo journalctl /usr/lib/systemd/systemd

# 查看指定进程的日志
$ sudo journalctl _PID=1

# 查看某个路径的脚本的日志
$ sudo journalctl /usr/bin/bash

# 查看指定用户的日志
$ sudo journalctl _UID=33 --since today

# 查看某个 Unit 的日志
$ sudo journalctl -u nginx.service
$ sudo journalctl -u nginx.service --since today

# 实时滚动显示某个 Unit 的最新日志
$ sudo journalctl -u nginx.service -f

# 合并显示多个 Unit 的日志
$ journalctl -u nginx.service -u php-fpm.service --since today

# 查看指定优先级(及其以上级别)的日志,共有8级
# 0: emerg
# 1: alert
# 2: crit
# 3: err
# 4: warning
# 5: notice
# 6: info
# 7: debug
$ sudo journalctl -p err -b

# 日志默认分页输出,--no-pager 改为正常的标准输出
$ sudo journalctl --no-pager

# 以 JSON 格式(单行)输出
$ sudo journalctl -b -u nginx.service -o json

# 以 JSON 格式(多行)输出,可读性更好
$ sudo journalctl -b -u nginx.serviceqq
 -o json-pretty

# 显示日志占据的硬盘空间
$ sudo journalctl --disk-usage

# 指定日志文件占据的最大空间
$ sudo journalctl --vacuum-size=1G

# 指定日志文件保存多久
$ sudo journalctl --vacuum-time=1years

(完)


Systemd 入门教程:实战篇

作者: 阮一峰

日期: 2016年3月 8日

如何使用它完成一些基本的任务。

一、开机启动

对于那些支持 Systemd 的软件,安装的时候,会自动在/usr/lib/systemd/system目录添加一个配置文件。

如果你想让该软件开机启动,就执行下面的命令(以httpd.service为例)。

$ sudo systemctl enable httpd

上面的命令相当于在/etc/systemd/system目录添加一个符号链接,指向/usr/lib/systemd/system里面的httpd.service文件。

这是因为开机时,Systemd只执行/etc/systemd/system目录里面的配置文件。这也意味着,如果把修改后的配置文件放在该目录,就可以达到覆盖原始配置的效果。

二、启动服务

设置开机启动以后,软件并不会立即启动,必须等到下一次开机。如果想现在就运行该软件,那么要执行systemctl start命令。

$ sudo systemctl start httpd

执行上面的命令以后,有可能启动失败,因此要用systemctl status命令查看一下该服务的状态。

$ sudo systemctl status httpd

httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled)
   Active: active (running) since 金 2014-12-05 12:18:22 JST; 7min ago
 Main PID: 4349 (httpd)
   Status: "Total requests: 1; Current requests/sec: 0; Current traffic:   0 B/sec"
   CGroup: /system.slice/httpd.service
           ├─4349 /usr/sbin/httpd -DFOREGROUND
           ├─4350 /usr/sbin/httpd -DFOREGROUND
           ├─4351 /usr/sbin/httpd -DFOREGROUND
           ├─4352 /usr/sbin/httpd -DFOREGROUND
           ├─4353 /usr/sbin/httpd -DFOREGROUND
           └─4354 /usr/sbin/httpd -DFOREGROUND

12月 05 12:18:22 localhost.localdomain systemd[1]: Starting The Apache HTTP Server...
12月 05 12:18:22 localhost.localdomain systemd[1]: Started The Apache HTTP Server.
12月 05 12:22:40 localhost.localdomain systemd[1]: Started The Apache HTTP Server.

上面的输出结果含义如下。

  • Loaded行:配置文件的位置,是否设为开机启动
  • Active行:表示正在运行
  • Main PID行:主进程ID
  • Status行:由应用本身(这里是 httpd )提供的软件当前状态
  • CGroup块:应用的所有子进程
  • 日志块:应用的日志

三、停止服务

终止正在运行的服务,需要执行systemctl stop命令。

$ sudo systemctl stop httpd.service

有时候,该命令可能没有响应,服务停不下来。这时候就不得不"杀进程"了,向正在运行的进程发出kill信号。

$ sudo systemctl kill httpd.service

此外,重启服务要执行systemctl restart命令。

$ sudo systemctl restart httpd.service

四、读懂配置文件

一个服务怎么启动,完全由它的配置文件决定。下面就来看,配置文件有些什么内容。

前面说过,配置文件主要放在/usr/lib/systemd/system目录,也可能在/etc/systemd/system目录。找到配置文件以后,使用文本编辑器打开即可。

systemctl cat命令可以用来查看配置文件,下面以sshd.service文件为例,它的作用是启动一个 SSH 服务器,供其他用户以 SSH 方式登录。

$ systemctl cat 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]
EnvironmentFile=/etc/sysconfig/sshd
ExecStart=/usr/sbin/sshd -D $OPTIONS
ExecReload=/bin/kill -HUP $MAINPID
Type=simple
KillMode=process
Restart=on-failure
RestartSec=42s

[Install]
WantedBy=multi-user.target

可以看到,配置文件分成几个区块,每个区块包含若干条键值对。

下面依次解释每个区块的内容。

五、 [Unit] 区块:启动顺序与依赖关系。

Unit区块的Description字段给出当前服务的简单描述,Documentation字段给出文档位置。

接下来的设置是启动顺序和依赖关系,这个比较重要。

After字段:表示如果network.targetsshd-keygen.service需要启动,那么sshd.service应该在它们之后启动。

相应地,还有一个Before字段,定义sshd.service应该在哪些服务之前启动。

注意,AfterBefore字段只涉及启动顺序,不涉及依赖关系。

举例来说,某 Web 应用需要 postgresql 数据库储存数据。在配置文件中,它只定义要在 postgresql 之后启动,而没有定义依赖 postgresql 。上线后,由于某种原因,postgresql 需要重新启动,在停止服务期间,该 Web 应用就会无法建立数据库连接。

设置依赖关系,需要使用Wants字段和Requires字段。

Wants字段:表示sshd.servicesshd-keygen.service之间存在"弱依赖"关系,即如果"sshd-keygen.service"启动失败或停止运行,不影响sshd.service继续执行。

Requires字段则表示"强依赖"关系,即如果该服务启动失败或异常退出,那么sshd.service也必须退出。

注意,Wants字段与Requires字段只涉及依赖关系,与启动顺序无关,默认情况下是同时启动的。

六、[Service] 区块:启动行为

Service区块定义如何启动当前服务。

6.1 启动命令

许多软件都有自己的环境参数文件,该文件可以用EnvironmentFile字段读取。

EnvironmentFile字段:指定当前服务的环境参数文件。该文件内部的key=value键值对,可以用$key的形式,在当前配置文件中获取。

上面的例子中,sshd 的环境参数文件是/etc/sysconfig/sshd

配置文件里面最重要的字段是ExecStart

ExecStart字段:定义启动进程时执行的命令。

上面的例子中,启动sshd,执行的命令是/usr/sbin/sshd -D $OPTIONS,其中的变量$OPTIONS就来自EnvironmentFile字段指定的环境参数文件。

与之作用相似的,还有如下这些字段。

  • ExecReload字段:重启服务时执行的命令
  • ExecStop字段:停止服务时执行的命令
  • ExecStartPre字段:启动服务之前执行的命令
  • ExecStartPost字段:启动服务之后执行的命令
  • ExecStopPost字段:停止服务之后执行的命令

请看下面的例子。

[Service]
ExecStart=/bin/echo execstart1
ExecStart=
ExecStart=/bin/echo execstart2
ExecStartPost=/bin/echo post1
ExecStartPost=/bin/echo post2

上面这个配置文件,第二行ExecStart设为空值,等于取消了第一行的设置,运行结果如下。

execstart2
post1
post2

所有的启动设置之前,都可以加上一个连词号(-),表示"抑制错误",即发生错误的时候,不影响其他命令的执行。比如,EnvironmentFile=-/etc/sysconfig/sshd(注意等号后面的那个连词号),就表示即使/etc/sysconfig/sshd文件不存在,也不会抛出错误。

6.2 启动类型

Type字段定义启动类型。它可以设置的值如下。

  • simple(默认值):ExecStart字段启动的进程为主进程
  • forking:ExecStart字段将以fork()方式启动,此时父进程将会退出,子进程将成为主进程
  • oneshot:类似于simple,但只执行一次,Systemd 会等它执行完,才启动其他服务
  • dbus:类似于simple,但会等待 D-Bus 信号后启动
  • notify:类似于simple,启动结束后会发出通知信号,然后 Systemd 再启动其他服务
  • idle:类似于simple,但是要等到其他任务都执行完,才会启动该服务。一种使用场合是为让该服务的输出,不与其他服务的输出相混合

下面是一个oneshot的例子,笔记本电脑启动时,要把触摸板关掉,配置文件可以这样写。

[Unit]
Description=Switch-off Touchpad

[Service]
Type=oneshot
ExecStart=/usr/bin/touchpad-off

[Install]
WantedBy=multi-user.target

上面的配置文件,启动类型设为oneshot,就表明这个服务只要运行一次就够了,不需要长期运行。

如果关闭以后,将来某个时候还想打开,配置文件修改如下。

[Unit]
Description=Switch-off Touchpad

[Service]
Type=oneshot
ExecStart=/usr/bin/touchpad-off start
ExecStop=/usr/bin/touchpad-off stop
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

上面配置文件中,RemainAfterExit字段设为yes,表示进程退出以后,服务仍然保持执行。这样的话,一旦使用systemctl stop命令停止服务,ExecStop指定的命令就会执行,从而重新开启触摸板。

6.3 重启行为

Service区块有一些字段,定义了重启行为。

KillMode字段:定义 Systemd 如何停止 sshd 服务。

上面这个例子中,将KillMode设为process,表示只停止主进程,不停止任何sshd 子进程,即子进程打开的 SSH session 仍然保持连接。这个设置不太常见,但对 sshd 很重要,否则你停止服务的时候,会连自己打开的 SSH session 一起杀掉。

KillMode字段可以设置的值如下。

  • control-group(默认值):当前控制组里面的所有子进程,都会被杀掉
  • process:只杀主进程
  • mixed:主进程将收到 SIGTERM 信号,子进程收到 SIGKILL 信号
  • none:没有进程会被杀掉,只是执行服务的 stop 命令。

接下来是Restart字段。

Restart字段:定义了 sshd 退出后,Systemd 的重启方式。

上面的例子中,Restart设为on-failure,表示任何意外的失败,就将重启sshd。如果 sshd 正常停止(比如执行systemctl stop命令),它就不会重启。

Restart字段可以设置的值如下。

  • no(默认值):退出后不会重启
  • on-success:只有正常退出时(退出状态码为0),才会重启
  • on-failure:非正常退出时(退出状态码非0),包括被信号终止和超时,才会重启
  • on-abnormal:只有被信号终止和超时,才会重启
  • on-abort:只有在收到没有捕捉到的信号终止时,才会重启
  • on-watchdog:超时退出,才会重启
  • always:不管是什么退出原因,总是重启

对于守护进程,推荐设为on-failure。对于那些允许发生错误退出的服务,可以设为on-abnormal

最后是RestartSec字段。

RestartSec字段:表示 Systemd 重启服务之前,需要等待的秒数。上面的例子设为等待42秒。

七、[Install] 区块

Install区块,定义如何安装这个配置文件,即怎样做到开机启动。

WantedBy字段:表示该服务所在的 Target。

Target的含义是服务组,表示一组服务。WantedBy=multi-user.target指的是,sshd 所在的 Target 是multi-user.target

这个设置非常重要,因为执行systemctl enable sshd.service命令时,sshd.service的一个符号链接,就会放在/etc/systemd/system目录下面的multi-user.target.wants子目录之中。

Systemd 有默认的启动 Target。

$ systemctl get-default
multi-user.target

上面的结果表示,默认的启动 Target 是multi-user.target。在这个组里的所有服务,都将开机启动。这就是为什么systemctl enable命令能设置开机启动的原因。

使用 Target 的时候,systemctl list-dependencies命令和systemctl isolate命令也很有用。

# 查看 multi-user.target 包含的所有服务
$ systemctl list-dependencies multi-user.target

# 切换到另一个 target
# shutdown.target 就是关机状态
$ sudo systemctl isolate shutdown.target

一般来说,常用的 Target 有两个:一个是multi-user.target,表示多用户命令行状态;另一个是graphical.target,表示图形用户状态,它依赖于multi-user.target。官方文档有一张非常清晰的 [Target 依赖关系图](https://www.freedesktop.org/software/systemd/man/bootup.html#System Manager Bootup)。

八、Target 的配置文件

Target 也有自己的配置文件。

$ 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

注意,Target 配置文件里面没有启动命令。

上面输出结果中,主要字段含义如下。

Requires字段:要求basic.target一起运行。

Conflicts字段:冲突字段。如果rescue.servicerescue.target正在运行,multi-user.target就不能运行,反之亦然。

After:表示multi-user.targetbasic.targetrescue.servicerescue.target之后启动,如果它们有启动的话。

AllowIsolate:允许使用systemctl isolate命令切换到multi-user.target

九、修改配置文件后重启

修改配置文件以后,需要重新加载配置文件,然后重新启动相关服务。

# 重新加载配置文件
$ sudo systemctl daemon-reload

# 重启相关服务
$ sudo systemctl restart foobar

(完)


via:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值