Linux-Systemd-服务管理轻松学-全-

Linux Systemd 服务管理轻松学(全)

原文:annas-archive.org/md5/d25769cf0f644186e0a711f910bdb61e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎,亲爱的读者,来到全球首本关于 systemd 及其生态系统的全面书籍。尽管 systemd 已经成为世界上最广泛使用的 Linux 初始化系统,但关于它的书籍并不多。当然,网络上有官方的 systemd 文档,但它简洁而直白,并没有提供太多实际的例子。还有一些不错的博客教程,但大多数只讲解基础知识。我只找到另外两本书,标题中包含 systemd,但这两本书都已过时,且也仅仅讲解基础内容。

我写这本书的目标是带你超越基础,向你展示如何成为一名更有效的 Linux 系统管理员。在每一章中,我们都将深入探讨 systemd 是如何实际工作的。请放心,本书将有大量的动手演示,向你展示如何让 systemd 按照你想要的方式运行。

本书适合谁阅读

如果你是 Linux 系统管理员,或者正在学习成为一名 Linux 系统管理员,那么这本书对你有帮助。如果你正在准备参加 Linux 认证考试,例如来自 CompTIA、Linux 专业学院或商业 Linux 发行版厂商的考试,这本书也是一个很好的学习辅导材料。

本书内容概述

第一章理解系统需要 systemd,回顾了 Linux init 系统的历史,并解释了为什么旧的 init 系统需要被更强大的替代方案所取代。我们还将简要讨论围绕着转向 systemd 所产生的争议。

第二章理解 systemd 目录和文件,探讨了包含 systemd 文件的各个目录。我们还将探讨各种 systemd 单元文件和配置文件,并解释每种类型的用途。最后,我们将简要了解与 systemd 相关的可执行文件。

第三章理解服务、路径和套接字单元,探讨了服务、路径和套接字单元文件的内部工作原理。我们将查看每个单元文件中的各个部分,并了解一些你可以设置的参数。过程中,我还会给出一些关于如何查找各种参数作用的信息的提示。

第四章控制 systemd 服务,探讨了如何控制 systemd 服务。我们将从如何列出系统中有哪些服务及其状态开始,然后介绍如何启用、禁用、启动、停止和重启服务。

第五章创建和编辑服务,介绍了如何使用 systemctl 创建和编辑 systemd 服务文件。对于需要使用 Docker 容器的朋友,我将展示一种使用新的 podman Docker 替代工具,将容器轻松转化为服务的酷炫方法。我们还将讨论如何在服务文件添加或更改后重新加载它。

第六章理解 systemd 目标,介绍了各种 systemd 目标。我们将解释它们是什么以及目标文件的结构。接着,我们将把 systemd 目标与旧版 SysVinit 的运行级别进行比较,并学习如何将系统从一个目标切换到另一个目标。

第七章理解 systemd 定时器,介绍了如何创建 systemd 定时器。我们还将把 systemd 定时器与旧版 cron 系统进行比较,看看我们更喜欢哪个。

第八章理解 systemd 启动过程,介绍了 systemd 启动过程,并将其与旧版 SysVinit 启动过程进行比较。

第九章设置系统参数,介绍了如何使用 systemd 工具设置特定的系统参数。你一旦了解了如何使用 systemd 来做这件事,或许会发现 systemd 确实使这变得更加简便。

第十章理解关机与重启命令,介绍了如何使用 systemctl 工具来关机和重启 Linux 系统。之后,我们将查看传统的 shutdown 命令是否仍然有效。

第十一章理解 cgroups 版本 1,介绍了 cgroups 的概念及其简短历史。然后我们将探讨 cgroups 如何帮助提升 Linux 系统的安全性。

第十二章使用 cgroups 版本 1 控制资源使用,介绍了如何使用 cgroups 来控制现代 Linux 系统上的资源使用情况。这包括如何控制内存和 CPU 使用,以及如何为用户分配资源。

第十三章理解 cgroups 版本 2,介绍了 cgroups 版本 2。我们将探讨它与版本 1 的区别以及它如何对版本 1 进行了改进。之后,我们将简要介绍如何使用 cgroups 版本 2。作为额外的内容,我们还将展示如何轻松使用 cgroups 版本 2 完成在版本 1 中很难做到的事情,例如创建 cpusets 并将 CPU 核心分配到正确的 非统一内存访问NUMA)节点。

第十四章使用 journald,介绍了 journald 的基本用法以及它与传统 rsyslog 的区别。我们还将探讨为什么我们仍然需要 rsyslog。最重要的是,你将学会如何从系统日志中提取和格式化所需的数据。

第十五章使用 systemd-networkd 和 systemd-resolved,展示了为什么你可能想要使用 systemd-networkd 和 systemd-resolved 而不是默认的 Network Manager,以及如何做到这一点。我们将深入探讨如何为不同场景设置 systemd-networkd,并说明在 Ubuntu 和 Red Hat 类型的发行版中,配置流程的不同之处。

第十六章理解通过 systemd 实现时间同步,介绍了在 systemd 系统上保持准确时间的各种方法。我们将探讨 ntpchronysystemd-timesyncd 和精确时间协议。我们还将讨论每种方法的优缺点,并讲解如何配置它们。

第十七章理解 systemd 和引导加载程序,讲解了如何使用 GRUB2 和 systemd-boot 设置机器以使用 EFI/UEFI 模式进行启动。然后,我们将介绍如何在设置为 UEFI 启动模式的机器上安装 Pop!_OS Linux,并简要讨论安全启动功能。

第十八章理解 systemd-logind,介绍了如何使用和配置 systemd-logind。我们还将学习如何使用 loginctl 工具查看用户登录会话的信息、控制 logind 服务并终止问题用户的会话。最后,我们将简要了解 polkit,它是一种向特定用户授予管理员权限的替代方式。

充分利用本书

为了进行本书中的演示,你应该具备基本的 Linux 命令行操作能力,并且知道如何创建 VirtualBox 虚拟机。你可以从 www.virtualbox.org/ 下载 VirtualBox,并在 distrowatch.com/ 查找各种 Linux 发行版的下载链接。当你创建虚拟机时,确保为虚拟机分配足够的内存以确保运行效率,同时分配足够的磁盘空间来存放演示所需的所有内容。(我建议为文本模式虚拟机分配至少 2 GB 内存,为图形模式虚拟机分配至少 4 GB 内存,除非我为某些演示另行说明。虚拟磁盘大小设置为约 20 GB。)

当您安装 Ubuntu 发行版时,您会自动被加入到sudo组,从而获得完全的sudo权限。安装 AlmaLinux 时,您将有机会为 root 用户创建密码。我的建议是不要这样做,而是直接勾选安装程序中的将此用户设置为管理员选项。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,网址为:github.com/PacktPublishing/Linux-Service-Management-Made-Easy-with-systemd。如果代码有更新,它将会在现有的 GitHub 库中进行更新。我们还提供了来自我们丰富图书和视频目录的其他代码包,网址为:github.com/PacktPublishing/。快去查看吧!

实战代码

本书的《实战代码》视频可以在bit.ly/31jQdi0查看。

下载彩色图片

我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件,您可以在这里下载:static.packt-cdn.com/downloads/9781801811644_ColorImages.pdf

使用的约定

本书中使用了若干文本约定。

文本中的代码:指示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。

任何命令行输入或输出都以如下格式书写:

donnie@ubuntu20-04:~$ sudo systemctl daemon-reload
[sudo] password for donnie: 
donnie@ubuntu20-04:~$

粗体:表示新术语、重要词汇或屏幕上显示的词汇。例如,菜单或对话框中的词汇会以这种方式显示在文本中。以下是一个例子:点击 Etcher 中的 Flash 以写入映像

提示或重要说明

显示方式如下。

联系我们

我们总是欢迎读者的反馈。

一般反馈:如果您对本书的任何内容有疑问,请在邮件主题中注明书名,并通过电子邮件联系我们:customercare@packtpub.com。

勘误:尽管我们已尽最大努力确保内容的准确性,但难免会出现错误。如果您在本书中发现错误,欢迎向我们报告。请访问www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表单”链接并输入详细信息。

盗版:如果您在互联网上发现任何我们作品的非法复制版本,我们将非常感激您能提供其位置地址或网站名称。请通过电子邮件联系版权@packt.com,并附上相关链接。

如果你有兴趣成为作者:如果你在某个领域有专长,并且有兴趣写作或为一本书做贡献,请访问 authors.packtpub.com

分享你的想法

一旦你读完 Linux 服务管理轻松掌握 systemd,我们很想听听你的想法!请 点击这里直接前往亚马逊评论页面,并分享你的反馈。

你的评价对我们和技术社区都非常重要,将帮助我们确保提供优质的内容。

第一部分:使用 systemd

完成第一部分后,您将知道如何控制系统服务、设置环境参数,并创建新的 systemd 单元。

本书的这一部分包括以下章节:

  • 第一章**,理解 systemd 的必要性

  • 第二章**,理解 systemd 目录和文件

  • 第三章**,理解服务、路径和套接字单元

  • 第四章**,控制 systemd 服务

  • 第五章**,创建和编辑服务

  • 第六章**,理解 systemd 目标

  • 第七章**,理解 systemd 定时器

  • 第八章**,理解 systemd 启动过程

  • 第九章**,设置系统参数

  • 第十章**,理解关机和重启命令

第一章:理解 systemd 的需求

在本章中,我们将首先简要回顾一下 Linux init 系统的历史。然后,我们将讨论传统 init 系统的不足之处,以及为什么某些 Linux 工程师觉得有必要开发一种新型的 init 系统。最后,我们将讨论围绕 systemd 的争议。为了方便参考,以下是本章的主题列表:

  • Linux init 系统的历史

  • SysV init 和 upstart 的不足之处

  • systemd 的优点

  • systemd 的争议

好了,介绍性内容就到这里,我们进入正题。

技术要求

对于本章,你只需要一台运行 systemd 的 Linux 虚拟机。在阅读本章时,你可能想查看一下虚拟机上的一些文件。

Linux init 系统的历史

那么,什么是 init 系统呢?init初始化(initialization)的缩写。因此,init 系统就是在系统启动时初始化操作系统的系统。启动完成后,init 系统会继续工作,管理系统的进程和服务。每个系统进程都会被分配一个进程 ID(PID)。init 进程的 PID 总是 1,所有其他在系统上启动的进程都是 init 进程的子进程或孙进程。

多年来,Linux 基础操作系统的 init 系统一直是 SysV(System 5 的简称,其中 V 是罗马数字 5)。SysV init 最初是由贝尔实验室的工程师为 Unix 操作系统开发的,最早可以追溯到 1970 年代初期。(那时,我还是初中的小伙子,头发也还是满头茂盛。)

注意

其实,除了我在这里提到的几种 Linux init 系统外,还有一些其他的系统。但这些是在 systemd 时代之前最常用的。

SysV init 在其时代运行得很好,但它从未是完美的。如今,随着新一代高性能硬件的出现,SysV init 显示出了它的年代感和不足之处。首次尝试提出更好的方案是在 2009 年 7 月,当时 Ubuntu 工程师发布了第一个版本的 init 系统。尽管它比 SysV 更好,但仍然存在一些问题,尤其是早期版本有不少 BUG。

SysV Init 和 upstart 的不足之处

SysV 的第一个问题是启动时间相对较长。当你启动一个 SysV 系统时,所有服务都必须按顺序启动。对于普通的桌面机器来说,这可能不算什么大问题,但对于需要运行多个服务的服务器来说,这就可能会成为一个问题。在这种情况下,每个服务都必须等着自己的轮次启动,这可能会耗费不少时间。

SysV 的下一个问题是它的复杂性。SysV 并不是通过简单易懂的配置文件来完成一切,而是通过复杂的 Bash shell 脚本来实现。控制系统服务的 init 脚本必须为每个服务分配一个优先级数字,以确保服务按正确的顺序启动和停止。以在 CentOS 5 机器上启动 Apache Web 服务器的 init 脚本为例。首先,我们可以看到它是一个相当长的脚本,如下所示:

[student@localhost init.d]$ pwd
/etc/init.d
[student@localhost init.d]$ ls -l httpd
-rwxr-xr-x 1 root root 3523 Sep 16  2014 httpd
[student@localhost init.d]$ wc -l httpd
131 httpd
[student@localhost init.d]$

你可以从 wc -l 输出中看到,它由 131 行组成。如你所见,其中 37 行是注释,这仍然剩下 94 行实际的代码:

[student@localhost init.d]$ grep ^# httpd | wc -l
37
[student@localhost init.d]$

打开来看,你会发现它非常复杂且繁琐。这只是它的第一部分:

图 1.1 – 一个老式的 SysV Init 脚本

脚本的结尾部分,你会看到控制 Apache 守护进程停止、启动、重启和重载的代码,如下所示:

图 1.2 – init 脚本的启动、停止、重启、重载部分

这段代码,或类似的代码,必须出现在每个 init 脚本中,以便人类用户能够控制守护进程。更复杂的是,开发者并不总是为不同的程序编写一致的代码。例如,一个守护进程的状态显示并不总是和另一个守护进程的状态显示相同。

然后,不同 Linux 发行版家族之间的不一致实现又带来了问题。使用 SysV 时,至少有三种不同的实现方式。红帽类发行版使用一种方法,Debian 类发行版使用另一种方法,而 Slackware 类发行版则使用第三种方法。例如,红帽控制服务的方法需要使用 servicechkconfig 命令。每次在 Debian 类系统上工作时,我总是需要查找服务管理命令,因为我总是记不住它们。而在 Slackware 上,则没有任何服务管理命令。要在 Slackware 机器上启用或禁用服务,只需要为相应的 init 脚本设置或移除可执行权限。

运行级别也是一个混淆源,因为每个发行版家族都有自己的一套运行级别定义。例如,以下是图形运行级别的定义:

  • 红帽系列使用了运行级别 5。

  • Slackware 系列使用了运行级别 4。

  • Debian 系列没有为文本模式或图形模式指定特定的运行级别。相反,你通过启用或禁用 X 服务器守护进程来启用或禁用图形模式。

所以,你可以看到这一切都相当混乱,尤其是对于在混合环境中工作的人来说。显而易见,我们需要一些稍微不那么混乱的东西。

如果这还不够,性能问题也摆在了面前。SysV 在其时代表现良好,当时计算硬件还较为原始。但在现代硬件上,尤其是那些拥有多个 CPU 每个还拥有多个核心的机器,我们需要一些更加健壮的东西。Ubuntu 的 upstart 本应解决这一问题,但它并没有完全实现承诺。如今,Upstart 已经完全死掉,但仍然有一些死忠粉丝拒绝放弃 SysV。在企业中,systemd 是王者。

systemd 的优点

我们刚刚看到了 SysV 和 Upstart 的问题。现在,让我们来看看是什么让 systemd 更好。

systemd 的简洁性

与 SysV 相比,systemd 的配置确实简单得多。例如,看看在 CentOS 7 上使用 systemd 的 Apache 服务文件是多么简短:

[donnie@localhost ~]$ cd /lib/systemd/system
[donnie@localhost system]$ ls -l httpd.service
-rw-r--r--. 1 root root 752 Jun 26  2018 httpd.service
[donnie@localhost system]$ wc -l httpd.service
22 httpd.service
[donnie@localhost system]$

只有 22 行,其中 5 行是注释,如下所示:

图 1.3 – 一个 systemd 服务文件

我稍后会解释 systemd 文件中的所有内容。目前,我只想向你展示,systemd 服务文件比 SysV 的 init 脚本简单得多。(正如我们在接下来的章节中将看到的,学习如何使用 systemd 指令比学习如何为 init 脚本编写 shell 脚本代码要容易得多。)

systemd 的一致性

下一个 systemd 优点是它的一致性。是的,朋友们,你们不再需要记住多套针对不同 Linux 发行版的系统管理命令了。相反,你现在只需在所有使用 systemd 的 Linux 发行版上使用相同的命令。因此,这消除了管理员的主要困扰来源,也为那些正在学习参加 Linux 认证考试的人们省去了麻烦。

systemd 的性能

与 SysV 相比,systemd 可以并行启动服务,而不是像 SysV 那样一次启动一个。这使得启动时间比 SysV 更快。一旦机器启动,性能也比 SysV 更加稳定。

使用 systemd,我们有了一种更清洁的方式来终止进程。例如,如果你需要使用 kill 命令强制终止一个 SysV 机器上的 Apache Web 服务器服务,你只能终止 Apache 进程本身。如果 Web 服务器进程由于运行 CGI 脚本而产生了任何子进程,这些进程会继续一段时间,成为 僵尸 进程。但当你使用 systemd 杀死服务时,与该服务相关的所有进程也会被终止。

systemd 安全性

一个额外的好处是,你可以配置 systemd 服务文件来控制系统安全的某些方面。以下是你可以做的一些操作:

  • 你可以创建一个 systemd 服务,限制某些目录的访问,或者只能从特定网络地址访问或被访问。

  • 通过使用命名空间,你可以有效地将服务与系统的其他部分隔离开。这也允许你在不运行 Docker 的情况下创建容器。

  • 你可以使用 cgroups 来限制资源使用。这有助于防止某些类型的拒绝服务攻击。

  • 你可以指定服务允许拥有的哪些根级内核能力。

有了这一切,你可以使 systemd 在某种程度上模拟一个强制访问控制系统,如 SELinux 或 AppArmor。

总的来说,systemd 比任何之前的 init 系统要好得多。但它并没有让所有人都满意。

systemd 的争议

如果你在计算机领域待了有一段时间,可能会发现我们这些极客在操作系统问题上常常表现得非常激烈。上世纪 90 年代初,我终于将我的 8088 机器(只能运行文本模式)替换为能够运行图形界面的机器。我第一次尝试了 Windows 3.1,迅速决定我非常讨厌它。于是,我购买了 OS/2,并且它让我更加满意,我在自己组装的 486 机器上运行了好几年。但我所有的极客朋友都非常喜欢 Windows,他们不断和我争论 Windows 比其他操作系统好。我认为他们都疯了,我们常常因此发生激烈的争执。

然后,当我接触 Linux 时,我很快就意识到,你不应该去任何 Linux 论坛问哪个 Linux 发行版最适合新手入门。那样做只会引发争吵,让可怜的新手更加困惑。现在,争论的焦点是 systemd 是否是一个 好东西。以下是一些反对意见:

  • systemd 尝试做得太多,违反了 Unix 的理念——每个工具只做一件事,并且做到最好。

  • 它由一个大公司(Red Hat)控制。

  • 它是一个安全问题。

  • 它的 journald 组件将系统日志保存为二进制格式,一些人认为这比 rsyslog 创建的纯文本文件更容易损坏。

如果你客观看待这些问题,可能会发现反对意见并没有那么严重:

  • 是的,systemd 生态系统不仅仅包含 init 系统。它还包括网络、引导加载器、日志记录和登录组件。但这些组件都是可选的,并不是所有的 Linux 发行版在默认设置中都使用它们。

  • 它主要是由 Red Hat 创建的,项目负责人是 Red Hat 的一名员工。但 Red Hat 将其发布在一个自由软件许可证下,这意味着没有任何一家公司能够完全控制它。即便 Red Hat 突然决定未来版本的 systemd 要变成专有软件,免费代码仍然存在,某人会将其分叉成一个新的免费版本。

  • 是的,systemd 确实存在一些安全漏洞。但这同样适用于 OpenSSL、Bash shell,甚至是 Linux 内核本身。只有在这些漏洞没有被修复的情况下,才有理由抱怨 systemd 的安全性。

  • journald组件确实会创建二进制格式的日志文件。但仍然可以在systemd发行版上运行rsyslog,而且大多数都在运行。某些发行版,例如 Red Hat Enterprise Linux 8 系列,使用journald收集系统信息,然后让journald将信息传递给rsyslog,以创建正常的文本文件。因此,在 RHEL 8 中,我们获得了两全其美的方案。

systemd发布不久后,一些从未尝试过它的人发布了博文,解释为什么systemd是纯粹的邪恶,并表示他们永远不会使用它。几年前,我在我的 BeginLinux Guru 频道上创建了一个systemd教程播放列表。第一个视频叫做为什么选择 systemd? 很多人在评论中说他们永远不会使用systemd,并表示他们会切换到非systemd的 Linux 发行版或 FreeBSD 类型的发行版,以避免使用它。

底线是:所有企业级 Linux 发行版现在都使用systemd。所以,我认为它可能会长期存在。

总结

在本章中,我们回顾了最常见的 Linux 初始化系统的历史。我们看到了遗留初始化系统的不足之处,也了解了为什么systemd是一个更好的替代品。最后,我们讨论了反对systemd的观点。

学习systemd的挑战之一是,直到现在,关于它并没有任何真正的全面文档。Red Hat 网站上有基本的使用文档,但甚至没有覆盖systemd生态系统的所有组件。我能找到的只有两本关于systemd的书,已经有几年历史了。(一本书是关于 Fedora 的,另一本是关于 Ubuntu 的。)即使这些书也遗漏了一些内容。所以,我给自己设定的挑战是创建一本全面的、实用的systemd指南。在接下来的章节中,我会尽力完成这个目标。

在下一章中,我们将快速浏览systemd的目录和文件。我在那里等你。

问题

  1. 谁创建了最初的 SysV init系统?

    a. 贝尔实验室

    b. 红帽

    c. Debian

    d. Ubuntu

  2. 以下关于 SysV 哪个是正确的?

    a. 它是一个现代化、强大的init系统。

    b. 启动机器时,它可以并行启动服务。

    c. 启动机器时,它只能按顺序启动服务。

    d. 它具有systemd所没有的安全功能。

  3. 以下关于systemd哪个不是正确的?

    a. 它具有可以在某种程度上模拟强制访问控制系统的安全功能。

    b. 它可以并行启动服务。

    c. 它可以使用cgroups限制资源使用。

    d. 它是一个需要被替换的遗留系统。

答案

  1. A

  2. C

  3. D

深入阅读

第二章:理解 systemd 目录和文件

在本章中,我们将探讨各种systemd单元文件和配置文件,并解释几种类型的目的。我们还将简要了解与systemd相关的一些可执行文件。在此过程中,我们还会查看这些文件所在的目录。

这是本章将涵盖的主题:

  • 理解systemd配置文件

  • 理解systemd单元文件

  • 理解systemd可执行文件

本章中的主题包含systemd的基本基础知识。我们将在接下来的章节中以此为基础进行拓展。

如果你准备好了,我们开始吧。

技术要求

如果你想跟着我做,你需要准备几个虚拟机VMs)。在这里,我使用 Ubuntu Server 20.04 来处理 Ubuntu 方面的内容,使用 AlmaLinux 8 来处理 Red Hat 方面的内容。(你还会看到我用 Fedora 来指出一些内容,但你自己并不需要有 Fedora 虚拟机。)

查看以下链接,查看代码实操视频:bit.ly/3xL4os5

理解 systemd 配置文件

在本节中,我们将查看控制systemd各个组件操作的配置文件。如果你想跟随我一起操作,你使用的发行版不太重要,因为所有启用systemd的发行版大致是相同的。好了—现在你可能在对我大喊:

大致相同?为什么,Donnie,你之前告诉我们,systemd 在所有发行版中实现得都是一致的!这是怎么回事?

这是一致的,因为管理和控制命令在所有发行版中都是相同的,但systemd生态系统不仅仅包括init系统,还包括其他几个不同的组件。这些组件是可选的,某些 Linux 发行版在默认配置中并不使用所有组件。正如你在这里看到的,几个组件在/etc/systemd/目录下有配置文件:

[donnie@localhost systemd]$ pwd
/etc/systemd
[donnie@localhost systemd]$ ls -l *.conf
-rw-r--r--. 1 root root  720 May 31  2016 bootchart.conf
-rw-r--r--. 1 root root  615 Mar 26  2020 coredump.conf
-rw-r--r--. 1 root root 1041 Mar 26  2020 journald.conf
-rw-r--r--. 1 root root 1042 Mar 26  2020 logind.conf
-rw-r--r--. 1 root root  584 Mar 26  2020 networkd.conf
-rw-r--r--. 1 root root  529 Mar 26  2020 pstore.conf
-rw-r--r--. 1 root root  764 Mar 26  2020 resolved.conf
-rw-r--r--. 1 root root  790 Mar 26  2020 sleep.conf
-rw-r--r--. 1 root root 1762 Mar 26  2020 system.conf
-rw-r--r--. 1 root root  677 Mar 26  2020 timesyncd.conf
-rw-r--r--. 1 root root 1185 Mar 26  2020 user.conf
[donnie@localhost systemd]$

timesyncd.conf文件,你在前面的代码片段中看到它排在倒数第二的位置,是你在某些地方看不到的组件之一。它用于将机器的时间同步到一个可信的外部源。你在这里看到它,但在chronyd上是看不见的,而且仅仅看到某个systemd组件的配置文件并不意味着该组件正在使用。在我提取前面代码片段的 Fedora 机器上,networkdresolvedtimesyncd组件都被禁用了。(与 RHEL 发行版类似,Fedora 使用chronyd来保持时间同步,但它仍然安装了timesyncd组件。)另一方面,如果你查看最新版本的 Ubuntu Server,你会发现这些可选组件默认启用。(我们稍后会看到如何判断一个服务是启用还是禁用。)

好的——让我们来看看这些配置文件的内容。我们将从查看 system.conf 文件开始,它设置了 systemd init 进程的配置。(由于空间原因,我只能在这里显示文件的一部分。你可以通过在虚拟机上运行 less /etc/systemd/system.conf 来查看完整文件。)以下是文件片段:

[Manager]
#LogLevel=info
#LogTarget=journal-or-kmsg
#LogColor=yes
#LogLocation=no
. . .
. . .
#DefaultLimitNICE=
#DefaultLimitRTPRIO=
#DefaultLimitRTTIME=

现在,我不会一行行地解释这个文件,因为我不想让你因为无聊而讨厌我。但说真的,在正常情况下,你可能永远不需要更改这些配置文件。如果你认为你可能需要对它们做些什么,最好的办法是阅读它们的相关手册页,手册页会详细说明每个参数的作用。诀窍是,对于大多数这些文件,你需要在文件名的前面加上 systemd- 字符串,才能找到它们的手册页。例如,要查看 system.conf 文件的手册页,输入以下命令:

man systemd-system.conf

此外,你可能已经注意到,在所有这些配置文件中,每一行都是注释掉的。这并不意味着这些行没有效果。相反,这意味着这些是默认的编译参数。要更改某些内容,你需要取消注释你想更改的参数行,并修改它的值。

专业提示

你可以使用 apropos 命令查找所有包含特定文本字符串的手册页,无论是手册页名称还是手册页描述中包含该字符串。例如,要查找所有匹配 systemd 字符串的页面,只需输入以下命令:apropos systemd

你还可以输入 man -k systemd,这是 apropos systemd 的同义词。(我一开始就养成了总是输入 apropos 的习惯,并且一直没有改掉这个习惯。)如果尝试后没有任何结果,你可能需要重新构建手册页数据库,你可以通过输入 sudo mandb 来完成这一步。

好了,我想我们已经讨论够了配置文件。接下来,我们将讨论 systemd 单元文件。

理解 systemd 单元文件

不同于使用一组复杂的 Bash 脚本,systemd init 系统通过各种类型的 单元 文件来控制系统和服务操作。每个单元文件的文件名都会附带一个扩展名,说明它是哪种类型的单元文件。在我们查看这些文件之前,让我们先看看它们存放在哪里。

/lib/systemd/system/ 目录是操作系统默认的单元文件存放位置,或者是你可能安装的任何软件包附带的单元文件。你可能会遇到需要修改这些单元文件或甚至创建自己单元文件的情况,但你不会在这个目录下进行操作。相反,你会在 /etc/systemd/system/ 目录下进行。这个目录下与 /lib/systemd/system/ 目录下同名的单元文件具有优先权。

小贴士

你可以通过输入以下命令阅读关于单元文件的内容:man systemd.unit

在这个手册页的底部,你会看到它会引导你查看每种特定类型单元文件的其他手册页。你很快会发现,最棘手的部分是每当你需要查找有关某个特定单元配置参数的内容时,都必须在不同的手册页中查找。为了简化这个过程,你可以在systemd.directives手册页中查找特定的指令,这将引导你到包含该指令信息的手册页。

现在你知道了单元文件的位置,让我们来看一看它们是什么

单元文件的类型

/lib/systemd/system目录中,你会看到各种类型的单元文件,每种文件执行不同的功能。以下是一些常见类型的列表:

  • service:这些是服务的配置文件。它们取代了旧System VSysV)系统中的初始化脚本。

  • socket:套接字可以启用不同系统服务之间的通信,或者当接收到连接请求时,它们可以自动唤醒一个处于休眠状态的服务。

  • slice:切片单元用于配置cgroups。(我们将在第二部分理解 cgroups中详细介绍这些。)

  • mountautomount:这些包含由systemd控制的文件系统的挂载点信息。通常,它们会自动创建,因此你不需要做太多操作。

  • target:目标单元在系统启动时使用,用于分组单元并提供众所周知的同步点。(我们将在第六章理解 systemd 目标中详细讲解这些。)

  • timer:定时器单元用于按计划调度任务。它们取代了旧的 cron 系统。(我们将在第七章理解 systemd 定时器中与它们一起工作。)

  • path:路径单元用于通过基于路径的激活启动服务。(我们将在第三章理解服务、路径和套接字单元中介绍服务、路径和套接字单元。)

  • swap:交换单元包含关于交换分区的信息。

    关于我们单元文件的基本描述差不多就是这些。我们将在后续章节中深入探讨它们的细节。

理解 systemd 可执行文件

通常,我们会在bin/sbin/目录中查找程序的可执行文件,的确你会在那里找到一些systemd工具的可执行文件,但大多数systemd的可执行文件实际上位于/lib/systemd/目录。为了节省空间,下面只列出部分:

donnie@donnie-TB250-BTC:/lib/systemd$ ls -l
total 7448
-rw-r--r--  1 root root 2367728 Feb  6  2020 libsystemd-shared-237.so
drwxr-xr-x  2 root root    4096 Apr  3  2020 network
-rw-r--r--  1 root root     699 Feb  6  2020 resolv.conf
-rwxr-xr-x  1 root root    1246 Feb  6  2020 set-cpufreq
drwxr-xr-x 24 root root   36864 Apr  3  2020 system
-rwxr-xr-x  1 root root 1612152 Feb  6  2020 systemd
-rwxr-xr-x  1 root root    6128 Feb  6  2020 systemd-ac-power
-rwxr-xr-x  1 root root   18416 Feb  6  2020 systemd-backlight
-rwxr-xr-x  1 root root   10304 Feb  6  2020 systemd-binfmt
-rwxr-xr-x  1 root root   10224 Feb  6  2020 systemd-cgroups-agent
-rwxr-xr-x  1 root root   26632 Feb  6  2020 systemd-cryptsetup
. . .
. . .

你会看到systemd本身的可执行文件在这里,还有systemd作为其系统的一部分运行的服务可执行文件。在一些 Linux 发行版中,你会在/bin/usr/bin目录下看到指向这些可执行文件的符号链接。大部分情况下,你不会直接与这些文件交互,所以让我们继续讲解你会互动的内容。

systemctl工具用于控制systemd,你会经常使用它。它是一个多功能工具,可以为你做很多事情。它允许你查看不同的单元及其状态,并可以启用或禁用它们。现在,我们将查看一些systemctl命令,它们允许你查看不同类型的信息。稍后我们将讨论如何使用systemctl控制和编辑特定单元。如果你想跟着操作,启动一个虚拟机,开始动手吧。

需要注意的一点是,一些systemctl命令需要根权限,而另一些则不需要。如果你只是查看系统或单元信息,可以使用普通用户权限。如果需要更改配置,则需要使用根用户权限。好吧,我们开始吧。

我们首先列出systemd当前在内存中的活动单元。我们可以使用systemctl list-units命令来做到这一点。输出内容非常长,所以我这里只展示前几行:

[donnie@localhost ~]$ systemctl list-units
UNIT                                                                                      LOAD   ACTIVE SUB        DESCRIPTION
proc-sys-fs-binfmt_misc.automount                                                         loaded active waiting   Arbitrary Executable File Formats File System Automount Point
sys-devices-pci0000:00-0000:00:17.0-ata3-host2-target2:0:0-2:0:0:0-block-sda-sda1.device  loaded active plugged   WDC_WDS250G2B0A-00SM50 1
sys-devices-pci0000:00-0000:00:17.0-ata3-host2-target2:0:0-2:0:0:0-block-sda-sda2.device  loaded active plugged   WDC_WDS250G2B0A-00SM50 2
sys-devices-pci0000:00-0000:00:17.0-ata3-host2-target2:0:0-2:0:0:0-block-sda.device       loaded active plugged   WDC_WDS250G2B0A-00SM50
sys-devices-pci0000:00-0000:00:1b.2-0000:02:00.1-sound-card1.device                       loaded active plugged   GP104 High Definition Audio Controller
sys-devices-pci0000:00-0000:00:1b.3-0000:03:00.1-sound-card2.device                       loaded active plugged   GP104 High Definition Audio Controller
. . .
. . .

这是automount部分,展示了已挂载的各种设备。正如你所看到的,这不仅仅包括存储设备。

接下来,我们有挂载、路径和作用域单元,具体如下:

. . .
. . .
-.mount                                                                                   loaded active mounted   /
boot.mount                                                                                loaded active mounted   /boot
dev-hugepages.mount                                                                       loaded active mounted   Huge Pages File System
dev-mqueue.mount                                                                          loaded active mounted   POSIX Message Queue File System
home.mount                                                                                loaded active mounted   /home
run-user-1000.mount                                                                       loaded active mounted   /run/user/1000
sys-fs-fuse-connections.mount                                                             loaded active mounted   FUSE Control File System
sys-kernel-config.mount                                                                   loaded active mounted   Kernel Configuration File System
sys-kernel-debug.mount                                                                    loaded active mounted   Kernel Debug File System
tmp.mount                                                                                 loaded active mounted   Temporary Directory (/tmp)
var-lib-nfs-rpc_pipefs.mount                                                              loaded active mounted   RPC Pipe File System
cups.path                                                                                 loaded active running   CUPS Scheduler
systemd-ask-password-plymouth.path                                                        loaded active waiting   Forward Password Requests to Plymouth Directory Watch
systemd-ask-password-wall.path                                                            loaded active waiting   Forward Password Requests to Wall Directory Watch
init.scope                                                                                loaded active running   System and Service Manager
session-1.scope                                                                           loaded active abandoned Session 1 of user donnie
session-3.scope                                                                           loaded active abandoned Session 3 of user donnie
session-4.scope                                                                           loaded active running   Session 4 of user donnie
. . .
. . .

请注意,这里每个分区都有一个挂载单元。

继续向下滚动,你将看到服务、切片、套接字、交换、目标和定时器单元的类似显示。在底部,你将看到状态代码的简要说明以及简短的总结,内容如下:

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.
182 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.
lines 136-190/190 (END)

使用--all选项还可以查看非活动的单元,像这样:

[donnie@localhost ~]$ systemctl list-units --all
  UNIT                                                                                                           LOAD      ACTIVE   SUB       DESCRIPTION
● boot.automount                                                                                                 not-found inactive dead      boot.automount
  proc-sys-fs-binfmt_misc.automount                                                                              loaded    active   waiting   Arbitrary Executable File Formats File System A
  dev-block-8:2.device                                                                                           loaded    active   plugged   WDC_WDS250G2B0A-00SM50 2
  dev-disk-by\x2did-ata\x2dWDC_WDS250G2B0A\x2d00SM50_181202802064.device                                         loaded    active   plugged   WDC_WDS250G2B0A-00SM50
  dev-disk-by\x2did-ata\x2dWDC_WDS250G2B0A\x2d00SM50_181202802064\x2dpart1.device                                 loaded    active   plugged   WDC_WDS250G2B0A-00SM50 1
  dev-disk-by\x2did-ata\x2dWDC_WDS250G2B0A\x2d00SM50_181202802064\x2dpart2.device                                loaded    active   plugged   WDC_WDS250G2B0A-00SM50 2
. . .
. . .

那是运气。我们在最上面找到了一单位未激活的单元。

你还可以使用-t选项查看特定类型的单元。例如,要查看仅服务单元,可以运行以下命令:

[donnie@localhost ~]$ systemctl list-units -t service
UNIT                                                                                      LOAD   ACTIVE SUB     DESCRIPTION
abrt-journal-core.service                                                                 loaded active running Creates ABRT problems from coredumpctl messages
abrt-oops.service                                                                         loaded active running ABRT kernel log watcher
abrt-xorg.service                                                                         loaded active running ABRT Xorg log watcher
abrtd.service                                                                             loaded active running ABRT Automated Bug Reporting Tool
alsa-state.service                                                                        loaded active running Manage Sound Card State (restore and store)
atd.service                                                                               loaded active running Deferred execution scheduler
auditd.service                                                                            loaded active running Security Auditing Service
avahi-daemon.service                                                                      loaded active running Avahi mDNS/DNS-SD Stack
. . .
. . .

你也可以以相同的方式查看其他单元。

现在,假设我们只想查看已死掉的服务。我们可以使用--state选项,像这样:

[donnie@localhost ~]$ systemctl list-units -t service --state=dead
  UNIT                                   LOAD      ACTIVE    SUB  DESCRIPTION
  abrt-vmcore.service                    loaded    inactive dead Harvest vmcores for ABRT
  alsa-restore.service                   loaded    inactive dead Save/Restore Sound Card State
  auth-rpcgss-module.service             loaded    inactive dead Kernel Module supporting RPCSEC_GSS
● autofs.service                         not-found inactive dead autofs.service
  blk-availability.service               loaded    inactive dead Availability of block devices
  dbxtool.service                        loaded    inactive dead Secure Boot DBX (blacklist) updater
  dm-event.service                       loaded    inactive dead Device-mapper event daemon
  dmraid-activation.service              loaded    inactive dead Activation of DM RAID sets
  dnf-makecache.service                  loaded    inactive dead dnf makecache
  dracut-cmdline.service                 loaded    inactive dead dracut cmdline hook
. . .
. . .

运行systemctl --state=help,你将看到一个列表,列出你可以查看的所有不同单元类型的状态。

除了查看当前在内存中的单元外,你还可以通过运行以下命令来查看系统中安装的单元文件:

[donnie@localhost ~]$ systemctl list-unit-files
UNIT FILE                                            STATE
proc-sys-fs-binfmt_misc.automount                    static
-.mount                                              generated
boot.mount                                           generated
dev-hugepages.mount                                  static
dev-mqueue.mount                                     static
home.mount                                           generated
proc-fs-nfsd.mount                                   static
. . .
. . .
session-1.scope                                      transient
session-3.scope                                      transient
session-4.scope                                      transient
abrt-journal-core.service                            enabled
abrt-oops.service                                    enabled
abrt-pstoreoops.service                              disabled
abrt-vmcore.service                                  enabled
abrt-xorg.service                                    enabled
abrtd.service                                        enabled
. . .
. . .

在这里,你会看到一些可能看起来很奇怪的东西。在顶部,你会看到一些处于 generated 状态的挂载文件。这些文件位于 /run/systemd/units/ 目录,并由 systemd 自动生成。为了创建这些挂载文件,systemd 每次启动机器或手动重新加载 fstab 文件时,都会读取 /etc/fstab 文件。

static 状态的单元文件是你无法启用或禁用的。相反,其他单元会将这些静态单元作为依赖项调用。

处于 transient 状态的单元文件处理的是暂时性的事物。在这里,我们看到三个作用域单元,它们管理三个用户会话。当用户注销会话时,这些单元中的一个会消失。

当然,处于 enabled 状态的单元会在机器启动时自动启动,而处于 disabled 状态的单元则不会。

要查看单个单元是否启用或活动,你可以使用 systemctlis-enabledis-active 选项。之前,我告诉你 networkdresolvedtimesyncd 服务在我的 Fedora 机器上都是禁用的。下面是证明这一点的方法:

[donnie@localhost ~]$ systemctl is-enabled systemd-timesyncd
disabled
[donnie@localhost ~]$ systemctl is-enabled systemd-networkd
disabled
[donnie@localhost ~]$ systemctl is-enabled systemd-resolved
disabled
[donnie@localhost ~]$

这就是证明它们不活跃的方法:

[donnie@localhost ~]$ systemctl is-active systemd-timesyncd
inactive
[donnie@localhost ~]$ systemctl is-active systemd-resolved
inactive
[donnie@localhost ~]$ systemctl is-active systemd-timesyncd
inactive
[donnie@localhost ~]$

另一方面,NetworkManager 服务在我的 Fedora 机器上是启用且活动的,正如你在这里看到的:

[donnie@localhost ~]$ systemctl is-enabled NetworkManager
enabled
[donnie@localhost ~]$ systemctl is-active NetworkManager
active
[donnie@localhost ~]$

现在,我将留给你去验证所有这些内容,尤其是在 Ubuntu 机器上。

你还可以查看仅某一类型单元文件的信息。在这里,我们将仅查看交换单元文件的信息:

[donnie@localhost units]$ systemctl list-unit-files -t swap
UNIT FILE                                            STATE
dev-mapper-fedora_localhost\x2d\x2dlive\x2dswap.swap generated
1 unit files listed.
[donnie@localhost units]$

就像它处理挂载单元文件一样,systemd 通过读取 /etc/fstab 文件生成了这个文件。

之前,我向你展示了 /etc/systemd/system.conf 文件,该文件设置了 systemd 的全局配置。使用 show 选项,你可以通过运行 systemctl show 来查看实际运行的配置。以下是部分输出:

[donnie@localhost ~]$ systemctl show
Version=v243.8-1.fc31
Features=+PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD +IDN2 -IDN +PCRE2 default-hierarchy=unified
Architecture=x86-64
Tainted=local-hwclock
FirmwareTimestampMonotonic=0
LoaderTimestampMonotonic=0
KernelTimestamp=Thu 2021-03-11 11:58:01 EST
KernelTimestampMonotonic=0
. . .
. . .
DefaultLimitRTPRIOSoft=0
DefaultLimitRTTIME=infinity
DefaultLimitRTTIMESoft=infinity
DefaultTasksMax=4608
TimerSlackNSec=50000
DefaultOOMPolicy=stop

使用 --property= 选项可以仅查看某一项,例如:

[donnie@localhost ~]$ systemctl show --property=DefaultLimitSIGPENDING
DefaultLimitSIGPENDING=15362
[donnie@localhost ~]$

systemctl 有一个手册页,你可以随时查阅。但如果你只是需要快速参考,可以运行 systemctl -h

好的,我觉得现在差不多了。那我们就把这一章总结一下,收尾吧。

小结

好的,我们已经迅速进入正题并覆盖了相当多的概念。我们介绍了各种类型的配置文件和单元文件,并了解了它们存放的位置。最后,我们使用 systemctl 命令查看了我们运行系统的信息。

在下一章,我们将通过展示服务、路径和套接字单元文件的内部工作原理来进一步探讨这一点。我们下次见。

问题

  1. 以下哪个命令可以告诉你当前运行的 systemd 配置?

    a. systemctl list

    b. systemctl show

    c. systemd show

    d. systemd list

  2. 以下哪一项陈述是正确的?

    a. 要配置你的磁盘分区,你需要手动配置挂载单元。

    b. 当 systemd 读取 fstab 文件时,你的磁盘分区挂载单元会自动生成。

    c. 您的驱动分区的挂载单元是静态单元。

    d. 您的驱动分区不需要挂载单元。

  3. 以下哪个命令可以告诉你 NetworkManager 服务是否正在运行?

    a. systemctl active NetworkManager

    b. systemd active NetworkManager

    c. systemd enabled NetworkManager

    d. systemctl is-enabled NetworkManager

    e. systemctl is-active NetworkManager

答案

  1. a

  2. b

  3. c

进一步阅读

systemd 单元和单元文件:

www.digitalocean.com/community/tutorials/understanding-systemd-units-and-unit-files

第三章:理解服务单元、路径单元和套接字单元

在本章中,我们将检查服务单元、路径单元和套接字单元文件的内部工作原理。我们将检查每个文件中的部分内容,并查看你可以设置的一些参数。在这个过程中,我还会给你一些提示,教你如何查找有关各个参数的使用信息。

本章我们将讨论以下主题:

  • 理解服务单元

  • 理解套接字单元

  • 理解路径单元

在你成为 Linux 管理员的过程中,你可能会被要求修改现有单元或创建新的单元。本章的知识可以帮助你实现这一点。所以,如果你准备好了,就开始吧。

技术要求

和往常一样,我将在 Ubuntu Server 20.04 虚拟机和 Alma Linux 8 虚拟机上进行演示。你可以随时启动自己的虚拟机来跟着做。

查看以下链接以观看《代码实战》视频:bit.ly/2ZQBHh6

理解服务单元

服务单元相当于旧版 SysV 系统中的 init 脚本。我们将使用它们来配置我们的各种服务,这些服务在过去我们称之为守护进程。服务几乎可以是任何你希望自动启动并在后台运行的东西。服务的例子包括安全外壳(SSH)、你选择的 Web 服务器、邮件服务器以及系统正常运行所需的各种服务。虽然有些服务文件可能简短明了,其他则可能相当长,包含更多启用的选项。要了解所有这些选项,只需输入以下命令:

man systemd.directives

你可以设置的所有参数的描述分布在多个不同的手册页中。这个systemd.directives手册页是一个索引,它会引导你找到每个参数的正确手册页。

与其尝试解释服务文件可以使用的每个参数,不如让我们通过一些示例文件来解释它们的作用。

理解 Apache 服务文件

我们将从 Apache Web 服务器的服务文件开始。在我的 Ubuntu Server 20.04 虚拟机中,它是/lib/systemd/system/apache2.service文件。首先需要注意的是,服务单元文件被分为三个部分。顶部部分是[Unit]部分,它包含可以放入任何类型单元文件中的参数。它看起来像这样:

[Unit]
Description=The Apache HTTP Server
After=network.target remote-fs.target nss-lookup.target
Documentation=https://httpd.apache.org/docs/2.4/ 

在这里,我们看到这三个参数:

  • Description=:好吧,这个应该是相当自解释的。它的作用是告诉用户这个服务是什么。systemctl status命令从这一行获取其描述信息。

  • After=:我们不希望 Apache 在某些其他事情发生之前启动。我们还没有讨论target文件,但没关系。现在,只需知道我们希望阻止 Apache 启动,直到网络、任何可能的附加远程文件系统和名称切换服务可用之后。

  • Documentation=:这是另一个不言自明的参数。它只是显示了 Apache 文档的位置。

要了解可以放置在任何单元文件[Unit]部分的选项,请输入以下内容:

man systemd.unit

接下来,我们有[Service]部分,这里有些更有趣的参数可以放在服务单元文件中,看起来像这样:

[Service]
Type=forking
Environment=APACHE_STARTED_BY_SYSTEMD=true
ExecStart=/usr/sbin/apachectl start
ExecStop=/usr/sbin/apachectl stop
ExecReload=/usr/sbin/apachectl graceful
PrivateTmp=true
Restart=on-abort

在这个特定的文件中,我们看到了这些参数:

  • Type=:在systemd.service手册页面上描述了几种不同的服务类型。在这种情况下,我们有forking类型,这意味着第一个启动的 Apache 进程将生成一个子进程。当 Apache 启动完成并且建立了适当的通信通道后,原始进程——进程将退出,子进程将继续作为主服务进程运行。当父进程退出时,systemd服务管理器最终将认为服务已完全启动。根据手册页面的说法,这是 Unix 服务的传统行为,systemd只是延续了这一传统。

  • Environment=:这设置了一个影响服务行为的环境变量。在这种情况下,它告诉 Apache 它是由systemd启动的。

  • ExecStart=, ExecStop=ExecReload=:这三行指向 Apache 可执行文件的位置,并指定了启动、停止和重新加载服务的命令参数。

  • PrivateTmp=:许多服务因各种原因而写入临时文件,你可能习惯于在每个人都可以访问的/tmp/目录中看到它们。在这里,我们看到了一个很酷的systemd安全特性。当设置为true时,这个参数会强制 Apache 服务将其临时文件写入一个私有的/tmp/目录,其他人无法访问。因此,如果你担心 Apache 可能会将敏感信息写入其临时文件,你会想使用这个特性。(你可以在systemd.exec手册页面上阅读更多关于这个特性以及其他安全特性的信息。)另外,请注意,如果你完全不设置这个参数,它将默认为false,这意味着你将不会得到这种保护。

  • Restart=:有时候,如果服务停止了,你可能希望它自动重新启动。在这种情况下,我们使用了on-abort参数,这意味着如果 Apache 服务因不正常的信号而崩溃,systemd会自动重新启动它。

好的,[Service]部分就到此为止。让我们继续[Install]部分,看起来像这样:

[Install]
WantedBy=multi-user.target

这个命名法看起来有点奇怪,因为我们似乎并没有在这里安装什么东西。实际上,它的作用是控制在启用或禁用某个单元时会发生什么。在这种情况下,我们表示希望 Apache 服务在multi-user.target单元中启用,这将导致服务在机器启动到多用户目标时自动启动。(我们稍后会介绍目标和启动过程。现在,只需要理解多用户目标是指机器完全启动并准备好使用的时候。对于老一辈的 SysV 用户来说,这里的目标相当于 SysV 的运行级别。)

理解安全外壳服务文件

有一些不同的内容,让我们看看安全外壳服务的服务文件,在这台 Ubuntu 机器上,它是/lib/systemd/system/ssh.service文件。这里是[Unit]部分:

[Unit]
Description=OpenBSD Secure Shell server
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target auditd.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run

[Unit]部分,我们看到ConditionPathExists=参数,这是之前没有看到的。它检查文件是否存在或不存在。在这种情况下,我们在文件路径前面看到一个感叹号(!),这意味着我们在检查指定文件的不存在。如果systemd发现该文件,它将不会启动安全外壳服务。如果我们去掉感叹号,systemd只有在该文件存在时才会启动服务。所以,如果我们想防止安全外壳服务启动,所需要做的就是在/etc/ssh/目录中创建一个虚拟文件,如下所示:

sudo touch /etc/ssh/sshd_not_to_be_run

我不确定这个功能到底有多有用,因为如果你不希望服务运行,直接禁用它也一样简单。但是,如果你觉得以后可能会用到这个功能,它就在那里为你提供。

接下来是[Service]部分:

[Service]
EnvironmentFile=-/etc/default/ssh
ExecStartPre=/usr/sbin/sshd -t
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS
ExecReload=/usr/sbin/sshd -t
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartPreventExitStatus=255
Type=notify
RuntimeDirectory=sshd
RuntimeDirectoryMode=0755

[Service]部分,我们看到了一些新的参数:

  • EnvironmentFile=:这个参数会让systemd从指定的文件中读取环境变量列表。路径前的减号(-)告诉systemd如果文件不存在,不用担心,还是照常启动服务。

  • ExecStartPre=:这告诉systemd在启动服务之前执行一个指定的命令,通过ExecStart=参数来启动服务。在这个例子中,我们希望运行sshd -t命令,它会测试安全外壳配置,以确保其有效。

  • KillMode=:我之前已经告诉过你,systemd的一个优点是它能在你需要向服务发送杀死信号时,停止该服务的所有进程。如果你没有在服务文件中包含这个参数,它就是默认行为。不过,有时你可能并不想这样。通过将此参数设置为process,杀死信号将只会终止服务的主进程,所有其他关联进程将继续运行。(你可以在systemd.kill手册页中阅读更多关于此参数的内容。)

  • Restart=:这次,它将不再因为on-abort自动重启停止的服务,而是因为on-failure重新启动。因此,除了因为异常信号重新启动服务外,systemd还会因不正常的退出代码、超时或看门狗事件重新启动该服务。(如果你在想,看门狗是一种内核特性,它可以在发生某些无法恢复的错误时重新启动服务。)

  • RestartPreventExitStatus=:当接收到某个退出代码时,这会防止服务自动重启。在这个例子中,我们不希望服务在退出代码为255时重启。(有关退出代码的更多信息,请参见systemd.exec手册页中的$EXIT_CODE, $EXIT_STATUS_部分。)

  • Type=:对于此服务,类型为notify,而不是我们在前一个示例中看到的forking。这意味着当服务启动完成后,服务将发送一个通知消息。发送通知消息后,systemd将继续加载后续单元。

  • RuntimeDirectory=RuntimeDirectoryMode=:这两个指令会在/run/目录下创建一个运行时目录,然后设置该目录的权限值。在这个例子中,我们将目录的权限设置为0755,这意味着目录的所有者将拥有读、写和执行权限,而其他人将仅有读和执行权限。

最后,这里是[Install]部分:

[Install]
WantedBy=multi-user.target
Alias=sshd.service

[Install]部分,我们看到Alias=参数,这非常实用。因为某些服务在不同的 Linux 发行版上可能有不同的名称。例如,安全外壳服务在 Red Hat 类型的系统上是sshd,而在 Debian/Ubuntu 系统上是ssh。通过包含Alias=sshd.service这一行,我们可以通过指定任意一个名称来控制服务。

理解 timesyncd 服务文件

对于最后一个示例,我想向你展示timesyncd服务的服务文件。它是/lib/systemd/system/systemd-timesyncd.service文件。首先是[Unit]部分:

[Unit]
Description=Network Time Synchronization
Documentation=man:systemd-timesyncd.service(8)
ConditionCapability=CAP_SYS_TIME
ConditionVirtualization=!container
DefaultDependencies=no
After=systemd-sysusers.service
Before=time-set.target sysinit.target shutdown.target
Conflicts=shutdown.target
Wants=time-set.target time-sync.target

对于这个文件,我主要想关注与安全相关的参数。在[Unit]部分,有ConditionCapability=参数,稍后我将解释。Wants=行与安全无关,它定义了该服务的依赖单元。如果这些依赖单元在该服务启动时未运行,systemd将尝试启动它们。如果它们未能启动,该服务仍将继续启动。

接下来,我们将查看[Service]部分,在这里我们将看到更多与安全相关的参数。(由于篇幅限制,这里只展示文件的一部分,欢迎在您自己的虚拟机上查看完整文件。)

[Service]
AmbientCapabilities=CAP_SYS_TIME
CapabilityBoundingSet=CAP_SYS_TIME
ExecStart=!!/lib/systemd/systemd-timesyncd
LockPersonality=yes
MemoryDenyWriteExecute=yes
. . .
. . .
ProtectSystem=strict
Restart=always
RestartSec=0
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RuntimeDirectory=systemd/timesync
StateDirectory=systemd/timesync
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service @clock
Type=notify
User=systemd-timesync
WatchdogSec=3min 

AmbientCapabilities=CapabilityBoundingSet= 参数都设置为 CAP_SYS_TIME[Unit] 部分的 ConditionCapability= 参数也是如此。在 [Service] 部分的末尾,我们看到 User=systemd-timesync 行,这告诉 systemd 以非特权帐户运行此服务。但是,设置系统时间需要 root 权限,而 systemd-timesync 用户没有该权限。我们可以通过为此用户分配一个根级别的内核能力来解决这个问题。在这种情况下,我们允许此用户设置系统时间,但不允许做其他操作。不过,一些系统可能无法实现 AmbientCapabilities= 指令。因此,ExecStart= 行中的双重感叹号 (!!) 告诉 systemd 以最小权限运行指定的服务。请注意,只有在系统无法处理 AmbientCapabilities= 指令时,这个双重感叹号选项才会生效。

注意

你可以通过输入 man capabilities 了解更多关于内核能力的信息。了解内核能力的一个重要点是,它们在不同的 CPU 架构之间可能有所不同。因此,适用于 ARM CPU 的能力集合与适用于 x86_64 CPU 的能力集合不同。

阅读 [Service] 部分的其余内容,你会看到很多显然是为了增强安全性的参数。我不会一一讲解,因为大多数参数你仅从名称就能大致知道它们的作用。对于那些不太明显的参数,我建议你查阅手册页。这些安全设置是一个强大的功能,你可以看到它们几乎在做与强制访问控制系统相同的工作。

最后,我们有 [Install] 部分:

[Install]
WantedBy=sysinit.target
Alias=dbus-org.freedesktop.timesync1.service

这里要注意的主要一点是,这个服务是 sysinit.target 所需要的,这意味着它会在系统初始化过程中启动。

我们只是略微触及了服务文件的应用范围。但由于有如此多不同的参数,我们只能合理地期望做一些简单的探索。你最好的做法是浏览手册页,熟悉相关内容,并在有问题时查阅手册页。

接下来,我们将讨论套接字单元。(幸运的是,这一部分不需要太长。)

理解套接字单元

套接字单元文件也位于 /lib/systemd/system/ 目录下,文件名以 .socket 结尾。以下是我在一台 Ubuntu 服务器机器上的部分文件列表:

donnie@ubuntu20-10:/lib/systemd/system$ ls -l *.socket
-rw-r--r-- 1 root root  246 Jun  1  2020 apport-forward.socket
-rw-r--r-- 1 root root  102 Sep 10  2020 dbus.socket
-rw-r--r-- 1 root root  248 May 30  2020 dm-event.socket
-rw-r--r-- 1 root root  197 Sep 16 16:52 docker.socket
-rw-r--r-- 1 root root  175 Feb 26  2020 iscsid.socket
-rw-r--r-- 1 root root  239 May 30  2020 lvm2-lvmpolld.socket
-rw-r--r-- 1 root root  186 Sep 11  2020 multipathd.socket
-rw-r--r-- 1 root root  281 Feb  2 08:21 snapd.socket
-rw-r--r-- 1 root root  216 Jun  7  2020 ssh.socket
. . .
. . .
-rw-r--r-- 1 root root  610 Sep 20 10:16 systemd-udevd-kernel.socket
-rw-r--r-- 1 root root  126 Aug 30  2020 uuidd.socket
donnie@ubuntu20-10:/lib/systemd/system$

Socket 单元可以为我们做几件事情。首先,它们可以代替旧的 SysV 系统上的 inetdxinetd 超级服务器 守护进程。这意味着,我们可以避免让一个服务器守护进程一直运行,即使在不需要的时候也要一直运行。相反,我们可以大部分时间将它关闭,只在系统检测到一个进来的网络请求时才启动它。我们来看一个简单的例子,看看 Ubuntu 机器上的ssh.socket文件:

[Unit]
Description=OpenBSD Secure Shell server socket
Before=ssh.service
Conflicts=ssh.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run
[Socket]
ListenStream=22
Accept=yes
[Install]
WantedBy=sockets.target

即使这个 socket 文件默认情况下会被安装,但它并不会默认启用。在 Ubuntu 的默认配置下,Secure Shell 服务是一直在运行的。在[Unit]部分,我们可以看到这两个有趣的指令:

  • Before=ssh.service:这告诉systemd在启动 Secure Shell 服务之前先启动这个 socket。

  • Conflicts=ssh.service:这告诉systemd,如果启用了这个 socket,就不允许 Secure Shell 服务正常运行。如果你启用这个 socket,正常的 SSH 服务会被关闭。

[Socket]部分,我们看到这个 socket 监听在端口22/tcp,这是 Secure Shell 的默认端口。Accept=yes这一行有点具有误导性,因为它并不完全意味着你想象的那样。它实际上是指每当有新的连接请求时,服务会为每个请求生成一个新的实例。根据systemd.socket的手册页面,这个设置应该仅用于设计时考虑到旧的inetdxinetd方案的服务。为了更好的性能,新的服务应该设计成不以这种方式运行。

为了演示这个是如何工作的,我首先想向你展示一下我在 Ubuntu 虚拟机上运行的ssh服务是否正常:

donnie@ubuntu20-10:~$ sudo systemctl is-active ssh
active
donnie@ubuntu20-10:~$

所以,它是active的,这意味着它作为一个正常的守护进程在运行。现在,让我们启用ssh.socket,然后看一下差异:

donnie@ubuntu20-10:~$ sudo systemctl enable --now ssh.socket
Created symlink /etc/systemd/system/sockets.target.wants/ssh.socket → /lib/systemd/system/ssh.socket.
donnie@ubuntu20-10:~$ sudo systemctl is-active ssh
inactive
donnie@ubuntu20-10:~$

所以,一旦我启用这个 socket,Conflicts=这一行就会自动关闭ssh服务。但是我仍然可以连接到这台机器,因为这个 socket 会自动启动 SSH 服务,恰好足够长的时间来处理连接请求。当服务不再需要时,它会自动进入休眠状态。

其次,注意到这个 socket 并没有提到启动哪个服务,或者它的可执行文件在哪。这是因为,当该 socket 被激活时,它会从ssh.service文件中自动获取这些信息。你不需要告诉它怎么做,因为任何 socket 文件的默认行为是从一个文件名相同前缀的服务文件中获取它的信息。

最后,socket 单元可以启用操作系统进程之间的通信。例如,一个 socket 可以从各种系统进程接收消息并将它们传递给日志系统,正如我们在这个systemd-journald.socket文件中看到的那样:

[Unit]
Description=Journal Socket
Documentation=man:systemd-journald.service(8) man:journald.conf(5)
DefaultDependencies=no
Before=sockets.target
. . .
. . .
IgnoreOnIsolate=yes
[Socket]
ListenStream=/run/systemd/journal/stdout
ListenDatagram=/run/systemd/journal/socket
SocketMode=0666
PassCredentials=yes
PassSecurity=yes
ReceiveBuffer=8M
Service=systemd-journald.service 

我们在这里看到的是,这个套接字并没有监听网络端口,而是监听来自 /run/systemd/journal/stdout 的 TCP 输出,以及来自 /run/systemd/journal/socket 的 UDP 输出。(ListenStream= 指令用于 TCP 源,ListenDatagram= 指令用于 UDP 源。systemd.socket 手册页没有明确说明这一点,所以你需要通过 DuckDuckGo 搜索来了解这一点。)

这里没有 Accept=yes 指令,因为与我们之前看到的安全 shell 服务不同,journald 服务不需要为每个传入连接生成一个新的实例。省略此设置时,它的默认值为 no

PassCredentials=yes 行和 PassSecurity=yes 行使发送过程将安全凭证和安全上下文信息传递给接收套接字。如果省略这两个参数,它们的默认值为 no。为了提高性能,ReceiveBuffer= 行为缓冲区分配了 8 MB 的内存。

最后,Service= 行指定了服务。根据 systemd.socket 的手册页,只有在设置了 Accept=no 时才能使用此选项。手册页还指出,通常不需要此项,因为默认情况下,套接字仍会引用与套接字同名的服务文件。但如果使用此项,它可能会引入一些额外的依赖项,原本不会引入这些依赖。

理解路径单元

你可以使用路径单元让 systemd 监控特定文件或目录,以查看它们何时发生变化。当 systemd 检测到文件或目录发生变化时,它将激活指定的服务。我们将以通用 Unix 打印系统CUPS)为例。

/lib/systemd/system/cups.path 文件中,我们看到:

[Unit]
Description=CUPS Scheduler
PartOf=cups.service
[Path]
PathExists=/var/cache/cups/org.cups.cupsd
[Install]
WantedBy=multi-user.target

PathExists= 行告诉 systemd 监控特定文件的变化,在这个例子中是 /var/cache/cups/org.cups.cupsd 文件。如果 systemd 检测到此文件的变化,它将激活打印服务。

总结

好了,我们又完成了一个章节,这是件好事。在本章中,我们研究了服务、套接字和路径单元文件的结构。我们看到了每种类型单元的三个部分,并查看了一些可以为这些部分定义的参数。当然,解释每个可用参数几乎是不可能的,所以我只展示了几个例子。接下来的几个章节中,我会给你更多的例子。

任何 IT 管理员都应该具备的一个重要技能是知道如何查找自己不知道的内容。对于 systemd 来说,这可能有点挑战性,因为相关信息分布在多个手册页中。我已经为你提供了一些查阅手册页的技巧,希望能帮到你。

接下来你需要掌握的技能是控制服务单元,这是下一章的主题。我们在那里见。

问题

  1. 哪种单元用于监控文件和目录的变化?

    a. system

    b. 文件

    c. 路径

    d. 定时器

    e. 服务

  2. 套接字单元可以:

    a. 如果有网络请求进入,自动通知用户

    b. 自动设置 Linux 和 Windows 机器之间的通信

    c. 监听网络连接,并充当防火墙

    d. 在检测到对该服务的连接请求时自动启动网络服务

  3. [Install] 部分的目的是什么?

    a. 它定义了在安装服务时需要安装的其他软件包。

    b. 它定义了启用或禁用单元时发生的情况。

    c. 它定义了特定于安装单元的参数。

    d. 它定义了特定于服务单元的参数。

答案

  1. c

  2. d

  3. b

深入阅读

Systemd 套接字单元:

www.linux.com/training-tutorials/end-road-systemds-socket-units/

ListenStreamListenDatagram= 的区别:

unix.stackexchange.com/questions/517240/systemd-socket-listendatagram-vs-listenstream

监控路径和目录:

www.linux.com/topic/desktop/systemd-services-monitoring-files-and-directories/

第四章:控制 systemd 服务

现在我们已经了解了 systemd 服务,接下来是学习如何控制它们。在本章中,我们将专注于此。具体而言,我们将涵盖以下技能:

  • 验证服务状态

  • 启动、停止和重新加载服务

  • 启用和禁用服务

  • 杀死一个服务

  • 屏蔽服务

这些技能非常实用,因为作为一名 Linux 服务器管理员,你会在日常工作中经常练习它们。所以,如果你准备好了,我们开始吧。

技术要求

本章所需的仅是某种虚拟机,并且你自己的用户账户需具有完全的 sudo 权限。对于我的演示,我将使用全新的 AlmaLinux 8,代表 RHEL(红帽企业版 Linux)方面,Ubuntu Server 20.04 代表 Ubuntu 方面。

查看以下链接以观看《代码实战》视频:bit.ly/3oev29P

关于 CentOS Linux 的说明

我知道,你可能习惯于在这些演示中看到 CentOS Linux。但在 2020 年底,Red Hat 公司宣布他们将在 2021 年底停止对 CentOS 8 企业版的支持。它的替代品 CentOS Stream 是一个滚动发布的发行版,可能不适合在企业中使用。幸运的是,其他组织提供了适合企业的 CentOS 8 替代品,包括 Oracle Enterprise Linux 8、Springdale Linux 8 和 Alma Linux 8。在撰写本文时,Rocky Linux 8 正在规划阶段,最终将由原 CentOS 项目的创始人发布。目前,还无法确定哪个将成为 CentOS 的最受欢迎替代品。(当然,还有 Red Hat Enterprise Linux 8 (RHEL 8),但你需要购买订阅才能进行有意义的操作。)

这将是一个动手操作,大家准备好了吗?如果你有精力,启动一个虚拟机并跟随我一起操作。

验证服务状态

我将在这次演示中使用 Alma Linux,原因稍后会变得明了。首先,让我们通过以下步骤安装 Apache Web 服务器:

sudo dnf install httpd

在你开始使用 Apache 之前,你需要知道它是否已启用,以便在重启机器时自动启动。你还需要知道它是否处于活动状态,即它是否正在运行。

要查看它是否已启用,请执行以下操作:

[donnie@localhost ~]$ systemctl is-enabled httpd
[sudo] password for donnie: 
disabled
[donnie@localhost ~]$

在这里,你可以看到我为何使用 RHEL 类型的发行版。当你在任何 RHEL 类型的机器上安装服务时,默认情况下它通常是禁用的。而在 Ubuntu 上安装服务时,默认情况下它通常是启用的。所以,通过在 Alma Linux 上进行此操作,我可以给你展示更多内容。

接下来,让我们通过以下操作查看 Apache 是否正在运行:

[donnie@localhost ~]$ systemctl is-active httpd
inactive
[donnie@localhost ~]$

好吧,它没有启用。现在,让我们同时查看这两项内容:

[donnie@localhost ~]$ systemctl status httpd
 httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; vendor preset: disabled)
   Active: inactive (dead)
     Docs: man:httpd.service(8)
[donnie@localhost ~]$

关于这些命令,我有几点想让你注意。首先,如果你只是想查看有关服务的信息,你不需要 sudo 权限。其次,如果你想对服务进行任何操作,你不需要附加.service文件扩展名。我的意思是,你可以附加,且不会有任何问题,但其实不需要。如果有多个类型的单元文件同名,systemctl默认会调用.service单元。例如,.service单元、.path单元和.socket单元,如你在这里所看到的:

[donnie@localhost ~]$ ls -l /lib/systemd/system/cups.*
-r--r--r--. 1 root root 142 Aug 27  2020 /lib/systemd/system/cups.path
-r--r--r--. 1 root root 248 Aug 27  2020 /lib/systemd/system/cups.service
-r--r--r--. 1 root root 136 Aug 27  2020 /lib/systemd/system/cups.socket
[donnie@localhost ~]$

没有文件扩展名时,systemctl将显示有关cups.service的信息,如下所示:

[donnie@localhost ~]$ systemctl status cups
cups.service - CUPS Scheduler
   Loaded: loaded (/usr/lib/systemd/system/cups.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2021-03-30 16:37:18 EDT; 33min ago
     Docs: man:cupsd(8)
 Main PID: 989 (cupsd)
   Status: "Scheduler is running..."
    Tasks: 1 (limit: 11274)
   Memory: 3.2M
   CGroup: /system.slice/cups.service
           └─989 /usr/sbin/cupsd -l
Mar 30 16:37:18 localhost.localdomain systemd[1]: Starting CUPS Scheduler...
Mar 30 16:37:18 localhost.localdomain systemd[1]: Started CUPS Scheduler.
Mar 30 16:38:14 localhost.localdomain cupsd[989]: REQUEST localhost - - "POST / HTTP/1.1" 200 362 Create-Printer-Subscriptions successful-ok
[donnie@localhost ~]$

这比is-active选项显示的有关正在运行服务的信息要多得多。顶部的cups.service - CUPS Scheduler一行来自cups.service文件[Unit]部分中的Description=CUPS Scheduler一行,关于手册页的信息来自Documentation=man:cupsd(8)一行。Main PID:这一行显示主 CUPS 进程的 PID 是989。通过这个方便的ps aux命令来验证:

[donnie@localhost ~]$ ps aux | grep 'cups'
root         989  0.0  0.5 340316 10196 ?        Ss   16:37   0:00 /usr/sbin/cupsd -l
donnie      8352  0.0  0.0 221904  1072 pts/1    R+   18:02   0:00 grep --color=auto cups
[donnie@localhost ~]$

确实,PID 是989

暂时不用担心那一行CGroup:。我们稍后会讨论 cgroups。

最后你会看到系统日志条目,它们在服务启动时创建。在 RHEL 类型的系统上,你会在/var/log/messages文件中看到它们。在 Debian 及其后代系统(如 Ubuntu)上,你会在/var/log/syslog文件中看到它们。

要查看其他类型单元的信息,你需要附加文件扩展名,如下所示:

[donnie@localhost ~]$ systemctl status cups.path
 cups.path - CUPS Scheduler
   Loaded: loaded (/usr/lib/systemd/system/cups.path; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2021-03-30 16:37:12 EDT; 1h 16min ago
Mar 30 16:37:12 localhost.localdomain systemd[1]: Started CUPS Scheduler.
[donnie@localhost ~]$

这样显示会更简短,因为.path单元的信息较少。

好的,我们已经有了一个好的开端。让我们回到那个 Apache 服务,看看我们能做些什么。

启动、停止和重新加载服务

我们已经看到,当你在 RHEL 类型的发行版上安装服务时,比如 Alma Linux,服务通常是禁用的,默认情况下没有激活。那么现在,我给你三个猜测的机会,猜猜启动服务的命令是什么。

放弃了吗?好的,下面是如何启动 Apache 的命令:

[donnie@localhost ~]$ sudo systemctl start httpd
[sudo] password for donnie: 
[donnie@localhost ~]$

嗯,这个很简单。让我们来看一下状态。这是命令输出的第一部分:

[donnie@localhost ~]$ sudo systemctl status httpd
 httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; vendor preset: disabled)
   Active: active (running) since Tue 2021-03-30 18:35:05 EDT; 1min 8s ago
     Docs: man:httpd.service(8)
 Main PID: 8654 (httpd)
   Status: "Running, listening on: port 80"
. . .
. . .

你可以看到这里服务是激活的,但它仍然是禁用的。这意味着如果我重启机器,服务不会自动启动。要查看更多信息,可以使用ps aux命令,如下所示:

[donnie@localhost ~]$ ps aux | grep httpd
root        8654  0.0  0.6 275924 11196 ?        Ss   18:35   0:00 /usr/sbin/httpd -DFOREGROUND
apache      8655  0.0  0.4 289796  8160 ?        S    18:35   0:00 /usr/sbin/httpd -DFOREGROUND
apache      8656  0.0  0.5 1347588 10032 ?       Sl   18:35   0:00 /usr/sbin/httpd -DFOREGROUND
apache      8657  0.0  0.5 1347588 10032 ?       Sl   18:35   0:00 /usr/sbin/httpd -DFOREGROUND
apache      8658  0.0  0.6 1478716 12080 ?       Sl   18:35   0:00 /usr/sbin/httpd -DFOREGROUND
donnie      8924  0.0  0.0 221904  1044 pts/1    R+   18:39   0:00 grep --color=auto httpd
[donnie@localhost ~]$

这里列出的第一个进程,PID 为8654,属于 root 用户,是我们在systemctl status输出中看到的主要进程。接下来的四个进程,PID 从86558658,是每当有人连接到该服务器上的网站时使用的,属于非特权的apache用户。这是 Apache 内建的一个安全特性,几乎从一开始就存在,并且与systemd无关。以非特权用户身份运行这些进程有助于防止攻击者为了恶意目的控制系统。

注意

如果你想了解其余的ps输出是什么意思,可以通过以下命令查看ps的手册页:

man ps

要停止 Apache 服务,只需执行sudo systemctl stop httpd。 (是的,我敢打赌你没有预料到这一点。)

如果你更改了正在运行的服务的配置,你需要重新加载它。你可以使用restart选项,这将重新启动服务并使新的配置生效。某些服务,例如 Apache,还具有reload选项。这样可以在不中断正在运行的服务的情况下读取新的配置。但是请注意,并非所有情况都可以使用reload。例如,在 Apache 中,你可以使用reload来重新加载网站配置文件的更改,但在某些情况下,像启用或禁用 Apache 模块时,你需要使用restart来读取 Apache 配置的更改。要查看reload是否适用于某个特定服务,可以尝试查阅该服务的文档。

启动、停止、重启或重新加载服务的具体命令可以在其关联的.service文件中定义。以下是来自 Alma 机器上httpd.service文件的相关行:

[Service]
. . .
. . .
ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND
ExecReload=/usr/sbin/httpd $OPTIONS -k graceful
. . .
. . .

目前,不需要担心这里看到的启动和重新加载选项意味着什么,因为这些知识是 Apache 特有的,而不是systemd的内容。我要你注意的是ExecReload=这一行。我们可以看到 Apache 有自己的内建方式来重新加载配置。与此对比,你可以看到这个来自 Alma 机器的sshd.service文件:

[Service]
. . .
. . .
ExecStart=/usr/sbin/sshd -D $OPTIONS $CRYPTO_POLICY
ExecReload=/bin/kill -HUP $MAINPID
. . .
. . .

在这里,我们看到安全外壳(Secure Shell)服务没有自己的内部机制来重新加载配置。相反,它依赖于几乎在 Linux 中永远存在的传统kill工具。不过要意识到,kill并不总是意味着杀死。当你使用kill工具时,它会向进程发送一个信号,让它做某些事情。通常,你会发送一个信号来真正“杀死”进程。但你也可以用它发送HUP信号给服务,这会导致服务在不中断的情况下重新加载其配置。(如果你想知道,HUP挂断(Hang Up)的缩写。这个信号最初的目的是通知正在运行的程序,当串行线路中断时。然而,HUP信号的用途已经改变,现在用于让服务重新加载配置。)你看到的$MAINPID实例是systemd用来访问主 Secure Shell 进程 PID 的环境变量。

可选地,你可以添加一行来定义当你发出stop命令时发生的事情。你在 Alma Linux 中看不到这一点,但在 Ubuntu 的apache2.service文件中可以看到,如下所示:

[Service]
. . .
. . .
ExecStart=/usr/sbin/apachectl start
ExecStop=/usr/sbin/apachectl stop
ExecReload=/usr/sbin/apachectl graceful
. . .
. . .

你没有看到ExecRestart=参数,因为没有这个参数。重启服务只是将其停止,然后再启动。

接下来,我们将了解如何启用和禁用服务。

启用和禁用服务

虽然 Apache 已经在运行,但如果我们重新启动 Alma Linux 机器,Apache 不会自动启动,直到你手动启动它。要开始这个演示,首先用以下命令停止 Apache:

sudo systemctl stop httpd

现在,通过以下方式启用它:

[donnie@localhost ~]$ sudo systemctl enable httpd
Created symlink /etc/systemd/system/multi-user.target.wants/httpd.service → /usr/lib/systemd/system/httpd.service.
[donnie@localhost ~]$

当我们启用 Apache 服务时,我们会在/etc/systemd/system/multi-user.target.wants/目录中创建一个指向httpd.service文件的符号链接。现在,我一直告诉你,单元文件位于/lib/systemd/system/目录。但细心的你会注意到,符号链接指向的是/usr/lib/systemd/system/目录中的服务文件。这是因为许多 Linux 发行版的新版已经删除了某些顶层目录,现在只使用一直位于/usr/目录下的相应目录。但天上的 Linux 大师们很贴心,为像我这样的老派程序员保留了这些顶层目录。他们通过在文件系统的根目录下创建符号链接来实现这一点,你可以在这里看到:

[donnie@localhost /]$ pwd
/
[donnie@localhost /]$ ls -l lib*
lrwxrwxrwx. 1 root root 7 Aug 14  2020 lib -> usr/lib
lrwxrwxrwx. 1 root root 9 Aug 14  2020 lib64 -> usr/lib64
[donnie@localhost /]$

所以,如果你像我一样总是忘记那些顶层目录不再存在了,没关系。符号链接完全有效。但我有些偏题了。

进入/etc/systemd/system/multi-user.target.wants/目录,你将看到通过systemctl enable命令创建的符号链接,如下所示:

[donnie@localhost ~]$ cd /etc/systemd/system/multi-user.target.wants/
[donnie@localhost multi-user.target.wants]$ ls -l httpd.service 
lrwxrwxrwx. 1 root root 37 Mar 30 19:22 httpd.service -> /usr/lib/systemd/system/httpd.service
[donnie@localhost multi-user.target.wants]$

好的,你现在可能在想那个 multi-user.target.wants 是怎么回事。那么,我稍后会详细介绍 .target 的概念。现在,只需接受多用户目标是操作系统完全启动并准备好正常操作的运行级别/etc/systemd/system/multi-user.target.wants/ 目录包含了在操作系统进入多用户模式时会自动启动的单元的符号链接。这个目录主要包含指向服务单元的符号链接,但有时也会包含指向其他类型单元的链接。在这台 Alma Linux 机器上,还有一个指向 cups.path 单元的链接,如下所示:

[donnie@localhost multi-user.target.wants]$ ls -l cups*
lrwxrwxrwx. 1 root root 33 Feb 11 18:14 cups.path -> /usr/lib/systemd/system/cups.path
lrwxrwxrwx. 1 root root 36 Feb 11 18:14 cups.service -> /usr/lib/systemd/system/cups.service
[donnie@localhost multi-user.target.wants]$

为了确定应该在哪里创建符号链接,systemctl enable 命令会从服务文件的 [Install] 部分获取设置。在 Alma 机器上的 httpd.service 文件底部,你可以看到这一行:

. . .
. . .
[Install]
WantedBy=multi-user.target

accounts-daemon.service 文件的底部,你会看到这一行:

. . .
. . .
[Install]
WantedBy=graphical.target

当该服务被启用时,它的符号链接位于 /etc/systemd/system/graphical.target.wants/ 目录中。

请注意,当你启用一个尚未运行的服务时,直到你重启机器,服务才会自动启动。你可以在这里看到:

[donnie@localhost multi-user.target.wants]$ systemctl is-enabled httpd
enabled
[donnie@localhost multi-user.target.wants]$ systemctl is-active
 httpd
inactive
[donnie@localhost multi-user.target.wants]$

你可以发出单独的 start 命令来启动服务,或者你可以使用 enable --now 选项通过一个命令来启用并启动服务,如下所示:

[donnie@localhost multi-user.target.wants]$ sudo systemctl enable --now httpd
Created symlink /etc/systemd/system/multi-user.target.wants/httpd.service → /usr/lib/systemd/system/httpd.service.
[donnie@localhost multi-user.target.wants]$

当你禁用一个单元时,它的符号链接会被移除。我们可以通过 Apache 服务看到这一点:

[donnie@localhost multi-user.target.wants]$ sudo systemctl disable httpd
[sudo] password for donnie: 
Removed /etc/systemd/system/multi-user.target.wants/httpd.service.
[donnie@localhost multi-user.target.wants]$ ls -l httpd*
ls: cannot access 'httpd*': No such file or directory
[donnie@localhost multi-user.target.wants]$

如果服务正在运行,执行 disable 命令后它仍会保持运行。你可以发出单独的 stop 命令,或者使用 disable --now 选项同时禁用并停止服务。

现在,对于 Ubuntu 粉丝们,以下是安装 Apache 在你的 Ubuntu 机器上的命令:

sudo apt install apache2

如果你查看 Apache 网站上的官方文档,你会看到官方的做法是将 httpd 作为 Apache 服务的名称。出于某种我至今未能搞明白的原因,Debian 开发者在一些方面总是采取不同的做法。Ubuntu 源自 Debian,因此 Ubuntu 开发者通常延续 Debian 的传统。无论如何,你可以在 Ubuntu 机器上尝试前面的命令,只需将 httpd 替换为 apache2。你唯一会看到的真正区别是,在 Ubuntu 上首次安装 Apache 后,服务将自动启用并运行。

你还可以做的另一个酷炫的事情是禁用服务的手动启动、停止和重启功能。最好的例子是 RHEL 类型机器上的 auditd 服务。在我 Alma 机器上的 auditd.service 文件的 [Unit] 部分,我们可以看到执行这一操作的这一行:

[Unit]
. . .
. . .
RefuseManualStop=yes
. . .
. . .

尝试重新启动服务会给我以下错误信息:

[donnie@localhost ~]$ sudo systemctl restart auditd
Failed to restart auditd.service: Operation refused, unit auditd.service may be requested by dependency only (it is configured to refuse manual start/stop).
See system logs and 'systemctl status auditd.service' for details.
[donnie@localhost ~]$

奇怪的是,如果我使用老式的service命令(来自 SysV 时代),我可以手动停止或重新启动auditd服务,就像我们在这里看到的那样:

[donnie@localhost ~]$ sudo service auditd restart
Stopping logging:                                          [  OK  ]
Redirecting start to /bin/systemctl start auditd.service
[donnie@localhost ~]$

我可以理解为什么我们要限制停止或重新启动auditd的权限,因为它与系统安全相关。但我从未理解为什么 RHEL 的维护者阻止用户使用systemctl进行操作,却仍允许我们用service来做。这就是那种让人不禁想“嗯……”的事情。另一个有趣的现象是,当你在 Ubuntu 上安装auditd时,你不会看到禁用这些功能的那一行。因此,在 Ubuntu 上,你可以按正常方式使用systemctl来停止和重新启动auditd

接下来,我们来看一下正确的杀死服务的方法。

杀死一个服务

我知道这很伤心,但即使在 Linux 上,事情有时也会崩溃。一个很好的例子就是 Firefox 浏览器。你有没有在不小心打开恶意网页时,浏览器完全卡住了的情况?就是说,你无法关闭标签页,电脑扬声器里传来刺耳的噪音,而且你也无法以正常方式关闭浏览器。你只能无奈地卡住。(如果你遇到过这种情况,不要觉得尴尬,这发生过在我们每个人身上。)在 Linux 机器上,你可以通过打开终端,使用ps aux | grep firefox来找到 Firefox 的 PID,然后发出kill命令来解决。比如说,假设 Firefox 的 PID 是3901,要终止它,只需执行:

kill 3901

默认情况下,这将向 Firefox 发送一个数字15,或者SIGTERM信号,给该进程一个机会通过关闭任何关联的文件或网络连接来进行自我清理。有时候,如果一个进程被严重锁定,数字15信号可能无法奏效。在这种情况下,你需要采取强力措施,使用数字9,或者SIGKILL信号,像这样:

kill -9 3901

数字9信号是你除非绝对必要,否则不想使用的信号。它会直接停止进程,而不给它们清理的机会。

注意

想了解更多关于各种 Linux 信号的信息,你可以查看你 Ubuntu 机器上的信号手册页面。(出于某种原因,Alma Linux 机器上的手册页面没有那么多信息。)命令是:

man signal

在 SysV 时期,你会使用相同的方法来终止有问题的服务,唯一不同的是你需要 sudo 权限来执行此操作,因为服务不会在你的用户帐户下运行。问题在于,有些服务会启动多个活动进程,而普通的 kill 命令可能无法终止它们所有。这些服务可能会以 僵尸 进程的形式继续存在,直到操作系统最终回收它们并将其清除。(当我说 回收 时,想象一下死神将尖木桩刺入僵尸心脏来最终杀死它们。哦,等等,刺心脏的是吸血鬼,而不是僵尸,所以算了。)一个很好的例子是 Apache 服务。我们已经看到,Apache 服务在启动时会生成多个进程,这仅仅是在没有运行任何活动网站的机器上。在实际的生产 Web 服务器上,Apache 可能会为 CGI 脚本、PHP 脚本或其他任何东西生成多个进程。如果你需要终止 Apache,务必确保那些脚本进程也被终止,特别是当它们可能正在做一些恶意的事情时。在我使用 systemd 的 Ubuntu 机器上,我会通过 sudo systemctl kill apache2 命令来完成这个操作。结果应该如下所示:

donnie@ubuntu2004:~$ systemctl is-active apache2
active
donnie@ubuntu2004:~$ sudo systemctl kill apache2
donnie@ubuntu2004:~$ systemctl is-active apache2
inactive
donnie@ubuntu2004:~$

与普通的 kill 命令一样,默认情况下它发送数字 15,即 SIGTERM 信号。如果你需要发送其他信号,请使用 -s 选项并指定信号名称。为了看到发生了什么,我将在我的 Ubuntu 机器上重新启动 Apache,并发送数字 9,即 SIGKILL 信号,像这样:

donnie@ubuntu2004:~$ systemctl is-active apache2
active
donnie@ubuntu2004:~$ sudo systemctl kill -s SIGKILL apache2
donnie@ubuntu2004:~$ systemctl is-active apache2
active
donnie@ubuntu2004:~$

哎呀,这对我们没有任何作用,是吧?为了查看原因,让我们看看 apache2.service 文件。在 [Service] 部分,你会找到答案:

[Service]
. . .
. . .
Restart=on-abort

[Service] 部分中的最后一行,即 Restart=on-abort 行,表示如果 Apache 收到不干净的杀死信号,它会自动重启。恰好 SIGKILL 被认为是不干净的信号。你可以在 systemd.service 手册页中查看对此的解释。打开该页面并向下滚动到表格 2,你会找到 Restart= 参数的不同选项,如下所示:

图 4.1 – 来自 systemd.service 手册页的表格 2

表格 2 上下的段落中,你会看到有关不同选项的解释,以及它们如何影响使用各种杀死信号。

在 Alma Linux 机器上,情况稍有不同。在它的 httpd.service 文件中,没有 Restart= 行。相反,我们看到这些行:

[Service]
. . .
. . .
# Send SIGWINCH for graceful stop
KillSignal=SIGWINCH
KillMode=mixed

KillSignal=行将默认的杀死动作从SIGTERM更改为SIGWINCH。这很奇怪,因为SIGWINCH应该只在进程所在的终端窗口大小发生变化时才会终止进程。而 Apache 通常并不在终端窗口中运行。不过,看起来 Red Hat 的某些人决定将SIGWINCH作为优雅地终止 Apache 的信号,所以就这么定了。KillMode=mixed行告诉systemd向主 Apache 进程发送SIGTERM信号,但向 Apache 控制组中的其余进程发送SIGKILL信号。systemd.kill手册页面没有说明当前面的KillSignal=行设置为SIGWINCH时这一行的作用,但我猜它会将SIGTERM替换为SIGWINCH。无论如何,让我们试着在 Alma 机器上终止 Apache,看看会发生什么:

[donnie@localhost ~]$ systemctl is-active httpd
active
[donnie@localhost ~]$ sudo systemctl kill httpd
[sudo] password for donnie: 
[donnie@localhost ~]$ systemctl is-active httpd
inactive
[donnie@localhost ~]$

它看起来和在 Ubuntu 机器上没什么区别。不过,向 Apache 发送SIGKILL信号时,你会看到不同的结果,如下所示:

[donnie@localhost ~]$ sudo systemctl kill -s SIGKILL httpd
[donnie@localhost ~]$ systemctl is-active httpd
failed
[donnie@localhost ~]$

如果没有 Ubuntu 在其apache2.service文件中的Restart=on-abort行,Alma 上的 Apache 服务在接收到SIGKILL信号时不会自动重启。请注意,is-active的输出显示为failed,而不是使用SIGTERMSIGWINCH时显示的inactive。无论如何,服务并没有运行,因此最终结果是一样的。

好的,一切正常。但是,如果你想防止某个服务运行呢?嗯,你可以将它屏蔽,这就是我们接下来要看的内容。

屏蔽服务

假设你有一个服务,你希望它永远不启动,无论是手动启动还是自动启动。你可以通过像这样将其屏蔽来实现:

[donnie@localhost ~]$ sudo systemctl mask httpd
Created symlink /etc/systemd/system/httpd.service → /dev/null.
[donnie@localhost ~]$

这次,我们不是创建一个指向服务文件的符号链接,而是创建了一个指向/dev/null设备的符号链接。让我们尝试启动我们屏蔽的 Apache 服务,看看会发生什么:

[donnie@localhost ~]$ sudo systemctl start httpd
Failed to start httpd.service: Unit httpd.service is masked.
[donnie@localhost ~]$

如果你改变主意,只需使用unmask选项。

总结

我们在这一章里已经覆盖了很多内容,甚至做了一些有趣的实操。我们学习了如何启动、停止、重启和重新加载服务。我们还了解了如何启用和禁用服务,并查看了启用服务时创建的符号链接。最后,我们展示了如何终止服务,以及如何屏蔽服务。作为附带收获,我们了解了一些服务参数的作用,以及不同 Linux 发行版的维护者如何设置服务,使其在不同的发行版上行为有所不同。

但是,如果你不喜欢在你使用的发行版中服务的设置怎么办?不用担心,我们将在下一章讨论这个问题,届时我们会讲解如何编辑和创建服务单元文件。我在那时见。

问题

  1. 当你运行sudo systemctl enable httpd命令时,这会为你做什么?

    a. 它会启动httpd服务。

    b. 它会导致httpd在启动机器时启动,并且还会立即启动。

    c. 它只会在你重启机器时启动 httpd

    d. 它在 /lib/systemd/system/ 目录下创建一个符号链接。

  2. 使用普通的 kill 命令对服务的影响是什么?

    a. 它会干净地关闭服务。

    b. 它会关闭主服务进程,但可能不会关闭生成的进程。

    c. 它不会关闭一个服务。

    d. 你可以在没有 sudo 权限的情况下使用 kill 关闭一个服务。

  3. 什么是 SIGTERM 信号?

    a. 它会立即杀死一个进程,不给它任何清理的机会。

    b. 当检测到终端窗口大小改变时,它会杀死一个进程。

    c. 它重新启动一个进程。

    d. 它优雅地终止一个进程,给它时间进行清理。

  4. 如何仅用一条命令启用并启动 httpd 服务?

    a. 你不能

    b. sudo systemctl enable httpd

    c. sudo systemctl start httpd

    d. sudo systemctl start --now httpd

    e. sudo systemctl enable --now httpd

  5. ExecRestart= 参数为我们做了什么?

    a. 它定义了如何重新启动服务。

    b. 它定义了如何重新加载服务配置。

    c. 什么都没有,因为这个参数不存在。

    d. 它定义了如何启动一个服务。

答案

  1. c

  2. b

  3. d

  4. e

  5. c

深入阅读

我的 管理服务 视频:youtu.be/IuDmg75n6FU

如何管理 systemd 服务:www.howtogeek.com/216454/how-to-manage-systemd-services-on-a-linux-system/

第五章:创建和编辑服务

我们刚刚了解了systemd服务是什么以及如何控制它们。不过,有时你可能需要改变某个服务的行为,或者创建一个全新的服务。在这一章中,我们将学习如何正确编辑服务。接下来,我们将学习如何创建一个新的服务。本章的具体内容如下:

  • 编辑现有服务

  • 创建新服务

  • 更改默认的 systemd 编辑器

  • 使用 Podman 创建新的容器服务

如果你准备好了,那我们就开始吧。

技术要求

如前所述,我将使用 Alma Linux 8 虚拟机和 Ubuntu Server 20.04 虚拟机。为了进行安全 shell(Secure Shell)练习,你需要进入两台虚拟机的 VirtualBox 网络设置,并选择ip a。这样,你就可以从宿主机的命令行远程登录到虚拟机。

查看以下链接,观看“代码实战”视频:bit.ly/3xP0yOH

编辑现有服务

我们已经看到,服务的单元文件位于/lib/systemd/system/目录中,所以你可能首先会想到直接去那里,使用你喜欢的文本编辑器编辑文件。虽然这么做是可行的,但你不应该这样做。如果进行系统更新,它可能会覆盖你编辑的文件,从而丢失你的修改。

正确的方法是在/etc/systemd/system/目录中创建你服务文件的编辑版。你可以像编辑其他配置文件一样,使用你喜欢的文本编辑器进行编辑。事实上,这也是你以前必须做的事。当 Red Hat 发布 RHEL 7.2 时,他们在systemctl命令中添加了一个edit功能,这使得操作变得更容易。(当然,现在所有运行systemd的 Linux 发行版都可以使用这个edit功能。)

我注意到有些人喜欢将自己的自定义单元文件添加到/lib/systemd/system/目录中,这样它们就可以与操作系统安装的单元文件并列。如果你是其中之一,请理解这样做是好的做法。这样做的风险是,在进行系统更新时,你的自定义单元文件可能会被删除或覆盖。而将自定义单元文件保存在/etc/systemd/system/目录中,会让你更容易跟踪哪些单元文件是你添加的,哪些是操作系统安装的。

现在,你可能会想知道如何了解可以对服务文件进行哪些更改。最简单的答案是阅读不同单元类型的 man 页面,查看可以添加、删除或修改的所有参数和选项。不过,如果像我一样,你会发现阅读这些 man 页面很容易让人昏昏欲睡。别误会,man 页面确实很有用。但如果你真的想学会如何让服务按照你的需求“唱歌跳舞”,最轻松的办法就是查看你系统中已经存在的服务文件,看看它们是如何设置的。然后,查看这些文件中列出的参数,并查阅相应的 man 页面,了解它们为你做了什么。我们在本章中会通过很多例子来说明这一点。

当你使用 systemctl edit 功能时,可以选择部分编辑文件或编辑整个文件。默认情况下,你将进行部分编辑。让我们从我能想到的最简单的例子开始。

对 [Install] 部分进行部分编辑

启动 Ubuntu 服务器虚拟机并向 apache2.service 文件添加一行 Alias=。首先这样做:

sudo systemctl edit apache2

你看到的内容类似于这样:

图 5.1 – Ubuntu 上的 systemd 服务编辑器

是的,看起来没什么,对吧?这只是一个在 nano 编辑器中打开的空文件。不过别担心,我们在这里要做的只是添加一个参数,且不需要看到整个服务文件就能做到这一点。由于我们使用的是 Ubuntu,Apache 服务的名称是 apache2。假设你刚从 Red Hat 环境过来,并且习惯了总是使用 httpd 作为 Apache 服务名。因此,每当你在 Ubuntu 机器上输入错误的服务名时,就会感到很沮丧。这就像是你一辈子都习惯开手动挡车,结果一上自动挡车就开始找离合器。(嗯,反正我是这么做的。)我们可以轻松解决这个问题,但首先我们来看一个已经存在的例子。

在另一个窗口中,查看 Ubuntu 机器上 ssh.service 文件的 [Install] 部分,如下所示:

. . .
. . .
[Install]
WantedBy=multi-user.target
Alias=sshd.service

最后的 Alias= 行就是我们的例子。现在,在 nano 窗口中输入以下内容:

[Install]
Alias=httpd.service

保存文件并通过按 Ctrl + X 键组合退出编辑器。当它询问是否保存已修改的缓冲区时,按 y 键。然后,按 Enter 键接受默认的文件名。接下来,查看 /etc/systemd/system/ 目录。你会看到我们刚刚创建了一个新的 apache2.service.d 目录:

donnie@ubuntu20-04:/etc/systemd/system$ ls -l
total 104
drwxr-xr-x 2 root root 4096 Apr  5 16:55 apache2.service.d
. . .
. . .

在那个目录中,你将看到以下 override.conf 文件:

donnie@ubuntu20-04:/etc/systemd/system/apache2.service.d$ ls -l
total 4
-rw-r--r-- 1 root root 30 Apr  5 16:55 override.conf
donnie@ubuntu20-04:/etc/systemd/system/apache2.service.d$

这个文件包含了我们刚刚添加的参数,内容如下:

[Install]
Alias=httpd.service

就是这样——整个文件。当我们启动 Apache 时,这个参数将被添加到原始服务文件中已经存在的内容。其优点是,如果原始服务文件在系统更新中被替换,你将获得更新所做的更改,并且仍然保留这个修改。

但是,在你使用这个修改之前,你需要将它加载到系统中。你可以通过执行以下命令来做到这一点:

donnie@ubuntu20-04:~$ sudo systemctl daemon-reload
[sudo] password for donnie: 
donnie@ubuntu20-04:~$

每次修改或添加服务文件时,你都需要执行daemon-reload。当你添加Alias=时,还需要在/etc/systemd/system/目录中为其创建一个符号链接。你可以通过ln -s命令手动创建它,但其实不必。当你在服务文件的[Install]部分添加Alias=行时,启用服务时链接会自动创建。在 Ubuntu 机器上,Apache 服务已经启用并正在运行,因此我们只需要禁用并重新启用它。(注意,不需要停止服务。)那么,我们先禁用 Apache,像这样:

donnie@ubuntu20-04:/etc/systemd/system$ sudo systemctl disable apache2
Synchronizing state of apache2.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install disable apache2
Removed /etc/systemd/system/multi-user.target.wants/apache2.service.
donnie@ubuntu20-04:/etc/systemd/system$

现在,我们将重新启用它,像这样:

donnie@ubuntu20-04:/etc/systemd/system$ sudo systemctl enable apache2
Synchronizing state of apache2.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install enable apache2
Created symlink /etc/systemd/system/httpd.service → /lib/systemd/system/apache2.service.
Created symlink /etc/systemd/system/multi-user.target.wants/apache2.service → /lib/systemd/system/apache2.service.
donnie@ubuntu20-04:/etc/systemd/system$

你可以在输出中看到,enable命令读取了我们插入[Install]部分的Alias=行,并创建了一个指向原始apache2.service文件的httpd.service链接。我们可以通过以下ls -l命令来验证这一点:

donnie@ubuntu20-04:/etc/systemd/system$ ls -l httpd.service 
lrwxrwxrwx 1 root root 35 Apr  5 17:39 httpd.service -> /lib/systemd/system/apache2.service
donnie@ubuntu20-04:/etc/systemd/system$

现在是关键时刻了。我们能通过调用httpd服务名称来控制我们 Ubuntu 机器上的 Apache 吗?我们来看看:

donnie@ubuntu20-04:~$ systemctl status httpd
• apache2.service - The Apache HTTP Server
     Loaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor preset: enabled)
    Drop-In: /etc/systemd/system/apache2.service.d
             └─override.conf
     Active: active (running) since Mon 2021-04-05 17:19:08 UTC; 34min ago
. . .
. . .

哦,没错。它运行得像冠军一样。(你难道不喜欢计划成功时的感觉吗?)要查看带有新编辑的服务文件,使用systemctl cat,像这样:

donnie@ubuntu20-04:~$ systemctl cat apache2
# /lib/systemd/system/apache2.service
[Unit]
Description=The Apache HTTP Server
. . .
. . .
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/apache2.service.d/override.conf
[Install]
Alias=httpd.service
donnie@ubuntu20-04:~$

输出的顶部部分显示了原始的服务文件,底部部分显示了你创建的override.conf文件。

当然,你也可以走反方向。如果你习惯了 Ubuntu 的方式,而突然发现自己在 RHEL 类型的机器上管理 Apache,你可以在httpd.service文件中添加Alias=apache2.service行,然后禁用并重新启用 Apache 来创建链接。唯一的不同之处是,在 Ubuntu 机器上,systemctl edit会启动 nano 文本编辑器,而在 RHEL 类型的机器上,它可能会启动 vi 文本编辑器。(RHEL 类型的发行版最近才从 vi 切换为 nano 作为默认的 systemd 编辑器。)

专业提示

记住,对服务文件中[Install]部分所做的任何更改都会影响每次启用或禁用该服务时的行为。

好的,现在我们已经在[Install]部分添加了一个很酷的选项,让我们来添加一些到[Service]部分吧。

[Service]部分进行局部编辑

让我们继续操作我们的 Ubuntu 服务器虚拟机,并在已经做过的基础上再添加一些内容。这次,我们将在 [Service] 部分添加一些选项,稍微增强一下安全性。不过,在此之前,让我们看看 Apache 的实际安全性如何。我们将通过 systemd-analyze 工具来检查。

systemd-analyze 的手册页上,你会看到这个工具有很多用途。目前,我们只介绍 security 选项。首先,通过执行以下命令来检查我们 Ubuntu 虚拟机上服务的整体安全配置:

donnie@ubuntu20-04:~$ systemd-analyze security
UNIT                                  EXPOSURE PREDICATE HAPPY
accounts-daemon.service                    9.6 UNSAFE  😨    
apache2.service                            9.2 UNSAFE  😨    
apport.service                             9.6 UNSAFE  😨    
. . .
. . .      🙂    
systemd-udevd.service                      8.4 EXPOSED 🙁    
thermald.service                           9.6 UNSAFE  😨    
unattended-upgrades.service                9.6 UNSAFE  😨    
user@1000.service                          9.4 UNSAFE  😨    
uuidd.service                              4.5 OK      🙂    
vgauth.service                             9.5 UNSAFE  😨    
donnie@ubuntu20-04:~$

这个命令会检查每个服务的安全性和沙盒设置,并为每个服务分配一个 EXPOSURE 分数。分数越高,服务就越不安全。所以,这就像高尔夫比赛一样,你需要尽可能获得最低分数。HAPPY 列本来应该显示不同程度的开心或难过表情符号,但粘贴到书中的时候表情符号无法显示。不过没关系,因为你可以在自己的虚拟机上看到这些表情。

现在,在你看到某个服务被标记为 UNSAFE(如我们这里看到的 Apache 服务)时,别太激动,你需要理解这只是检查了服务文件中的安全设置。它并没有考虑服务自身配置文件中的任何安全设置,也没有考虑可能嵌入在服务可执行文件中的安全选项,或者任何可能生效的强制访问控制MAC)选项。不过,尽管如此,这仍然是一个有用的工具,可以为你提供加强安全设置的建议。

接下来,我们来看看一些针对 Apache 服务的建议:

donnie@ubuntu20-04:~$ systemd-analyze security apache2
  NAME                                                        DESCRIPTION                                                             EXPOSURE
✗ PrivateNetwork=                                             Service has access to the host's network                                     0.5
✗ User=/DynamicUser=                                          Service runs as root user                                                    0.4
✗ CapabilityBoundingSet=~CAP_SET(UID|GID|PCAP)                Service may change UID/GID identities/capabilities                           0.3
✗ CapabilityBoundingSet=~CAP_SYS_ADMIN                        Service has administrator privileges                                         0.3
✗ CapabilityBoundingSet=~CAP_SYS_PTRACE                       Service has ptrace() debugging abilities                                     0.3
. . .
. . .

这里的输出太多,无法全部显示,但没关系。让我们向下滚动一些,展示一些与我们想做的事情更相关的设置:

. . .
. . .
✓ PrivateMounts=                                              Service cannot install system mounts                                            
✓ PrivateTmp=                                                 Service has no access to other software's temporary files                       
✗ PrivateUsers=                                               Service has access to other users                                            0.2
✗ ProtectClock=                                               Service may write to the hardware clock or system clock                      0.2
✗ ProtectControlGroups=                                       Service may modify the control group file system                             0.2
✗ ProtectHome=                                                Service has full access to home directories                                  0.2
. . .
. . .
✗ ProtectSystem=                                              Service has full access to the OS file hierarchy                             0.2
. . .
. . .

当你看到条目前面有一个 X 时,意味着这个设置是不安全的。如果条目前有一个勾选标记,表示该参数已配置为安全设置。但是,如果你胡乱地将不安全的设置更改为安全的设置,可能会导致服务无法正常运行。一些所谓的不安全设置其实是服务正常运行所必需的。以 User=/DynamicUser= 设置为例。如果没有该参数,Apache 服务将以 root 权限运行。这不一定不好,因为 Apache 服务需要 root 权限来执行某些任务。如果你将该选项设置为非 root 用户,Apache 将无法启动。而且,Apache 开发者已经考虑到了这一点。他们的设置方式是,只有第一个 Apache 进程会以 root 权限运行,其他所有的 Apache 进程——即浏览器连接的进程——都不会以 root 权限运行。我们已经看到,在 RHEL 类型的发行版中,比如 Alma Linux,Apache 进程是以 apache 用户身份运行的。而在 Ubuntu 中,我们看到它们是以 www-data 账户运行的:

donnie@ubuntu20-04:/etc/apache2$ ps aux | grep apache
root        2290  0.0  0.2   6520  4480 ?        Ss   18:40   0:00 /usr/sbin/apache2 -k start
www-data    2291  0.0  0.2 752656  4344 ?        Sl   18:40   0:00 /usr/sbin/apache2 -k start
www-data    2292  0.0  0.2 752656  4344 ?        Sl   18:40   0:00 /usr/sbin/apache2 -k start
donnie      2554  0.0  0.0   6432   724 pts/0    S+   19:55   0:00 grep --color=auto apache
donnie@ubuntu20-04:/etc/apache2$

这个非根用户是在 Apache 配置文件中定义的。让我们看看我们的好朋友grep能否帮助我们找到这个设置的位置:

donnie@ubuntu20-04:/etc/apache2$ grep -r 'USER' *
apache2.conf:User ${APACHE_RUN_USER}
envvars:export APACHE_RUN_USER=www-data
donnie@ubuntu20-04:/etc/apache2$

所以,在 Ubuntu 机器上,非 root 的 www-data 用户定义在 /etc/apache2/envvars 文件中,并在 /etc/apache2/apache2.conf 文件中调用。(关于 Alma Linux 机器上这个设置的位置,我留给你自己去找。)总之,关于我们不能更改的设置就说到这里。接下来,我们来看看一些我们可以更改的设置。

提示:

在 RHEL 类型的机器上,接下来我将展示的所有内容已经由 SELinux 覆盖。但如果你希望获得双重保护,仍然可以使用这些设置。Ubuntu 和 SUSE 使用 AppArmor 而不是 SELinux。不幸的是,除非你自行创建自定义的 AppArmor 配置文件,否则 AppArmor 对 Apache 几乎没有任何保护。而在 systemd 中设置这些保护则要简单得多。

首先,让我们来看看如何保护用户的主目录。我们将再次使用 sudo systemctl edit apache2 命令,这将打开我们之前创建的 override.conf 文件。[Install] 部分仍然存在,我们只需添加一个 [Service] 部分,如下所示:

[Service]
ProtectHome=yes
ProtectSystem=strict
[Install]
Alias=httpd.service

默认情况下,Apache 服务可以读取或写入文件系统中的任何位置。ProtectHome=yes 设置可以防止 Apache 访问 /root//home//run/user/ 目录,即使这些目录设置了全局可读权限。如果用户希望通过自己的主目录提供 Web 内容,同时防止 Apache 写入这些目录,我们还可以将此设置为 read-only

ProtectSystem=strict 设置会使 Apache 只能对整个文件系统进行只读访问,除了 /dev//proc//sys/ 目录。我们保存文件并重启 Apache,看看结果:

donnie@ubuntu20-04:~$ sudo systemctl daemon-reload
donnie@ubuntu20-04:~$ sudo systemctl restart apache2
Job for apache2.service failed because the control process exited with error code.
See "systemctl status apache2.service" and "journalctl -xe" for details.
donnie@ubuntu20-04:~$

哎呀,这不好。让我们检查一下状态,看看问题可能出在哪里:

donnie@ubuntu20-04:~$ sudo systemctl status apache2
. . .
. . .
Apr 10 21:52:20 ubuntu20-04 apachectl[3848]: (30)Read-only file system: AH00091: apache2: could not open error log file /var/log/apache2/error.log.
Apr 10 21:52:20 ubuntu20-04 apachectl[3848]: AH00015: Unable to open logs
Apr 10 21:52:20 ubuntu20-04 apachectl[3836]: Action 'start' failed.
Apr 10 21:52:20 ubuntu20-04 apachectl[3836]: The Apache error log may have more information.
. . .
. . .
donnie@ubuntu20-04:~$

所以,Apache 想要写入/var/log/apache2/目录中的日志文件,但它不能。让我们更改ProtectSystem设置,看看能否解决问题:

[Service]
ProtectHome=yes
ProtectSystem=full  
[Install]
Alias=httpd.service

ProtectSystem=设置为full会使 Apache 对/boot//usr//etc/目录具有只读访问权限。通常,Apache 不需要写入这些目录,所以现在它应该能正常工作。我们试试看:

donnie@ubuntu20-04:~$ sudo systemctl daemon-reload
donnie@ubuntu20-04:~$ sudo systemctl restart apache2
donnie@ubuntu20-04:~$

没有错误信息,这很好。让我们检查一下状态:

donnie@ubuntu20-04:~$ sudo systemctl status apache2
● apache2.service - The Apache HTTP Server
     Loaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor preset: enabled)
    Drop-In: /etc/systemd/system/apache2.service.d
             └─override.conf
     Active: active (running) since Sat 2021-04-10 21:58:22 UTC; 4s ago
. . .
. . .

好的,这没问题。所以,太棒了!我们搞定了,宝贝!但说实话,看看systemd.exec手册页,你会看到更多你可能用得上的安全设置。

注意

你可以看到,我不小心使用了sudo来查看服务状态。习惯性地在使用systemctl时加上sudo,但幸运的是,即使在不需要时使用它也不会造成任何问题。

仅为好玩,我们再添加一些安全选项,然后再次检查安全状态。这次,我们会让我们的[Service]部分看起来像这样:

[Service]
ProtectHome=yes
ProtectSystem=full
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
NoNewPrivileges=yes

我留给你去阅读这些新参数的systemd.exec手册页。

编辑完成后,重新启动 Apache,确保它能正常启动。然后,再次运行systemd-analyze security apache2.service。你应该会看到整体的安全评分比之前有所提升。

你可以在服务文件中设置更多与安全相关的参数。不过要记住,大多数服务要求某些设置保持在UNSAFE模式下才能正常运行。最好的方法是,在虚拟机上尝试不同服务的这些设置。这样,你就能清楚地了解哪些设置适用于不同的服务。而且,为了安全起见,在将任何服务修改投入生产环境之前,务必进行彻底的测试。

接下来,让我们来看一下如何对 Secure Shell 服务进行完全编辑。

进行完全编辑

部分编辑非常适用于当你只需要添加一个服务文件中尚未存在的参数时。但是,如果你需要删除一个参数、更改参数的值,或添加一个与其他现有参数冲突的参数,这时就需要进行完全编辑。进行完全编辑的另一个原因是,你可能希望将整个文件展现在眼前,这样你就能清楚地了解自己在做什么。例如,要对ssh.service文件进行完全编辑,只需使用--full选项,如下所示:

donnie@ubuntu20-04:~$ sudo systemctl edit --full ssh.service

这次,你将看到整个ssh.service文件,如下所示:

图 5.2 – 使用--full 选项编辑服务文件

这次,我们来设置 Secure Shell 服务的访问控制。

专业提示

如果你在 Linux 圈子里待了有一段时间,可能会对tcpwrappers这个概念有所了解。它的名字很奇怪,但概念很简单。你只需要在/etc/hosts.allow文件中配置希望允许访问特定网络服务的 IP 地址,然后在/etc/hosts.deny文件中拒绝所有其他 IP 地址。这个方法在 Ubuntu 中依然有效,但 Red Hat 在 RHEL 8 中已移除tcpwrappers。所以,如果你想在任何 RHEL 8 类型的发行版上配置访问控制(比如我们使用的 Alma Linux 8),你需要通过配置服务文件来实现。在 Ubuntu 上,你可以使用任意一种方法。

假设你只想允许来自一台特定桌面机器的 SSH 登录。为此,我们将在ssh.service文件的[Service]部分使用IPAddressAllow=IPAddressDeny=参数。(当然,如果你想在 Alma Linux 机器上尝试,应该是sshd.service文件。)像我刚才展示的那样打开文件进行编辑,并在[Service]部分的末尾添加两行,使用你自己主机的 IP 地址填写IPAddressAllow=行。这两行应该像这样:

IPAddressAllow=192.168.0.222
IPAddressDeny=any

整个文件现在应该看起来像这样:

图 5.3 - 编辑后的 ssh.service 文件

如果你只插入IPAddressAllow=行,而不插入IPAddressDeny=行,你会发现没有任何东西被阻止。因此,每当你想设置访问白名单时,都需要同时使用这两行。

保存文件并执行sudo systemctl daemon-reload。然后,重启或重新加载安全外壳服务。如果你使用了正确的主机 IP 地址,你应该能够通过 SSH 登录。为了真正测试这个功能,再次编辑文件,并使用错误的主机 IP 地址。这次,你应该会被阻止进行 SSH 登录。不过要注意,你不需要再次执行daemon-reload命令。这个新设置在保存文件后会立即生效。因此,如果你在远程操作,并且输入了错误的主机 IP 地址,你将会被锁定。在实际操作中,你需要进入服务器机房,从服务器的本地终端正确配置。

当你进行完整编辑时,修改后的原始服务文件副本将被保存到/etc/systemd/system/目录,如下所示:

donnie@ubuntu20-04:~$ cd /etc/systemd/system/
donnie@ubuntu20-04:/etc/systemd/system$ ls -l ssh.service 
-rw-r--r-- 1 root root 586 Apr 11 20:07 ssh.service
donnie@ubuntu20-04:/etc/systemd/system$

只要这个文件存在,它将始终覆盖/lib/systemd/system/目录中的原始文件。

接下来,让我们来看一下如何创建一个全新的服务。

创建一个新服务

要从头开始创建一个全新的服务,使用--force--full选项组合,如下所示:

donnie@ubuntu20-04:~$ sudo systemctl edit --force --full timestamp.service

这将在/etc/systemd/system/目录下创建一个新的服务文件,正如我们之前看到的那样。

在这个演示中,我们将创建一个服务,将定期的时间戳写入我们的系统日志文件。我们的 timestamp.service 文件将如下所示:

[Unit]
Description=Service Creation Demo
Wants=network.target
After=syslog.target network-online.target
[Service]
ExecStart=/usr/local/bin/timestamp.sh
Restart=on-failure
RestartSec=20
KillMode=process
[Install]
WantedBy=multi-user.target

这里,我们看到 RestartSec= 参数,这是我们以前没有见过的。它与 Restart= 行一起工作,表示在重启崩溃的服务之前等待指定的秒数。在这里,我们设定等待 20 秒。我们没有看到 Type= 行,因为我们不需要它。如果没有这一行,systemd 会使用默认的 Type=simple,这正是我们需要的。(我留给你去阅读 systemd.service 手册页中的 simple Type 部分。)

接下来,我们将创建 timestamp.sh 脚本,该脚本每 60 秒将时间戳写入系统日志文件。让我们将它做成这样:

#!/bin/bash
echo "Starting the timestamp service" | systemd-cat -p info
while :
do
        echo "timestamp.service: The current time is $(date '+%m-%d-%Y %H:%M:%S')" | systemd-cat -p info
        sleep 60
done

如你所见,这只是一个简单的 while 循环,它每隔 60 秒将当前时间传递给 systemd-cat 工具。然后,systemd-cattimestamp 消息发送到系统日志文件。-p info 选项将该消息标记为 info 级别的优先级。

接下来,使脚本文件可执行,并将其复制到 /usr/local/bin/ 目录。然后,启动服务:

donnie@ubuntu20-04:~$ chmod u+x timestamp.sh 
donnie@ubuntu20-04:~$ sudo cp timestamp.sh /usr/local/bin
donnie@ubuntu20-04:~$ sudo systemctl start timestamp
donnie@ubuntu20-04:~$

如果你真的想启用该服务,可以启用它,但目前我们不需要它。

状态应该如下所示:

donnie@ubuntu20-04:~$ systemctl status timestamp
● timestamp.service - Service Creation Demo
     Loaded: loaded (/etc/systemd/system/timestamp.service; disabled; vendor preset: enabled)
     Active: active (running) since Sun 2021-04-11 21:57:26 UTC; 13min ago
   Main PID: 14293 (timestamp.sh)
      Tasks: 2 (limit: 2281)
     Memory: 820.0K
     CGroup: /system.slice/timestamp.service
             ├─14293 /bin/bash /usr/local/bin/timestamp.sh
             └─14411 sleep 60
Apr 11 22:00:26 ubuntu20-04 cat[14335]: timestamp.service: The current time is 04-11-2021 22:00:26
. . .
. . .

要在日志文件中查看时间戳,执行以下操作:

donnie@ubuntu20-04:~$ journalctl -xe

或者,你可以通过在 Ubuntu 机器上执行 sudo tail -f /var/log/syslog,或在 Alma 机器上执行 sudo tail -f /var/log/messages 来实时查看它们的添加情况。当你看到足够的信息时,只需按 Ctrl + C 退出。

更改默认的 systemd 编辑器

到目前为止,我一直在展示如何在 nano 文本编辑器中完成这些操作,nano 是大多数现代 Linux 发行版的默认 systemd 编辑器。但是,如果你不喜欢 nano,而想使用其他编辑器呢?假设你最喜欢的文本编辑器是 Vim,你想用它来替代 nano。

使用替代文本编辑器的一种方法是每次运行 systemctl edit 命令时指定替代编辑器,像这样:

[donnie@localhost ~]$ sudo EDITOR=vim systemctl edit --full sshd
[donnie@localhost ~]$

这样可以,但每次运行 systemctl edit 命令时都这样做可能会有点麻烦。幸运的是,一旦你知道如何操作,修改默认编辑器非常简单。

首先,编辑位于你个人主目录中的 .bashrc 文件。在文件的最底部,添加这一行:

export SYSTEMD_EDITOR=vim

保存文件后,重新加载新的配置:

[donnie@localhost ~]$ source .bashrc
[donnie@localhost ~]$

接下来,打开 sudoers 文件:

[donnie@localhost ~]$ sudo visudo

向下滚动到你看到 Defaults 行的位置,然后添加这一行:

Defaults   env_keep += "SYSTEMD_EDITOR"

保存文件后,尝试运行 systemctl edit 命令。你现在应该看到 vim 而不是 nano。

我们已经看到了一些很酷的东西,但我们才刚刚开始。为了达到极致的酷,我们来看看如何使用 podman 自动为我们创建容器服务文件。

使用 podman 创建新的容器服务

容器技术已经存在很长时间了,但直到 Docker 推出了其新的容器管理系统,容器才变得非常流行。原始的 Docker 系统确实很酷,但它有一些不足,尤其是在安全性方面。为此,Red Hat 的好心人开发了自己的 Docker 替代品,叫做 podmanpodman 提供了大大增强的安全性,并且具备 Docker 中没有的酷功能。唯一的问题是,podman 目前仅在 RHEL 类型和 Fedora 发行版上可用,而其他所有人仍然使用 Docker。因此,我们将在 Alma Linux 机器上演示这些操作。

要在你的 Alma 机器上安装 podman,请执行:

[donnie@localhost ~]$ sudo dnf install podman-docker

这将安装 podman 包,并附带一个 shell 脚本,任何时候你不小心输入 docker 时,它会调用 podman。(实际上,这可能不是偶然的。你可能有一些 shell 脚本会调用 docker 命令,安装 podman-docker 包将避免你修改它们以使用 podman 命令。)为了避免混淆,接下来的演示中,我将只展示 podman 命令。

podman 工具通常不需要管理员权限,这是它相对于 Docker 的一个优势。但是,为了使其正常工作,我们需要在 root 用户账户下创建 Docker 容器。我们可以通过进入 root 用户 shell 来完成这项操作,也可以在普通用户 shell 中执行 sudo 来运行 podman 命令。由于除非必要,我通常不喜欢进入 root shell,因此我们将使用 sudo 来执行。

让我们创建一个 wordpress 容器,像这样:

[donnie@localhost ~]$ sudo podman run -d -p 8080:80 --name wordpress wordpress

WordPress 是一个免费的开源博客平台。在这里,我们以 detached(后台)模式运行一个 wordpress 容器,并使用 -d 参数。我们没有将默认的 80 端口暴露给网络,而是暴露了 8080 端口。(请注意,如果启动失败,可能是因为端口 8080 已经有其他应用在监听。如果是这种情况,请尝试使用其他端口。)--name 参数设置了容器的名称,我们稍后将在命令中使用该名称来创建服务文件。

我们将通过 sudo podman ps 来验证它是否正在运行:

[donnie@localhost ~]$ sudo podman ps
CONTAINER ID  IMAGE                               COMMAND               CREATED        STATUS            PORTS                 NAMES
cc06c35f21ce  docker.io/library/wordpress:latest  apache2-foregroun...  2 minutes ago  Up 2 minutes ago  0.0.0.0:8080->80/tcp  wordpress
[donnie@localhost ~]$

你也可以尝试从主机的网页浏览器访问 WordPress,但你首先需要在 Alma 机器的防火墙上打开 8080/tcp 端口,方法如下:

[donnie@localhost ~]$ sudo firewall-cmd --permanent --add-port=8080/tcp
success
[donnie@localhost ~]$ sudo firewall-cmd --reload
success
[donnie@localhost ~]$

然后,打开主机的网页浏览器,访问 Alma 机器的 IP 地址上的 8080 端口。URL 应该像这样:

192.168.0.9:8080/

这应该会弹出 WordPress 的初始界面,引导你完成设置过程。

好的,这很棒,但问题是当你重启机器时,容器不会自动启动。为了解决这个问题,我们将使用 sudo podman generate systemd 命令来创建服务文件,方法如下:

[donnie@localhost ~]$ sudo podman generate systemd wordpress | sudo tee /etc/systemd/system/wordpress-container.service

注意,在 /etc/ 目录中,使用 sudo 进行正常的重定向操作(使用 > 符号)效果不好,但将输出通过管道传递到 tee 工具就能正常工作。正如你所看到的,tee 工具将输出发送到屏幕和指定的文件。

执行 systemctl cat wordpress-container 将显示生成的服务文件:

图 5.4 – podman 生成的服务文件

如果这还不够顺畅,那我就不知道什么才算顺畅了。让我们启用该服务并重启,看看会发生什么:

[donnie@localhost ~]$ sudo systemctl daemon-reload
[donnie@localhost ~]$ sudo systemctl enable wordpress-container
Created symlink /etc/systemd/system/multi-user.target.wants/wordpress-container.service → /etc/systemd/system/wordpress-container.service.
Created symlink /etc/systemd/system/default.target.wants/wordpress-container.service → /etc/systemd/system/wordpress-container.service.
[donnie@localhost ~]$ sudo shutdown -r now

重启完成后,执行 sudo podman ps 命令应该会显示容器正在运行:

[donnie@localhost ~]$ sudo podman ps
[sudo] password for donnie: 
CONTAINER ID  IMAGE                               COMMAND               CREATED         STATUS                 PORTS                  NAMES
cc06c35f21ce  docker.io/library/wordpress:latest  apache2-foregroun...  12 minutes ago  Up About a minute ago  0.0.0.0:8080->80/tcp  wordpress
[donnie@localhost ~]$

(你这里只需要使用 sudo,因为容器是在 root 用户账户下运行的。)

当然,systemctl status 也应该显示该服务正在运行:

[donnie@localhost ~]$ systemctl status wordpress-container
● wordpress-container.service - Podman container-cc06c35f21cedd4d2384cf2c048f013748e84cabdc594b110a8c8529173f4c81.service
   Loaded: loaded (/etc/systemd/system/wordpress-container.service; enabled; vendor preset: disabled)
   Active: active (running) since Wed 2021-04-14 15:27:37 EDT; 2min 38s ago
. . .
. . .

好的,一切正常。但是,如果你需要创建一个容器服务,并希望从你自己的用户账户运行,而不需要根权限呢?好吧,我们也为你提供了这种方法。只需在你自己的主目录中创建服务文件,并从那里运行。

我们将像之前一样开始,只是使用不同的容器名称,并从正常的用户 shell 中启动,如下所示:

[donnie@localhost ~]$ podman run -d -p 9080:80 --name wordpress-noroot wordpress

这次我们使用了不同的网络端口,以避免与之前的设置冲突。现在,让我们停止容器:

[donnie@localhost ~]$ podman container stop wordpress-noroot
a6e2117dd4d5148d01a55d64ad8753c03436bfd9a573435e95d927f74 dc48f9e
[donnie@localhost ~]$

接下来,我们将在用户的正常主目录下创建一个子目录:

[donnie@localhost ~]$ mkdir -p .config/systemd/user/
[donnie@localhost ~]$

我们将生成服务文件,跟之前一样:

[donnie@localhost ~]$ podman generate systemd wordpress-noroot > .config/systemd/user/wordpress-noroot.service
[donnie@localhost ~]$

我之前忘了提到,这些生成的服务文件在 [Install] 部分有一个小的不同。你将看到两个目标,而不是只有一个,如下所示:

. . .
. . .
[Install]
WantedBy=multi-user.target default.target

Default.target 是在你希望从自己的用户账户运行服务时需要使用的。从现在开始,管理命令大致相同,唯一的区别是你不需要使用 sudo,而且需要使用 --user 选项来告诉 systemd 服务单元文件位于你自己的主目录中。让我们加载新的服务文件并检查状态:

[donnie@localhost ~]$ systemctl --user daemon-reload
[donnie@localhost ~]$ systemctl --user status wordpress-noroot
● wordpress-noroot.service - Podman container-a6e2117dd4d5148d01a55d64ad8753c03436bfd9a573435e95d927f74dc48f9e.service
   Loaded: loaded (/home/donnie/.config/systemd/user/wordpress-noroot.service; disabled; vendor preset: enabled)
   Active: inactive (dead)
. . .
. . .

让我们启用它并启动,然后再次检查状态:

[donnie@localhost ~]$ systemctl --user enable --now wordpress-noroot
Created symlink /home/donnie/.config/systemd/user/multi-user.target.wants/wordpress-noroot.service → /home/donnie/.config/systemd/user/wordpress-noroot.service.
Created symlink /home/donnie/.config/systemd/user/default.target.wants/wordpress-noroot.service → /home/donnie/.config/systemd/user/wordpress-noroot.service.
[donnie@localhost ~]$ systemctl --user status wordpress-noroot
● wordpress-noroot.service - Podman container-a6e2117dd4d5148d01a55d64ad8753c03436bfd9a573435e95d927f74dc48f9e.service
   Loaded: loaded (/home/donnie/.config/systemd/user/wordpress-noroot.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2021-04-14 15:44:26 EDT; 12s ago
. . .
. . .

如果你拥有根权限,可以在防火墙上打开端口 9080/tcp,并从外部机器访问 WordPress,就像我们之前做的那样。

目前,我们的无根 WordPress 服务在启动机器时不会自动启动。但它会在你登录机器时启动,并在你登出时停止。通过对你自己的用户账户执行以下操作来修复这个问题:

[donnie@localhost ~]$ loginctl enable-linger donnie
[donnie@localhost ~]$

现在,容器服务将在我登出时继续运行,并且在我重启机器时会自动启动。

总结

在这一章中,我们学习了如何编辑和创建服务单元文件。过程中,我们介绍了可以设置的各种参数,包括一些与安全相关的参数。我们还看到,在将任何修改投入生产之前,测试服务文件的更改是非常重要的。当然,我一直强调的那一点是,掌握如何使用systemd手册页的重要性。

在下一章,我们将讨论systemd目标。到时候见。

问题

  1. 如何对ssh.service文件进行部分编辑?

    a) sudo systemctl edit --partial ssh.service

    b) sudo systemctl edit ssh.service

    c) sudo systemedit ssh.service

    d) sudo systemedit --partial ssh.service

  2. 如何创建一个全新的服务?

    a) sudo systemctl edit --new newservice.service

    b) sudo systemctl edit --full newservice.service

    c) sudo systemctl edit --full --force newservice.service

    d) sudo systemctl edit newservice.service

  3. 如何为服务创建访问白名单?

    a) 只需在[Service]部分插入IPAddressAllow=指令。

    b) 在[Service]部分同时插入IPAddressAllow=指令和IPAddressDeny=指令。

    c) 只需在[Unit]部分插入IPAddressAllow=指令。

    d) 在[Unit]部分同时插入IPAddressAllow=指令和IPAddressDeny=指令。

答案

b

c

b

进一步阅读

第六章:理解systemd targets

在本章中,我们将看看systemd targets 是什么,以及它们能为我们做些什么。现在,我得告诉你,这个话题有些困惑,我希望能澄清这一点。

本章涵盖的特定主题包括以下内容:

  • 理解systemd targets 的目的

  • 理解 target 文件的结构

  • systemd targets 与 SysVinit 运行级别进行比较

  • 理解 target 依赖关系

  • 更改默认 target

  • 临时更改 target

理解 targets 非常重要,它可以在服务器机房或在你自己家里为你提供帮助。如果你准备好了,我们就开始吧。

技术要求

对于本章,您需要一台运行带有图形桌面环境的虚拟机。我将使用我的AlmaLinux 虚拟机,它运行的是Gnome 3 桌面

查看以下链接以观看《代码实战》视频:bit.ly/3Dgar9d

一如既往,这是实践操作,随时跟着做吧。

理解systemd targets 的目的

systemd的遗产,我们有targets代替了运行级别。许多这些 targets 执行着运行级别曾经执行的相同功能。这部分很容易理解。

困惑之处在于,targets 不仅仅是运行级别。正如我们很快会看到的,targets 有很多种,每种都有其特定的目的。在systemd中,target 是一个单元,它将其他systemd单元组合在一起,达到特定的目的。一个 target 可以组合的单元包括服务、路径、挂载点、套接字,甚至其他 targets。

通过执行systemctl list-units -t target命令,您可以查看系统上所有活动的 targets,结果应该是这样的:

图 6.1 – AlmaLinux 中的活动 targets

添加--inactive选项来查看非活动的 targets:

图 6.2 – AlmaLinux 中的非活动 targets

你可能通过查看这些 targets 的名称就能大致猜到它们在做什么。对于那些不那么明显的,您可以查看systemd.special手册页,或搜索具有特定 target 名称的手册页。

接下来,让我们深入看看这些 target 文件,看看我们能发现什么。

理解 target 文件的结构

正如我之前所说,学习systemd的最好方式是查看各种单元文件的示例。在本节中,我们将查看一些.target文件。

理解sockets.target文件

让我们从 sockets.target文件开始,它是我们拥有的最简单的 target 之一:

[Unit]
Description=Sockets
Documentation=man:systemd.special(7)

对,只有这些,整个文件。[Unit] 部分是它唯一的部分,只包含 Description= 行和 Documentation= 行。乍一看,你可能会觉得这肯定对我们没有任何作用。但你错了。查看 /etc/systemd/system/sockets.target.wants 目录,你会看到这个目标只是我们需要运行的所有套接字的集合:

[donnie@localhost sockets.target.wants]$ ls -l
total 0
lrwxrwxrwx. 1 root 43 May  1 17:27 avahi-daemon.socket -> /usr/lib/systemd/system/avahi-daemon.socket
lrwxrwxrwx. 1 root 35 May  1 17:31 cups.socket -> /usr/lib/systemd/system/cups.socket
. . .
. . .
lrwxrwxrwx. 1 root 39 May  1 17:34 sssd-kcm.socket -> /usr/lib/systemd/system/sssd-kcm.socket
lrwxrwxrwx. 1 root 40 May  1 17:27 virtlockd.socket -> /usr/lib/systemd/system/virtlockd.socket
lrwxrwxrwx. 1 root 39 May  1 17:27 virtlogd.socket -> /usr/lib/systemd/system/virtlogd.socket
[donnie@localhost sockets.target.wants]$

为了查看这如何工作,让我们看一下 cups.socket 文件:

[Unit]
Description=CUPS Scheduler
PartOf=cups.service
[Socket]
ListenStream=/var/run/cups/cups.sock
[Install]
WantedBy=sockets.target

你可以在 [Install] 部分看到这个套接字是由 sockets.target 所需要的。换句话说,这个 sockets.target 的套接字会被激活。当然,sockets.target 在几乎所有 Linux 系统上默认都是激活的,所以你通常不需要自己去激活它。cups.socket 通常也是默认激活的,但你可能并不总是需要它。假设你正在运行一个文本模式的服务器,而且你确定你永远不会从中打印任何东西。你可以像禁用服务一样禁用 cups.socket

[donnie@localhost ~]$ sudo systemctl disable --now cups.socket 
[sudo] password for donnie: 
Removed /etc/systemd/system/sockets.target.wants/cups.socket.
[donnie@localhost ~]$

当你这样做时,相关的 cups.service 仍然在运行,所以你还需要停止并禁用它。如果你改变主意,随时可以重新启用该服务和套接字。

理解 sshd.service 文件中的依赖关系

我们已经查看过 sshd.service 文件,但再看一遍还是很有价值的。为了节省空间,我将只展示 [Unit][Install] 部分,这也是我们需要查看的两个部分。

[Unit][Install] 部分

以下是 sshd.service 文件中的 [Unit][Install] 部分:

[Unit]
Description=OpenSSH server daemon
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target sshd-keygen.target
Wants=sshd-keygen.target
. . .
. . .
[Install]
WantedBy=multi-user.target

我们已经在 [Install] 部分看到了 WantedBy=multi-user.target 这一行,意味着当机器启动到多用户模式时,安全 shell 服务会自动启动。

sshd.service 文件的 [Unit] 部分,我们看到 sshd.service 直到 network.targetsshd-keygen.target 启动后才会启动。

现在,让我们看看 network.target 文件里有什么。

理解被动目标

network.target 文件是这样的:

[Unit]
Description=Network
Documentation=man:systemd.special(7)
Documentation=https://www.freedesktop.org/wiki/Software/systemd/NetworkTarget
After=network-pre.target
RefuseManualStart=yes

这里有一个有趣的地方,就是我们在文件末尾看到的 RefuseManualStart=yes 这一行。这意味着这个目标会自动启动,我们不能手动启动它。这就是为什么我们将 network.target 视为 被动 目标的原因。我们还看到 network.target 会在 network-pre.target 启动后启动,而 network-pre.target 也是一个被动目标。

更有趣且稍显好奇的是,network.target 似乎并没有为我们做任何事情。我的意思是,这里没有任何真正做事的代码;它似乎并没有启动任何服务,并且在 /etc/systemd/system/ 目录下也没有 .wants 目录,无法让我们向其中添加服务。我们可以在这里看到:

[donnie@localhost system]$ pwd
/etc/systemd/system
[donnie@localhost system]$ ls -l network.target.wants
ls: cannot access 'network.target.wants': No such file or directory
[donnie@localhost system]$

那么,这里到底发生了什么呢?嗯,这是一个需要一点侦探工作才能找出答案的问题,因为 systemd 的开发者没有很好地记录这一点。答案是,多个目标被硬编码systemd 可执行文件中。network.target 就是一个例子。为了更好地理解这一点,我们可以使用 strings 工具来查看可能存在于 systemd 可执行文件中的任何文本字符串。使用命令如下:

[donnie@localhost systemd]$ pwd
/lib/systemd
[donnie@localhost systemd]$ strings systemd | grep '\.target'

输出应类似如下:

图 6.3 – 硬编码进 systemd 可执行文件的目标

要理解,并非所有这些硬编码的目标都是被动目标。例如,在列表的顶部,你会看到一些与关闭机器、电源重启或抢救机器相关的目标。(我指的是从emergency.target到其他所有目标。)这些目标是我们可以自己调用的。

被动目标在系统初始化部分的引导过程中会自动启动。当机器能够访问网络时,network.target 会被激活。通过在sshd.service文件的[Unit]部分中加入After=network.target,我们确保在网络激活并可用之后,安全外壳服务才会启动。

理解服务模板

为了刷新记忆,让我们再看一下我们sshd.service文件中的[Unit]部分:

[Unit]
Description=OpenSSH server daemon
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target sshd-keygen.target
Wants=sshd-keygen.target

我们看到 sshd.service 要求 sshd-keygen.target,并且在 sshd-keygen.target 启动之前不会启动。让我们来看一下 sshd-keygen.target 文件:

[Unit]
Wants=sshd-keygen@rsa.service
Wants=sshd-keygen@ecdsa.service
Wants=sshd-keygen@ed25519.service
PartOf=sshd.service

我们看到 sshd.target 要求三次调用 sshd-keygen@.service。文件名中的 @ 符号表示这是一个服务模板。当我们调用服务模板时,会将一个变量的值放在 @ 符号后面,这样我们就可以用不同的参数多次运行同一个服务。为了说明我说的是什么,我们来看一下 sshd-keygen@.service 文件:

[Unit]
Description=OpenSSH %i Server Key Generation
ConditionFileNotEmpty=|!/etc/ssh/ssh_host_%i_key
[Service]
Type=oneshot
EnvironmentFile=-/etc/sysconfig/sshd
ExecStart=/usr/libexec/openssh/sshd-keygen %i
[Install]
WantedBy=sshd-keygen.target

首先要注意的是 %i 变量。在 sshd-keygen.target 文件中,我们看到该变量的三个值分别是 rsaecdsaed25519。这些值代表了我们希望在系统中拥有的三种安全外壳密钥类型。

ConditionFileNotEmpty=|!/etc/ssh/ssh_host_%i_key 这一行验证这三个密钥文件是否已存在。在我的 AlmaLinux 系统上,这些密钥如下所示:

[donnie@localhost ~]$ cd /etc/ssh
[donnie@localhost ssh]$ ls -l *key
-rw-r-----. 1 root ssh_keys  492 Feb 11 18:29 ssh_host_ecdsa_key
-rw-r-----. 1 root ssh_keys  387 Feb 11 18:29 ssh_host_ed25519_key
-rw-r-----. 1 root ssh_keys 2578 Feb 11 18:29 ssh_host_rsa_key
[donnie@localhost ssh]$

在这个 ConditionFileNotEmpty= 行中,! 表示我们正在寻找这三个密钥文件的缺失。在 ! 前面的管道符号 (|) 是触发符号。当你将这两个符号结合在一起时,如果这三个密钥文件已经存在,就什么也不会发生。但如果密钥文件不存在| 符号将使该服务运行,以创建这些文件。

[Service]部分,我们看到ExecStart=/usr/libexec/openssh/sshd-keygen %i这一行。这将导致每次%i变量在sshd-keygen.target中定义的每个值都会运行一次sshd-keygen命令。每次运行时,它都会创建我们需要的三个密钥文件之一。

这里最后需要查看的是Type=oneshot这一行,它也位于[Service]部分。这会使服务作为一个普通脚本运行,执行一些指定的单次任务,而不是作为一个持续运行的守护进程。当指定的命令执行完毕后,服务会关闭。

好了,我们已经看到了什么是目标,并且也看了一些简单的示例。现在,让我们来看看取代旧式运行级别的目标。

将 systemd 目标与 SysVinit 运行级别进行比较

旧的 SysV 运行级别定义了操作系统达到某个状态时需要运行的服务。这个概念很简单,除了有四组不同的运行级别定义,Linux 用户需要了解这些定义。首先是通用定义集,这是由 Linux 基金会的大师们作为Linux 标准基础的一部分创建的。Red Hat 的定义几乎与通用定义一样。SlackwareDebian的开发者则从左场出来,创建了他们自己的定义,这些定义与通用定义完全不同。(当然,Slackware 和 Debian 是两种最古老的存活 Linux 发行版,所以他们可能在 Linux 基金会的专家们创建通用定义之前就创建了自己的定义。)这让新 Linux 用户有些困惑,尤其是对于那些需要为 Linux 专业认证考试做准备的人来说。这也让开发者创建能在所有不同 Linux 发行版系列上运行的新服务变得有点困难。幸运的是,目前我们只需要考虑通用定义,以及它们与systemd目标的比较。我们来看看下面的表格:

systemd中,有一些运行级别类型的目标,它们没有对应的 SysV 版本:

  • emergency.target类似于rescue.target,只是文件系统以只读方式挂载。

  • hibernate.target保存系统状态,然后关闭机器。

  • suspend.target仅仅是将系统置于睡眠状态,而不关闭电源。

hibernate.targetsuspend.target,它们在 Linux 的服务器实现中不需要,但对于越来越多在笔记本和台式计算机上使用 Linux 的人来说,这些功能是非常有帮助的。在 systemd 之前,实施这些功能没有一个好的、标准化的方式。

请注意,在官方的通用定义集中,runlevel 2runlevel 4并不完全对应于多用户目标。由于某些原因,每次解释运行级别与目标的关系时,总是将运行级别 2 和 4 放在这里,我也不太清楚为什么。

SysV 和 systemd 之间的一个重大区别是,在 SysV 中,每个运行级别都是一个独立的单元。所以,如果你将机器设置为启动到 runlevel 5,它会直接进入 runlevel 5。在 systemd 中,一个目标可以依赖于另一个目标,而后者又可能依赖于另一个目标。以这个 graphical.target 单元文件为例:

[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

Requires=multi-user.target 行意味着,除非 multi-user.target 已经在运行,否则 graphical.target 将无法启动。所有在多用户模式下启动的服务将在图形模式下继续运行。Wants=display-manager.service 行意味着它希望启动显示管理器,但如果显示管理器没有启动,它不会失败。

Conflicts=rescue.service rescue.target 行告诉 systemd,如果启动了 rescue.servicerescue.target,则关闭 graphical.target;如果启动了 graphical.target,则关闭 rescue.servicerescue.target。当然,shutdown.target 也是一个冲突项,但我们不需要列出它。Conflicts=shutdown.target 参数已经隐含在其中。

After= 行似乎有点奇怪,对吧?我的意思是,确实可以理解在 multi-user.targetdisplay-manager.service 启动完成之前,graphical.target 无法运行。但是 rescue.servicerescue.target 呢?为什么需要这些抢救单元在启动 graphical.target 之前运行呢?实际上,我们并不需要它们。只是 After= 指令也影响着在关闭目标时会发生什么。在这种情况下,该指令表示,如果你决定从 graphical.target 切换到 rescue.targetrescue.targetrescue.service 需要等到 graphical.target 关闭后才会启动。所以,通过从图形模式切换到抢救模式,After= 行以相反的方式运作。

最后一行是 AllowIsolate=yes。这意味着我们可以根据需要从这个目标切换到另一个目标。例如,如果我们需要从图形模式切换到纯命令行模式,我们可以 孤立multi-user.target。(是的,术语有点奇怪,但就是这样。)在我们继续之前,让我们再多谈谈 依赖关系

理解目标依赖关系

在这个 graphical.target 文件中,Requires=multi-user.target 行表示,multi-user.target 必须在 graphical.target 启动之前已经运行。所以,multi-user.targetgraphical.target 的依赖项。现在,让我们看看 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

在这里,我们看到 multi-user.target 需要 basic.target。所以,让我们看看 basic.target 文件,看看它需要什么:

[Unit]
Description=Basic System
Documentation=man:systemd.special(7)
Requires=sysinit.target
Wants=sockets.target timers.target paths.target slices.target
After=sysinit.target sockets.target paths.target slices.target tmp.mount

好的,basic.target 需要 sysinit.target。那么,让我们看看 sysinit.target 需要什么:

[Unit]
Description=System Initialization
Documentation=man:systemd.special(7)
Conflicts=emergency.service emergency.target
Wants=local-fs.target swap.target
After=local-fs.target swap.target emergency.service emergency.target

sysinit.target并不要求任何内容,但它需要local-fs.targetswap.target。这些链式目标中的一些在/etc/systemd/system/目录下有自己的.wants目录,里面包含指向将要启动的服务的符号链接。例如,这里是/etc/systemd/system/sysinit.target.wants/目录的内容:

[donnie@localhost sysinit.target.wants]$ ls -l
total 0
lrwxrwxrwx. 1 root 44 May  1 17:22 import-state.service -> /usr/lib/systemd/system/import-state.service
lrwxrwxrwx. 1 root 44 May  1 17:27 iscsi-onboot.service -> /usr/lib/systemd/system/iscsi-onboot.service
. . .
. . .
lrwxrwxrwx. 1 root 56 May  1 17:21 selinux-autorelabel-mark.service -> /usr/lib/systemd/system/selinux-autorelabel-mark.service
[donnie@localhost sysinit.target.wants]$

尝试弄清楚一个目标的所有依赖项可能看起来像是一个复杂的操作,但实际上并不是。要查看graphical.target的依赖项,我们只需执行systemctl list-dependencies graphical.target,像这样:

[donnie@localhost ~]$ systemctl list-dependencies graphical.target 
graphical.target
● ├─accounts-daemon.service
● ├─gdm.service
● ├─rtkit-daemon.service
● ├─systemd-update-utmp-runlevel.service
● ├─udisks2.service
● └─multi-user.target
●   ├─atd.service
●   ├─auditd.service
●   ├─avahi-daemon.service
. . .
. . .

整个输出太长,无法在此列出,但你大概明白了。

--after--before选项显示必须在目标启动之前之后启动的依赖项。(不,我没有搞错。--after选项表示目标必须在列出的依赖项之后启动,而--before选项表示目标必须在列出的依赖项之前启动。)举个简单的例子,看看在network.target启动之前,必须先启动哪些内容:

[donnie@localhost ~]$ systemctl list-dependencies --after network.target 
network.target
● ├─NetworkManager.service
● ├─wpa_supplicant.service
● └─network-pre.target
●   ├─firewalld.service
●   └─nftables.service
[donnie@localhost ~]$

如果你愿意,你还可以创建一个图形化表示来展示目标的依赖项。为此,首先需要安装graphviz包。在 AlmaLinux 机器上执行的命令是:

sudo dnf install graphviz

接下来,我们使用systemd-analyze来创建显示graphical.target依赖项的图形文件。命令如下所示:

[donnie@localhost ~]$ systemd-analyze dot graphical.target | dot -Tsvg > graphical.svg
   Color legend: black     = Requires
                 dark blue = Requisite
                 dark grey = Wants
                 red       = Conflicts
                 green     = After
[donnie@localhost ~]$

最后,在Firefox中打开生成的graphical.svg文件。可以调整图像大小以适应屏幕,或者使用底部的滑块查看图像的不同部分:

图 6.4 – graphical.target 依赖项

(注意,您的图形可能与我的不完全相同。我不知道为什么,但事实就是如此。)

我想表达的观点是,目标可以有一整条依赖链。这使得我们可以拥有更模块化的配置,从而无需为每个目标文件创建其完整的依赖列表。这与 SysV 的工作方式相反。对于 SysV,每个运行级别都有一个符号链接目录,指向为该指定运行级别启动的所有服务。

现在我们已经了解了什么是目标以及它们是如何构建的,让我们看看如何设置默认目标。

更改默认目标

当你安装 Linux 操作系统时,安装程序将根据是否选择安装图形桌面环境来配置multi-user.targetgraphical.target为默认目标。当你启动 Linux 机器时,默认目标可能非常明显。如果出现图形桌面,你可以放心地认为graphical.target已设置为默认目标:

图 6.5 – 带有 Gnome 3 桌面的 AlmaLinux

然而,如果没有显示图形桌面,这并不一定意味着机器以 multi-user.target 作为其默认设置。可能是 graphical.target 是默认设置,并且图形显示管理器未能启动。(我曾经见过视频卡驱动程序配置错误时发生几次。)

要查看设置为默认的目标,请使用 systemctl get-default

[donnie@localhost system]$ systemctl get-default
graphical.target
[donnie@localhost system]$

您还可以通过查看 /etc/systemd/system/default.target 符号链接来查看默认设置,看起来像这样:

[donnie@localhost system]$ ls -l default.target 
lrwxrwxrwx. 1 root 40 May  4 18:30 default.target -> /usr/lib/systemd/system/graphical.target
[donnie@localhost system]$

我们看到符号链接指向 graphical.target 文件。

现在,假设您不再希望此计算机启动到图形模式。只需将其设置为多用户模式,就像这样:

[donnie@localhost system]$ sudo systemctl set-default multi-user
[sudo] password for donnie: 
Removed /etc/systemd/system/default.target.
Created symlink /etc/systemd/system/default.target → /usr/lib/systemd/system/multi-user.target.
[donnie@localhost system]$

您可以看到 default.target 符号链接现在指向 multi-user.target 文件。现在重新启动此计算机,图形桌面将不会启动。验证此操作有效后,继续通过以下方式将其切换回图形模式:

[donnie@localhost ~]$ sudo systemctl set-default graphical
Removed /etc/systemd/system/default.target.
Created symlink /etc/systemd/system/default.target → /usr/lib/systemd/system/graphical.target.
[donnie@localhost ~]$

然后重新启动机器以返回到图形模式。

好的,这一切都很好。但是有时我们可能只是想暂时切换到另一个目标,而不更改默认设置。让我们看看如何操作。

临时更改目标

您还可以在不更改默认设置的情况下从一个目标切换到另一个目标。这在某些情况下非常方便。例如,假设您正在设置一个带有 Nvidia 显卡的游戏电脑。现在,如果您的 Linux 计算机只是用来上网或进行普通办公工作,那么随 Linux 发行版提供的开源 Nvidia 驱动程序完全足够。然而,对于游戏来说,开源驱动可能无法提供您真正需要的游戏性能。为了解决这个问题,您将访问 Nvidia 网站并下载他们的专有驱动程序。安装过程的第一步是将机器从图形模式切换到文本模式。使用 systemd,我们将使用 systemctl isolate 选项,就像这样:

[donnie@localhost ~]$ sudo systemctl isolate multi-user

这将关闭图形服务器,并将您带回文本模式登录提示符:

图 6.6 – Alma Linux 上的文本模式登录

要返回到图形模式,您可以重新启动机器,假设仍将 graphical.target 设置为默认。或者,您可以再次运行 isolate 命令,如下所示:

[donnie@localhost ~]$ sudo systemctl isolate graphical

请注意,有时候切换回图形模式可能会有些古怪,因此您可能会发现最好的方法是重新启动。此外,如果您安装了视频驱动程序,无论如何都需要重新启动。

现在,如果你像我一样是个年纪较大的老家伙,你可能已经习惯了老式的做法,以至于不能适应新的方式。好消息是,你仍然可以使用旧的 SysV 命令来更改目标,如果你真的想的话。为了支持这种方式,systemd的开发者创建了指向相应目标的符号链接。它们看起来是这样的:

[donnie@localhost ~]$ cd /lib/systemd/system
[donnie@localhost system]$ ls -l runlevel*.target
lrwxrwxrwx. 1 root 15 Apr  7 04:46 runlevel0.target -> poweroff.target
lrwxrwxrwx. 1 root 13 Apr  7 04:46 runlevel1.target -> rescue.target
lrwxrwxrwx. 1 root 17 Apr  7 04:46 runlevel2.target -> multi-user.target
lrwxrwxrwx. 1 root 17 Apr  7 04:46 runlevel3.target -> multi-user.target
lrwxrwxrwx. 1 root 17 Apr  7 04:46 runlevel4.target -> multi-user.target
lrwxrwxrwx. 1 root 16 Apr  7 04:46 runlevel5.target -> graphical.target
lrwxrwxrwx. 1 root 13 Apr  7 04:46 runlevel6.target -> reboot.target
[donnie@localhost system]$

旧版的inittelinit命令仍然存在,所以你可以使用其中任一命令来更改运行级别。例如,你可以使用这两个命令中的任意一个从图形模式切换到多用户模式:

sudo init 3
sudo telinit 3

要返回图形模式,只需再次运行这些命令,将3替换为5

说笑归说笑,很高兴他们为那些真正需要的人包括了向后兼容的内容。不过,你仍然应该习惯现代的systemctl isolate方式,因为systemd的开发者随时可能会移除这些向后兼容的内容。

好的,我想这差不多涵盖了所有内容。让我们结束这一章并收尾。

总结

和往常一样,我们覆盖了大量内容并看到了很多酷的东西。我们了解了systemd目标的目的及其结构。然后,我们比较了systemd目标和旧版 SysVinit 运行级别的异同,并看了如何查看目标的依赖关系。最后,我们讨论了如何设置默认的运行级别以及如何临时更改运行级别。

在下一章中,我们将讨论systemd定时器。下章见。

问题

  1. 什么是目标?

    a) 它只是旧式运行级别的另一种名称。

    b) 它是一个将其他单元组合在一起以实现特定目的的单元。

    c) 它是一个启动服务的单元。

    d) 它是一个监听网络连接的单元。

  2. 什么是被动目标?

    a) 它是一个你无法自己启动的目标。

    b) 它是一个占位目标,不做任何事情。

    c) 被配置为TargetMode=passive的目标是被动目标。

    d) 它是一个仅在后台运行的目标。

  3. 如何从图形模式切换到文本模式?

    a) sudo systemctl isolate text-mode

    b) sudo systemctl 3

    c) sudo systemctl isolate multi-user

    d) sudo runlevel multi-user

  4. SysV 运行级别和systemd目标之间的主要区别是什么?

    a) SysV 运行级别相互依赖,systemd目标是独立的单元。

    b) systemd目标是相互依赖的。每个 SysV 运行级别都有自己完整的服务列表。

    c) SysV 运行级别比systemd目标运行得更高效。

    d) 没有真正的区别。

  5. 以下哪个命令可以显示默认目标?

    a) systemctl show-target

    b) systemctl show-default

    c) systemctl default

    d) systemctl get-default

答案

  1. b

  2. a

  3. c

  4. b

  5. d

进一步阅读

network.target的解释:

www.freedesktop.org/wiki/Software/systemd/NetworkTarget/

启动 CentOS 进入紧急模式或恢复模式:

www.thegeekdiary.com/how-to-boot-into-rescue-mode-or-emergency-mode-through-systemd-in-centos-rhel-7-and-8/

启动 Ubuntu 进入紧急模式或恢复模式:

linuxconfig.org/how-to-boot-ubuntu-18-04-into-emergency-and-rescue-mode

第七章: 理解 systemd 定时器

繁忙的系统管理员喜欢找到方法简化他们的工作。一个方法是通过设置自动执行的计划来将尽可能多的日常任务自动化。在本章中,我们将学习如何使用 systemd 定时器来做到这一点。我们将探讨的具体主题包括以下内容:

  • systemd 定时器与 cron 进行比较

  • 理解定时器选项

  • 创建定时器

如果你准备好了,那我们就开始吧。

技术要求

一如既往,我们将使用 Ubuntu Server 20.04 虚拟机和 Alma Linux 8 虚拟机进行演示。所有内容都是动手操作,因此可以随时在你自己的虚拟机上跟着做。

查看以下链接以观看“代码实战”视频:bit.ly/31pQdfS

systemd 定时器与 cron 进行比较

cron 调度工具家族自 1975 年 5 月以来便成为了 Unix 和类 Unix 操作系统的一部分。在 1980 年代,作为理查德·斯托曼(Richard Stallman)倡导的 自由软件 运动的一部分,几种自由软件版本的 cron 被创建。互联网名人堂成员保罗·维克西(Paul Vixie)于 1987 年创建了自己版本的 cron,该版本在 Linux 世界中成为了最广泛使用的版本。(实际上,如果你查看 cron 的手册页,你仍然可以在底部的 Authors 部分看到保罗·维克西的名字。)

cron 的一个大优点是它的极简性。创建一个 cron 作业所需要的仅仅是一行简单的代码,通常看起来像这样:

图 7.1 – 一个 cron 作业示例

在这个非常简单的示例中,我从我一台古老的 CentOS 6 虚拟机上拿到的,我在每小时的 25 分钟和 55 分钟运行一个简单的任务。每小时两次,这个 cron 作业会将一条消息插入到系统日志文件中。任何非特权用户只要需要,都可以创建 cron 作业来执行一些非特权任务,而拥有适当 root 权限的用户则可以创建系统级作业。作业可以设置为在一周中的特定日子、一个月中的特定日子、特定时间或重启机器时运行。这里有很多灵活性,而且一切都非常容易设置。

cron 的另一个优点是它在 Unix 和类 Unix 操作系统的世界中无处不在,而 systemd 仅存在于 Linux 世界中。如果你是一个管理着混合 Unix 和 Linux 服务器的大型系统管理员,你可能会发现继续使用 cron 更为方便。

设置 systemd 定时器并不难,但确实需要花费更多的时间和精力。首先,你不能直接从 systemd 定时器访问命令或脚本。你首先需要创建一个 systemd 服务,然后从定时器中调用该服务。然而,使用 systemd 定时器有很多优点,因此学习如何使用它们可能值得付出这些努力。

使用 systemd 定时器,你可以在设置任务调度时获得更多的灵活性和准确性。你为定时器创建的服务可以利用资源管理、安全性以及所有使用 systemd 时带来的好处。你可以创建在某个预定义事件发生时触发的定时器,或者可以指定你希望定时器触发的日历和时钟时间。作为额外的好处,systemd 会在系统日志文件中记录定时器事件的完成情况。而在使用 cron 任务时,你是没有这些功能的。

所以,你现在可能在想应该使用这两种任务调度系统中的哪一个。实际上,cron 仍然会预装在现代 Linux 系统上。如果你只是需要快速创建一个简单的任务,使用 cron 完全没有问题。但如果你需要设置一些更复杂的任务调度,那么一定要使用 systemd 定时器。即使你只需要做一些简单的事情,设置一个定时器也可能会让你更熟悉这个过程,值得一试。

好了,这部分介绍大概够了。接下来我们来看看如何查看你系统上 systemd 定时器的信息。

查看定时器信息

当你首次安装 Linux 操作系统时,会发现系统上已经有一些活跃的定时器来处理某些管理任务。你可以通过使用 systemctl list-unit-files -t timer 命令查看它们。在你的 Alma Linux 系统上,输出应该如下所示:

图 7.2 – Alma Linux 上的定时器

我们看到安装了 12 个定时器,但只有两个是启用的。两个是静态的,这意味着它们无法启用或禁用,其他的都被禁用了。

在 Ubuntu 服务器上,我们看到启用的定时器更多:

图 7.3 – Ubuntu 服务器上的定时器

systemctl list-timers 命令展示了六个信息字段,内容如下:

图 7.4 – systemctl list-timers

这六个字段分别是:

  • 下次运行时间:这显示定时器下次预定的运行时间。

  • 剩余时间:这显示定时器下次运行前剩余的时间。

  • 上次运行时间:这显示定时器最后一次运行的时间。

  • 已过时间:这显示自定时器最后一次运行以来已经过去的时间。

  • 单元:这是定时器的单元文件名称。

  • 激活服务:这是定时器运行的服务名称。通常它与定时器的名称相同,但不一定是。

你可以通过 systemctl status 查看一些信息,像这样:

[donnie@localhost ~]$ systemctl status dnf-makecache.timer
• dnf-makecache.timer - dnf makecache --timer
   Loaded: loaded (/usr/lib/systemd/system/dnf-makecache.timer; enabled; vendor preset: enabled)
   Active: active (waiting) since Thu 2021-05-20 12:29:35 EDT; 2h 55min ago
  Trigger: Thu 2021-05-20 15:39:35 EDT; 14min left
May 20 12:29:35 localhost.localdomain systemd[1]: Started dnf makecache --timer.
[donnie@localhost ~]$

就像你可以使用服务和目标一样,你也可以查看定时器的依赖树,方法如下:

[donnie@localhost ~]$ systemctl list-dependencies dnf-makecache.timer
dnf-makecache.timer
• ├─network-online.target
• │ └─NetworkManager-wait-online.service
• └─sysinit.target
•   ├─dev-hugepages.mount
•   ├─dev-mqueue.mount
•   ├─dracut-shutdown.service
•   ├─import-state.service
•   ├─iscsi-onboot.service
•   ├─kmod-static-nodes.service
•   ├─ldconfig.service
. . .
. . .

我们已经看到几种查看系统上定时器信息的方法。接下来,我们来看一些配置选项。

理解定时器选项

解释定时器选项的最佳方式是查看一些已经存在于我们系统中的定时器示例。我们将首先查看 Alma Linux 机器上的一个定时器。

理解单调定时器

有两种方法可以指定你希望服务自动运行的时间。在这一部分,我们将介绍单调方法。这意味着你不需要配置作业在特定的日历时间和时钟时间运行,而是配置作业在某种作为起始点的事件之后运行。起始点可以是系统启动、定时器激活、定时器关联服务上次运行后的时间,或者其他几种情况(你可以通过查看systemd.timer的手册页来查看所有单调起始点)。作为单调定时器的示例,我们来看看 Alma Linux 机器上的dnf-makecache.timer

类似 Red Hat 的操作系统,如 Alma Linux,使用dnf工具进行更新和包管理。和所有 Linux 包管理系统一样,dnf维护一个关于发行版包仓库中内容的本地缓存。定期需要刷新缓存。我们可以手动执行sudo dnf makecache命令来实现,但 Red Hat 类系统都自带一个定时器来自动执行这一操作。以下是该定时器的样子:

[Unit]
Description=dnf makecache --timer
ConditionKernelCommandLine=!rd.live.image
# See comment in dnf-makecache.service
ConditionPathExists=!/run/ostree-booted
Wants=network-online.target
[Timer]
OnBootSec=10min
OnUnitInactiveSec=1h
Unit=dnf-makecache.service
[Install]
WantedBy=timers.target

[Unit]部分,我们看到:

  • ConditionKernelCommandLine=!rd.live.image:如果机器是从某种实时媒体(例如实时 DVD)启动的,这条指令会阻止定时器运行。

  • ConditionPathExists=!/run/ostree-booted:该指令会查找/run/ostree-booted目录,如果该目录存在,定时器就不会运行。(根据ostree的手册页,你会使用ostree来管理不同版本的文件系统树。这些文件系统树是以只读方式挂载的,因此尝试更新它们的缓存效果不大。)

  • Wants=network-online.target:这会阻止定时器在网络服务可用之前运行。(你已经看到了一些使用systemd定时器可以做的事情,而这些是cron做不到的。)

接下来,我们看到[Timer]部分,其中有两个单调定时器设置的示例。正如我之前提到的,单调定时器是相对于某个起始点来定义的,而不是由日历和时钟时间来决定的:

  • OnBootSec=10min:正如我们稍后会看到的,这个定时器会激活dnf-makecache.service。这一行使得该服务在系统启动后 10 分钟运行。

  • OnUnitInactiveSec=1h:这一行表示定时器会在上次运行后的一个小时再次运行dnf-makecache.service。换句话说,这一行使得该服务大约每小时运行一次。

  • Unit=dnf-makecache.service:在这种情况下,这一行并不是必需的。默认情况下,定时器会激活与定时器同名的服务。只有在定时器激活一个与其名称不同的服务时,你才需要这行。不过,有些人喜欢在任何情况下都使用这个参数,这也是可以的。

    注释

    你可以在systemd.timer手册页中查看其余的单调定时器参数。

[Install]部分是相当标准的内容。我们看到的只是WantedBy=timers.target这一行,它使得定时器在timers.target启动时运行。

既然提到这个,我们不妨看看这个定时器启动的dnf-makecache.service

[Unit]
Description=dnf makecache
ConditionPathExists=!/run/ostree-booted
After=network-online.target
[Service]
Type=oneshot
Nice=19
IOSchedulingClass=2
IOSchedulingPriority=7
Environment="ABRT_IGNORE_PYTHON=1"
ExecStart=/usr/bin/dnf makecache --timer

[Service]部分,我们看到了一些在服务文件中没有见过的内容:

  • Type=oneshot:好了,实际上我们之前已经见过这个。这里我只是想说明,对于定时器调用的服务,需要使用oneshot类型。(当你仔细想想时,实际上是很有道理的。)

  • Nice=19:这会使服务以19的优先级运行,即以最低的优先级运行。(Nice 值的范围从-20到正19。虽然这看起来有点不符合直觉,但-20表示可以分配给进程的最高优先级,而正19表示最低优先级。)这个设置有助于防止该服务影响到可能更重要的其他进程。

  • IOSchedulingClass=2:这设置了我们想要使用的输入/输出调度程序方案类型。值为2意味着我们希望使用best-effort类型的调度类。(你可以在systemd.exec手册页中查看其他IOSchedulingClass类型。)

  • IOSchedulingPriority=7IOSchedulingPriority的值范围从07,其中0是最高优先级,7是最低优先级。这是另一种避免该服务影响系统其他部分的方式。

  • ExecStart=/usr/bin/dnf makecache --timer:这里的--timer选项与systemd定时器无关。实际上,这是一个与dnf命令一起使用的选项。根据dnf手册,--timer使dnf更加关注资源使用,因此如果计算机正在使用电池电源时,它不会运行。它还会使dnf makecache命令在最近已经执行过时立即中止。

这个服务文件的[Install]部分因为缺失而显得尤为显眼。没有[Install]部分使得这是一个静态类型的服务,你不能启用它。相反,它会在dnf-makecache.timer激活时自动运行。

好的,以上就是这个例子的内容。接下来,我们将探讨指定任务运行时间的其他方式。

理解实时定时器

你可以使用实时定时器来在任何日历日期和任何时刻运行任务。对于我们第一个简单的例子,我们来看看 Alma 机器上的fstrim.timer

[Unit]
Description=Discard unused blocks once a week
Documentation=man:fstrim
[Timer]
OnCalendar=weekly
AccuracySec=1h
Persistent=true
[Install]
WantedBy=timers.target

下面是详细解释:

  • OnCalendar=weekly:你将使用OnCalendar参数来指定你希望任务运行的时间。将该任务设置为每周运行意味着它将在每周一的午夜运行。(稍后我们会看到这些参数是在哪里定义的。)

  • AccuracySec=1h:这定义了任务允许延迟的时间。一小时的延迟意味着这个任务可能在周一凌晨到一点之间的任何时候运行。如果你省略这一行,默认的延迟时间为一分钟。如果你希望任务准确地在周一午夜运行,可以将1h改为1us,这将提供最高精度。

  • Persistent=true:那么,如果你的机器在周一凌晨关机,会发生什么呢?没有这一行的话,这个任务将被跳过。有了这一行,当你下次启动机器时,任务将会运行。

如你所见,这个计时器中没有Unit=行,正如我们在之前的例子中看到的那样。所以,默认情况下,这个fstrim.timer将激活fstrim.service,它会清理存储驱动器上未使用的块。以下是服务的样子:

[Unit]
Description=Discard unused blocks
[Service]
Type=oneshot
ExecStart=/usr/sbin/fstrim -av

好的,这里没有什么新内容。它只是一个标准的静态oneshot类型服务,就像我们在之前的例子中看到的那样。

在 Alma 机器上,默认情况下fstrim.timer是禁用的,正如我们所看到的那样:

[donnie@localhost ~]$ systemctl is-enabled fstrim.timer
disabled
[donnie@localhost ~]$

如果你正在使用固态硬盘或精简配置的存储,fstrim.timer非常有用。如果需要启用该计时器,只需像启用服务一样进行操作,示例如下:

[donnie@localhost ~]$ sudo systemctl enable --now fstrim.timer
[sudo] password for donnie: 
Created symlink /etc/systemd/system/timers.target.wants/fstrim.timer → /usr/lib/systemd/system/fstrim.timer.
[donnie@localhost ~]$

在 Ubuntu 机器上,你会看到fstrim.timer默认是启用的:

donnie@ubuntu2004:~$ systemctl is-enabled fstrim.timer
enabled
donnie@ubuntu2004:~$

接下来,我们来仔细看看如何定义OnCalendar的时间。

理解实时计时器的日历事件

好的,事情可能会变得有点复杂。我的意思是,配置cron任务的时间是简单直接的。理解如何为systemd计时器设置时间则需要一些时间来适应。最好的办法是打开systemd.time的手册页面,滚动到CALENDAR EVENTS部分。那里的解释可能不够清晰,但你基本可以通过查看示例来理解。让我们看看能否弄清楚其中的含义。

在我们刚刚查看的fstrim.timer例子中,我们看到OnCalendar=weekly行,这使得任务每周一凌晨运行。在systemd.time的手册页面中,你将看到所有预定义事件时间的完整列表:

minutely → *-*-* *:*:00
hourly → *-*-* *:00:00
daily → *-*-* 00:00:00
monthly → *-*-01 00:00:00
weekly → Mon *-*-* 00:00:00
yearly → *-01-01 00:00:00
quarterly → *-01,04,07,10-01 00:00:00
semiannually → *-01,07-01 00:00:00

其中大部分很容易理解。唯一可能让你有些困惑的是quarterlysemiannuallyquarterly任务将在每年 1 月、4 月、7 月和 10 月的第一天午夜运行,如01,04,07,10部分所示。semiannually任务将在每年 1 月和 7 月的第一天午夜运行,如01,07部分所示。

向下滚动systemd.time页面,你会看到一大堆关于如何设置任务时间的例子。与其尝试展示整个列表,我只会给你一个例子,然后为你逐一解释。来看看:

2003-03-05 → 2003-03-05 00:00:00

左侧显示的是日期和时间,像人类通常写的那样。右侧显示的是你将用作OnCalendar=参数的值。我选择这个例子是因为它用了所有的字段。(是的,我知道 2003 年已经过去,但这就是 man 页面里的内容。)要创建一个任务,使其在 2003 年 3 月 3 日的午夜运行,OnCalendar=行应如下所示:

OnCalendar=2003-03-05 00:00:00

由于这是几年前的设置,让我们修正它,让任务在未来运行:

OnCalendar=2525-03-05 00:00:00

啊,是的。在 2525 年,如果人类仍然活着,如果女性还能存活……(除了我,谁还记得那首傻歌?)

说真的,这并不像最初看起来那么难。我们有Year-Month-Date,然后是 24 小时格式的Hour:Minute:Second。所以,实际上,这非常简单。现在,假设我们希望任务每天晚上 6:15 运行。我们只需用标准的通配符符号( * )替换Year-Month-Date字段,并更改时间:

OnCalendar=*-*-* 18:15:00

这不错,但我改变了主意,不想每天都运行。我想我会改成每月的第五天运行,就像这样:

OnCalendar=*-*-05 18:15:00

不,还是不够频繁。让我们让任务在每月的第五、第十和第十五天运行:

OnCalendar=*-*-05,10,15 18:15:00

Day-of-Week字段是可选的。让我们让任务在每月的第五、第十和第十五天运行,但只有在这些日子恰好是周一或周三时才运行:

OnCalendar=Mon,Wed *-*-05,10,15 18:15:00

更好的是,我们就让它每周一和周三运行:

OnCalendar=Mon,Wed *-*-* 18:15:00

你可以使用波浪号( ~ )符号来表示从每个月的最后一天起回溯指定的天数。如果你希望任务在每年二月的倒数第三天运行,只需要这样做:

OnCalendar=*-02~03 18:15:00

现在,让我们让任务在每年五月的最后一个周一运行:

OnCalendar=Mon *-05~07/1 18:15:00

最后,让我们更疯狂一些,让任务每十分钟运行一次:

OnCalendar=*:00/10

好的,这些应该足够给你一个线索。如果你需要查看更多例子,只需查看systemd.time的 man 页面。

创建定时器

创建你自己的定时器是一个两阶段的过程。你首先创建你想要运行的服务,然后创建并启用定时器。

创建系统级定时器

假设你是一个注重安全的人,怀疑有人可能会试图在你的机器上植入 rootkit。你想设置 Rootkit Hunter,每天在工作时间结束后运行一次。

注:

我想用 Ubuntu 和 Alma Linux 都试试。不幸的是,Ubuntu 的 Rootkit Hunter 包存在一个 bug,导致 Rootkit Hunter 无法更新其签名数据库。这也不太令人惊讶,因为 Ubuntu 的质量控制一直不算完美。所以,在这个例子中,我们就选择 Alma 来演示。

由于 Ubuntu 中的 Rootkit Hunter 包存在 bug,我们将在 Alma 机器上执行此操作。Rootkit Hunter 不在常规的 Alma 软件源中,因此你首先需要安装 EPEL 软件源,方法如下:

sudo dnf install epel-release
sudo dnf update
sudo dnf install rkhunter

通过执行以下命令创建rkhunter.service文件:

sudo systemctl edit --full --force rkhunter.service

使文件看起来像这样:

[Unit]
Description=Rootkit Hunter
[Service]
Type=oneshot
ExecStartPre=/usr/bin/rkhunter --propupd
ExecStartPre=/usr/bin/rkhunter --update
ExecStart=/usr/bin/rkhunter -c --cronjob --rwo

在实际扫描之前,我们需要创建rkhunter.dat文件,用于存储文件属性并更新 rootkit 签名数据库。我们将通过两个ExecStartPre=行来完成。ExecStart=行中有三个选项,如下所示:

  • -c:这是检查选项,执行实际的扫描。

  • --cronjob:通常,Rootkit Hunter 在扫描过程中会暂停几次并等待用户输入。此选项会让 Rootkit Hunter 在不暂停的情况下完成扫描。

  • --rwo:这个选项让 Rootkit Hunter 只报告它找到的任何问题。

在创建定时器之前,最好先手动启动服务,以验证它是否正常工作。我们将使用以下命令进行验证:

[donnie@localhost ~]$ sudo systemctl start rkhunter

运行完成后,查看/var/log/rkhunter/rkhunter.log文件,验证是否存在任何问题。如果一切正常,我们就准备创建定时器。可以使用以下命令:

[donnie@localhost ~]$ sudo systemctl edit --full --force rkhunter.timer

为了演示,设置OnCalendar=时间为未来几分钟。这样,你就不需要等待太久它就能运行。完成后,文件应如下所示:

[Unit]
Description=Rootkit Hunter
[Timer]
OnCalendar=*-*-* 17:50:00
Persistent=true
[Install]
WantedBy=timer.target

执行daemon-reload,然后启用定时器:

[donnie@localhost ~]$ sudo systemctl daemon-reload
[donnie@localhost ~]$ sudo systemctl enable --now rkhunter.timer
Created symlink /etc/systemd/system/timer.target.wants/rkhunter.timer → /etc/systemd/system/rkhunter.timer.
[donnie@localhost ~]$

现在只需要等待定时器运行,看看它是否能正常工作。运行完成后,你可以在/var/log/rkhunter/rkhunter.log文件中查看结果。

接下来,让我们允许一个普通的非特权用户创建一个定时器。

创建一个用户级别的定时器

你可以在 Ubuntu 或 Alma 虚拟机上尝试此演示。我已经有一段时间没有使用 Ubuntu 机器了,所以我选择使用它。

假设你是一个普通用户,想要将你的主目录备份到便捷的移动硬盘上。你将把它插入电脑的 USB 端口,系统会自动将其挂载到/media/backup/目录下。

注意

如果你没有便携式备份硬盘,可以通过手动创建备份目录并设置权限,使普通用户能够写入目录,像这样:

sudo mkdir /media/backup

sudo chmod 777 /media/backup

现在,只需创建服务和定时器。任何非特权用户都可以通过使用--user选项来完成这一操作。

和之前一样,我们从创建服务开始,像这样:

donnie@ubuntu2004:~$ systemctl edit --user --full --force backup.service

这将自动在/home/donnie/.config/目录下创建必要的文件和目录。我将使用rsync来做备份,所以我的backup.service文件将如下所示:

[Unit]
Description=Backup my home directory
[Service]
Type=oneshot
ExecStart=/usr/bin/rsync -a /home/donnie /media/backup

我想确保这个可以正常工作,所以我会先执行daemon-reload,然后手动运行服务,确保没问题后再创建定时器:

donnie@ubuntu2004:~$ systemctl daemon-reload --user
donnie@ubuntu2004:~$ systemctl start --user backup.service
donnie@ubuntu2004:~$

如果运行成功,我应该会在/media/backup/目录下看到donnie/目录:

donnie@ubuntu2004:~$ ls -l /media/backup/
total 20
drwxr-xr-x 6 donnie donnie  4096 May 21 15:58 donnie
drwx------ 2 root   root   16384 May 22 15:34 lost+found
donnie@ubuntu2004:~$

到目前为止,一切顺利。让我们看看donnie/目录里有什么:

donnie@ubuntu2004:~$ ls -la /media/backup/donnie/
total 44
drwxr-xr-x 6 donnie donnie 4096 May 21 15:58 .
drwxrwxrwx 4 root   root   4096 May 22 15:49 ..
-rw------- 1 donnie donnie 3242 May 21 22:07 .bash_history
-rw-r--r-- 1 donnie donnie  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 donnie donnie 3771 Feb 25  2020 .bashrc
drwx------ 2 donnie donnie 4096 Jan  6 01:47 .cache
drwxr-xr-x 4 donnie donnie 4096 May 21 20:30 .config
-rw------- 1 donnie donnie  263 Apr  3 19:00 .lesshst
drwxrwxr-x 3 donnie donnie 4096 May 21 15:58 .local
-rw-r--r-- 1 donnie donnie  807 Feb 25  2020 .profile
-rw-r--r-- 1 donnie donnie    0 Jan  6 01:47 .sudo_as_admin_successful
drwxr-xr-x 3 donnie donnie 4096 Jan  6 01:56 snap
donnie@ubuntu2004:~$

很好。备份服务已启动,我也达到了预期效果。现在,是时候创建定时器了:

donnie@ubuntu2004:~$ systemctl edit --user --full --force backup.timer

我只需要让它每天午夜运行,所以我会这样设置:

[Unit]
Description=Back up my home directory
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timer.target default.target

当然,如果你不想等到午夜再检查是否有效,只需将OnCalendar=时间设置为你想要的任何时间。

请注意,当我们使用--user选项时,需要在WantedBy=行中有default.target

接下来,我将执行daemon-reload并启用定时器:

donnie@ubuntu2004:~$ systemctl daemon-reload --user
donnie@ubuntu2004:~$ systemctl enable --user --now backup.timer
Created symlink /home/donnie/.config/systemd/user/timer.target.wants/backup.timer → /home/donnie/.config/systemd/user/backup.timer.
Created symlink /home/donnie/.config/systemd/user/default.target.wants/backup.timer → /home/donnie/.config/systemd/user/backup.timer.
donnie@ubuntu2004:~$

我还可以使用--user选项查看有关该定时器的信息:

donnie@ubuntu2004:~$ systemctl list-timers --user
NEXT                        LEFT    LAST                        PASSED    UNIT         ACTIVATES     
Sun 2021-05-23 00:00:00 UTC 7h left Sat 2021-05-22 15:47:05 UTC 41min ago backup.timer backup.service
1 timers listed.
Pass --all to see loaded but inactive timers, too.
donnie@ubuntu2004:~$

目前来看,这个定时器只会在我登录到系统时运行。为了确保即使我未登录时它也能运行,我将为自己启用linger功能:

donnie@ubuntu2004:~$ loginctl enable-linger donnie
donnie@ubuntu2004:~$

好的,我想这一章就到此为止了。我们来总结一下。

总结

在这一章中,我们探讨了systemd定时器,并将其与传统的cron系统进行了比较。我们查看了不同的定时器选项以及指定定时器运行时间的不同方式。最后,我们还了解了如何为系统级和用户级任务创建定时器。

在下一章中,我们将简要了解systemd下的启动过程。我在那儿见。

问题

  1. cronsystemd定时器有什么不同?

    a. 设置systemd定时器要简单得多。

    b. 非特权用户可以设置自己的cron任务,但无法设置自己的定时器。

    c. cron任务可以直接运行命令或脚本,但systemd定时器只能运行关联的服务。

    d. 非特权用户可以设置自己的systemd定时器,但无法设置自己的cron任务。

  2. 什么时候作业会运行的两种指定方式是什么?(选择两个)

    a. 单调时间

    b. 实时钟

    c. 日历时间

    d. 实时

  3. 以下哪个 man 页面会告诉你如何格式化OnCalendar=参数的时间?

    a. systemd.time

    b. systemd.timer

    c. systemd.unit

    d. systemd.exec

  4. 以下哪种时间配置等同于每月设置?

    a. *-01-01 00:00:00

    b. *-*-* 00:00:00

    c. *-*-01 00:00:00

    d. *-01-01 00:00:00

答案

  1. c

  2. a, d

  3. a

  4. c

进一步阅读

)

)

第八章:理解 systemd 启动过程

在本章中,我们将简要了解systemd的启动过程。你可能觉得这有些枯燥,但我可以向你保证,它并不会。我的目标是给你提供一些实用信息,使得启动过程更高效,而不是让你经历一段枯燥无味的启动过程。之后,我还将展示一些systemd如何在一定程度上与传统的System V (SysV) 兼容。本章的具体内容包括以下几部分:

  • 比较 SysV 启动和systemd启动

  • 分析启动性能

  • Ubuntu Server 20.04 上的一些差异

  • 理解systemd生成器

请注意,本章不会讨论引导程序,因为我们会留到后面再讲。

好了——如果你准备好了,我们就开始吧。

技术要求

技术要求和以往一样——只需启动一个 Ubuntu 和一个 Alma 虚拟机 (VM),这样你就可以跟着操作。

请查看以下链接,观看《代码实战》视频:bit.ly/3phdZ6o

比较 SysV 启动和 systemd 启动

不管运行哪个操作系统,计算机的启动过程基本上都是一样的。你打开电源开关,然后进入机器的systemd启动序列。

理解 SysV 和 systemd 启动过程的相似性

一旦机器能够访问到机器硬盘的 MBR,操作系统就开始加载。在/boot/目录中,你会看到一个压缩的 Linux 内核文件,文件名通常包含vmlinuz。你还会看到文件名中有initramfsinitrd。该过程的第一步是将 Linux 内核镜像解压并加载到系统内存中。在此阶段,内核仍然无法访问根文件系统,因为它无法访问所需的驱动程序。这些驱动程序位于初始 RAM 磁盘镜像中。所以,下一步是加载这个初始 RAM 磁盘镜像,它将建立一个内核可以访问的临时根文件系统。一旦内核加载了适当的驱动程序,镜像将被卸载。启动过程接着继续,访问机器根文件系统中的所需内容。

接下来,情况就不同了。为了展示如何不同,让我们快速浏览一下 SysV 启动过程。

理解 SysV 启动过程

我不会深入探讨 SysV 启动过程的细节,因为没有必要。我只想给你提供足够的信息,让你能理解它与systemd启动的不同之处。

init 进程(始终为 PID 1)是第一个启动的进程。这个 init 进程将通过一系列复杂的、盘根错节的 bash 脚本来控制其余的启动过程,这些脚本位于 /etc/ 目录下。在某个时刻,init 进程将从 /etc/inittab 文件中获取关于默认运行级别的信息。一旦基本系统初始化完成,系统服务将根据默认运行级别启用的内容,从 /etc/init.d/ 目录下的 bash 脚本启动。

在 SysV 机器上,启动过程可能相当慢,因为所有服务都是串行启动的——换句话说,SysV 在启动时一次只能启动一个服务。当然,我可能把 SysV 描述得比实际情况更糟糕了。尽管按今天的标准它已经过时,但在其时代,它确实能很好地与当时的硬件配合工作。我的意思是,当你谈论的是一台配备单核 750 兆赫 (MHz) Pentium III 处理器和 512 兆字节 (MB) 内存的服务器时,反正没有太多办法能加快它的速度。(我现在还收藏着几台这样的旧机器,但已经很久没启动过它们了。)

正如我所说的,这只是一次快速的概览。就我们当前的目的来说,这就是你需要了解的关于 SysV 启动的所有内容。接下来,让我们跳过这个话题,看看 systemd 启动过程是如何工作的。

理解 systemd 启动过程

systemd 中,systemd 进程是第一个启动的进程。它也以 PID 1 运行,就像我们在 Alma 机器上看到的那样:

[donnie@localhost ~]$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  1.9  0.8 186956 15088 ?        Ss   14:18   0:07 /usr/lib/systemd/systemd --switched-root --system --deserialize 17
. . .
. . .

奇怪的是,在 Ubuntu 机器上,PID 1 仍然显示为 init 进程,就像我们在这里看到的那样:

donnie@ubuntu20-04:~$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  1.2  0.5 101924 11308 ?        Ss   18:26   0:04 /sbin/init maybe-ubiquity
. . .
. . .

这是因为 Ubuntu 开发者出于某种奇怪的原因,创建了一个指向 systemd 可执行文件的 init 符号链接,正如我们在这里看到的:

donnie@ubuntu20-04:~$ cd /sbin
donnie@ubuntu20-04:/sbin$ ls -l init
lrwxrwxrwx 1 root root 20 Mar 17 21:36 init -> /lib/systemd/systemd
donnie@ubuntu20-04:/sbin$

我不知道为什么 Ubuntu 开发者认为有必要这样做。不过它确实能工作,所以没问题。

与运行复杂的 bash 脚本初始化系统不同,systemd 运行目标(targets)。它首先查看 default.target 文件,检查它是设置为 graphical 还是 multi-user。正如我在 第六章 中提到的,理解 systemd 目标,依赖关系链从默认目标开始并向后延伸。假设我们的机器将图形目标设为默认。在 graphical.target 文件中,我们会看到以下一行:

Requires=multi-user.target

这意味着图形目标必须等到多用户目标启动后才能启动。在 multi-user.target 文件中,我们看到这一行:

Requires=basic.target

现在,如果我们继续追溯这个链条的起源,我们会发现基本目标 Requires sysinit.target 文件,而该文件又 Wants local-fs.target 文件,后者会在 local-fs-pre.target 文件之后启动。

那么,这一切到底意味着什么呢?实际上,一旦systemd进程确定了默认目标,它会按照以下顺序加载启动目标:

  1. local-fs-pre.target

  2. local-fs.target

  3. sysinit.target

  4. basic.target

  5. multi-user.target

  6. graphical.target(如果启用了的话)

好的—我知道了。你现在可能在喊:但是 Donnie,你说过 systemd 是并行启动进程,而不是按顺序启动的。 的确,systemd会并行启动它的启动进程。记住我之前告诉过你的,目标是一个集合,由其他systemd单元组成,目的是为了特定的功能。在每个目标内,进程是并行启动的。

注意

你可以在bootup手册页上看到这个启动链的图形化表示。

我之前也提到过,其中一些目标是硬编码到systemd可执行文件中的。这意味着其中一些目标没有自己的.target文件,而其他的则有.target文件,似乎没有任何作用。有几种方法可以查看这些硬编码目标的情况。第一种方法是使用systemctl list-dependencies查看目标。以下是查看local-fs.target文件时的输出:

[donnie@localhost ~]$ systemctl list-dependencies local-fs.target
local-fs.target
  ├─-.mount
  ├─boot.mount
  ├─ostree-remount.service
  └─systemd-remount-fs.service
[donnie@localhost ~]$

这个目标启动了挂载文件系统的服务。我们看到它挂载了boot分区,表示为boot.mount。然后它挂载了root文件系统,表示为-.mount

我之前向你展示过如何查看硬编码到systemd可执行文件中的目标列表。我们也可以查找仅针对某个目标的特定信息。以下是查看local-fs.target文件的方式:

[donnie@localhost systemd]$ strings /lib/systemd/systemd | grep -A 100 'local-fs.target'
local-fs.target
options
fstype
Failed to parse /proc/self/mountinfo: %m
Failed to get next entry from /proc/self/mountinfo: %m
. . .
. . .
mount_process_proc_self_mountinfo
mount_dispatch_io
mount_enumerate
mount_enumerate
mount_shutdown
[donnie@localhost systemd]$

默认情况下,grep只会显示它找到搜索词的行。-A选项使它显示搜索词所在行后指定数量的行。我这里使用的-A 100选项告诉grep显示local-fs.target所在行后面的 100 行。虽然我们看不到具体的程序代码,但嵌入的文本字符串能让我们大致了解发生了什么。我的 100 行选择完全是随意的,但你可以根据需要增加行数,直到看到与挂载文件系统无关的行。

获取这些硬编码目标的第三种方式是查看bootupsystemd.special的手册页。虽然这两个手册页没有提供太多细节,但你仍然可以从中学到一些东西。

现在,问题解决后,我们来看一下如何分析启动问题。

分析启动性能

假设你的服务器启动时间比预期的要长,你想知道为什么。幸运的是,systemd附带了内置的systemd-analyze工具,可以帮助你。

让我们从这里开始,看看我的 AlmaLinux 机器启动 GNOME 3 桌面的时间:

[donnie@localhost ~]$ systemd-analyze
Startup finished in 2.397s (kernel) + 19.023s (initrd) + 1min 26.269s (userspace) = 1min 47.690s
graphical.target reached after 1min 25.920s in userspace
[donnie@localhost ~]$

如果你没有指定选项,systemd-analyze将默认使用time选项。(你如果真的想,也可以输入systemd-analyze time,但你会看到与这里相同的结果。)输出的第一行显示了内核、初始 RAM 磁盘镜像和用户空间加载所需的时间。第二行显示了图形目标启动所需的时间。实际上,整体启动时间看起来并不算太差,特别是考虑到我用来运行这个虚拟机的主机的年代。(这台主机大约是 2009 年左右的戴尔,搭载的是一颗老式的 Core 2 Quad 中央处理器CPU)。)如果我在一台较新的主机上运行这个虚拟机,或者直接在裸机上运行 Alma,启动时间可能会稍微快一点。还有一个因素是,这个虚拟机运行的是资源消耗较大的 GNOME 3 桌面环境。我个人更喜欢轻量级的桌面环境,可能会稍微缩短启动时间。不幸的是,Red Hat Enterprise Linux 8RHEL 8)及其所有免费的子版本仅提供 GNOME 3 桌面环境。(如果你安装了第三方的Extra Packages for Enterprise LinuxEPEL)库,确实可以安装轻量级的XForms Common EnvironmentXFCE)桌面,但这超出了本书的范围。)

现在,假设这台机器的启动过程确实太慢了,如果可能的话你想加速它。首先,我们使用blame选项来查看我们要责怪谁:

[donnie@localhost ~]$ systemd-analyze blame
     1min 4.543s plymouth-quit-wait.service
         58.883s kdump.service
         32.153s wordpress-container.service
         32.102s wordpress2-container.service
         18.200s systemd-udev-settle.service
         14.690s dracut-initqueue.service
         13.748s sssd.service
         12.638s lvm2-monitor.service
         10.781s NetworkManager-wait-online.service
         10.156s tuned.service
          9.504s firewalld.service
. . .
. . .

这个blame选项会显示在启动过程中所有已启动的服务,以及每个服务启动所需的时间。服务按启动所需时间的降序排列。查看整个列表,看看是否有你可以安全禁用的服务。例如,在列表的下方,你会看到wpa_supplicant.service正在运行,正如我在这里所示:

[donnie@localhost ~]$ systemd-analyze blame | grep 'wpa_supplicant'
           710ms wpa_supplicant.service
[donnie@localhost ~]$

如果你使用的是桌面计算机或笔记本电脑,并且可能需要使用无线适配器,这样做是很好的,但如果是在没有无线功能的服务器上,则没有必要。因此,你可以考虑禁用这个服务。(当然,这个服务启动只用了 710 毫秒ms),但这仍然是个时间。)

注意

禁用不必要的服务对性能和安全性都有好处。一个一直存在的安全基本原则是,你应该始终最小化系统上运行的服务数量。这可以减少潜在攻击者的攻击路径。

如果你想查看每个目标在启动过程中花了多长时间,可以使用critical-chain选项,如下所示:

[donnie@localhost ~]$ systemd-analyze critical-chain
The time after the unit is active or started is printed after the "@" character.
The time the unit takes to start is printed after the "+" character.
graphical.target @2min 1.450s
. . .
. . .
                    └─local-fs-pre.target @26.988s
                      └─lvm2-monitor.service @4.022s +12.638s
                                              └─dm-event.socket @3.973s
                                                └─-.mount
                                                  └─system.slice
                                                    └─-.slice
[donnie@localhost ~]$

由于格式原因,我只能向你展示输出的一个小部分,所以请自己尝试一下,看看完整的输出内容。

这些命令在 Ubuntu 机器上与在 Alma 机器上执行时相同,但在 Ubuntu Server 20.04 上默认目标的设置有所不同。让我们来看一下。

Ubuntu Server 20.04 的一些差异

我的 Ubuntu Server 20.04 机器,完全以文本模式运行,启动速度明显更快,正如你在这里看到的:

donnie@ubuntu20-04:~$ systemd-analyze
Startup finished in 8.588s (kernel) + 44.944s (userspace) = 53.532s
graphical.target reached after 38.913s in userspace
donnie@ubuntu20-04:~$

我必须承认,自从 Ubuntu Server 20.04 发布以来,我并没有使用太多,我仍然会遇到一些让我惊讶的新特性。在为本章配置虚拟机之前,我从未注意到 Ubuntu Server 20.04 默认使用 graphical.target,尽管没有安装图形界面。解释是,accounts-daemon.service 文件是由图形目标启动的,而不是由多用户目标启动的,正如我们在这里看到的:

donnie@ubuntu20-04:/etc/systemd/system/graphical.target.wants$ ls -l
total 0
lrwxrwxrwx 1 root 43 Feb  1 17:27 accounts-daemon.service -> /lib/systemd/system/accounts-daemon.service
donnie@ubuntu20-04:/etc/systemd/system/graphical.target.wants$

如果你查看 graphical.target 文件,你会看到它只想要 display-manager.service 文件,而不是要求它,正如这行所示:

Wants=display-manager.service

所以,即使该虚拟机上没有显示管理器,它仍然能够顺利进入 graphical.target。但让我们回到那个 accounts-daemon.service 文件。它到底是什么呢?根据 www.freedesktop.org/wiki/Software/AccountsService/ 上的官方文档,"AccountsService 是一个 D-Bus 服务,用于访问用户账户及与这些账户相关的信息。" 是的,我知道——这并不是一个很好的解释。更好的解释是,它是一个服务,允许你通过 图形用户界面GUI)类型的工具来管理用户和用户账户。那么,为什么在没有图形界面的情况下,我们会在 Ubuntu Server 上启用它呢?这是个好问题,我没有一个很好的答案。这不是我们需要在文本模式服务器上运行的东西。不过,没关系,我们马上就会处理这个问题。

那么,D-Bus 到底是什么?

accounts-daemon.service 文件是一个服务,旨在通过 D-Bus 消息启动。我们可以在 accounts-daemon.service 文件的 [Service] 部分的 Type=dbus 行中看到这一点:

[Service]
Type=dbus
BusName=org.freedesktop.Accounts
ExecStart=/usr/lib/accountsservice/accounts-daemon
Environment=GVFS_DISABLE_FUSE=1
Environment=GIO_USE_VFS=local
Environment=GVFS_REMOTE_VOLUME_MONITOR_IGNORE=1

然而,我们在 [Install] 部分看到,出于性能原因,我们仍然会在启动过程中启动这个服务:

[Install]
# We pull this in by graphical.target instead of waiting for the bus
# activation, to speed things up a little: gdm uses this anyway so it is nice
# if it is already around when gdm wants to use it and doesn't have to wait for
# it.
WantedBy=graphical.target

(这里提到的 gdm 代表 GNOME 显示管理器,它负责具有 GNOME 3 桌面系统的用户登录操作。)

如我之前所说,我们不需要在文本模式服务器上运行这个 accounts-daemon.service 文件。所以,让我们将这个 Ubuntu 机器的 default.target 文件设置为 multi-user,这将阻止 accounts-daemon.service 文件在我们启动机器时自动启动。你可能记得,这是执行该操作的命令:

donnie@ubuntu20-04:~$ sudo systemctl set-default multi-user

现在,当你重新启动机器时,你应该会发现它启动得更快。万一accounts-daemon.service被需要,D-Bus 消息会启动它。

出于好奇,我创建了一个没有 GNOME 桌面的全新 AlmaLinux 虚拟机,看看它是否也会默认使用graphical.target。结果发现没有 GNOME 的 Alma 默认使用multi-user.target,并且甚至没有安装AccountsService包。(因此,没有 GUI 类型的用户管理工具,accounts-daemon.service文件甚至不需要。)

接下来,让我们用systemd生成器激发一些真正的兴奋。

理解 systemd 生成器

systemd生成器可以让忙碌的管理员的工作变得更轻松,还能提供与传统 SysV 的向后兼容性。我们首先来看看生成器如何简化磁盘和分区配置。

理解挂载单元

查看任一虚拟机的/lib/systemd/system/目录,你会看到在安装操作系统时创建的几个挂载单元文件,下面是这台 Alma 机器的示例:

[donnie@localhost system]$ ls -l *.mount
-rw-r--r--. 1 root 750 Jun 22  2018 dev-hugepages.mount
-rw-r--r--. 1 root 665 Jun 22  2018 dev-mqueue.mount
-rw-r--r--. 1 root 655 Jun 22  2018 proc-sys-fs-binfmt_misc.mount
-rw-r--r--. 1 root root 795 Jun 22  2018 sys-fs-fuse-connections.mount
-rw-r--r--. 1 root root 767 Jun 22  2018 sys-kernel-config.mount
-rw-r--r--. 1 root root 710 Jun 22  2018 sys-kernel-debug.mount
-rw-r--r--. 1 root root 782 May 20 08:24 tmp.mount
[donnie@localhost system]$

除了tmp.mount文件外,所有这些挂载单元都是内核功能,与我们想要挂载的磁盘和分区无关。与 Ubuntu 不同,Alma 将/tmp/目录挂载在自己的分区上,这也是为什么在 Ubuntu 机器上看不到tmp.mount文件的原因。让我们瞥一眼tmp.mount文件,看看里面有什么。这里是[Unit]部分:

[Unit]
Description=Temporary Directory (/tmp)
Documentation=man:hier(7)
Documentation=https://www.freedesktop.org/wiki/Software/systemd/APIFileSystems
ConditionPathIsSymbolicLink=!/tmp
DefaultDependencies=no
Conflicts=umount.target
Before=local-fs.target umount.target
After=swap.target

ConditionPathIsSymbolicLink=!/tmp行防止系统挂载/tmp/,如果/tmp被发现是一个符号链接而不是实际的mount点目录。(记住,!符号表示否定操作。)接下来我们看到该挂载单元与umount.target文件存在Conflicts,这意味着umount操作将卸载/tmp/

接下来,我们来看看[Mount]部分的内容:

[Mount]
What=tmpfs
Where=/tmp
Type=tmpfs
Options=mode=1777,strictatime,nosuid,nodev

What=Type=行表示这是一个临时文件系统Where=行定义了挂载点目录。最后是Options=行,其中包含以下选项:

  • mode=1777:这个选项设置了挂载点目录的权限值。777部分为所有人设置了完全的读、写和执行权限。1部分设置了粘滞位,它防止用户删除彼此的文件。

  • strictatime:这个选项使得内核在此分区上的所有文件上保持完整的访问时间(atime)更新。

  • nosuid:如果此分区上的任何文件设置了设置用户 IDSUID)位,此选项会阻止 SUID 执行任何操作。(SUID 位是一种提升非特权用户权限的方式,如果它被设置在不应设置的文件上,可能会成为安全问题。)

  • nodev:这个安全功能防止系统识别此分区上的任何字符设备或块设备文件。(你应该只在/dev/目录下看到设备文件。)

最后,我们有[Install]部分,内容如下:

[Install]
WantedBy=local-fs.target

所以,这个分区是在启动过程一开始通过 local-fs.target 文件挂载的。

好的——现在你已经基本了解了挂载单元文件的样子。你现在可能在想:我们的普通磁盘分区的挂载单元文件在哪儿? 啊,我很高兴你问了。

虽然可以手动为你的普通磁盘分区创建挂载单元文件,但这并非必要。事实上,systemd.mount 的手册页不推荐这样做。在该手册页的 FSTAB 部分,你会看到,配置 /etc/fstab 文件中的分区是完全可能的,而且是推荐的,和你以往的做法一样。systemd 生成器会根据 fstab 文件中的信息动态生成适当的挂载单元文件。例如,这是 Alma 机器的 fstab 文件:

/dev/mapper/almalinux-root /      xfs     defaults        0 0
UUID=42b88c40-693d-4a4b-ac60-ae042c742562 /boot  xfs     defaults        0 0
/dev/mapper/almalinux-swap none   swap    defaults        0 0

这两行 /dev/mapper 表示根文件系统分区和交换分区以逻辑卷的形式挂载。我们还看到根分区格式化为 xfs 分区。UUID= 行表示 /boot/ 分区以普通分区的形式挂载,并由其全局唯一标识符UUID)编号标识。(这很有道理,因为 Linux 系统无法从逻辑卷启动。)

好的——SysV 系统会直接从 fstab 文件获取信息并加以使用。正如我之前提到的,systemd 会使用这些信息并动态生成挂载单元文件,这些文件位于 /run/systemd/generator/ 目录下,正如我们在这里看到的:

[donnie@localhost ~]$ cd /run/systemd/generator/
[donnie@localhost generator]$ ls -l
total 12
-rw-r--r--. 1 root root 254 Jun 15 14:16  boot.mount
-rw-r--r--. 1 root root 235 Jun 15 14:16 'dev-mapper-almalinux\x2dswap.swap'
drwxr-xr-x. 2 root root  80 Jun 15 14:16  local-fs.target.requires
-rw-r--r--. 1 root root 222 Jun 15 14:16  -.mount
drwxr-xr-x. 2 root root  60 Jun 15 14:16  swap.target.requires
[donnie@localhost generator]$

很明显,这些文件中哪些是对应 /boot/swap 分区的。不是那么明显的是,-.mount 文件对应的是根文件系统分区。让我们来看一下 boot.mount 文件,看看里面有什么:

# Automatically generated by systemd-fstab-generator
[Unit]
SourcePath=/etc/fstab
Documentation=man:fstab(5) man:systemd-fstab-generator(8)
Before=local-fs.target
[Mount]
Where=/boot
What=/dev/disk/by-uuid/42b88c40-693d-4a4b-ac60-ae042c742562
Type=xfs

从你在之前的示例和 fstab 文件中看到的内容,你应该能搞明白这里发生了什么。

你可能想看看 -.mount 文件里面的内容,但你不能像平常那样去查看。如果你尝试的话,会得到如下结果:

[donnie@localhost generator]$ cat -.mount
cat: invalid option -- '.'
Try 'cat --help' for more information.
[donnie@localhost generator]$

无论你尝试哪种命令行工具,这都会发生。这是因为文件名前缀中的 符号使得 Bash shell 认为我们正在处理一个选项开关。要使其正常工作,只需在文件名前加上 ./,这样你就会使用绝对路径。命令看起来会是这样的:

[donnie@localhost generator]$ cat ./-.mount
# Automatically generated by systemd-fstab-generator
[Unit]
SourcePath=/etc/fstab
Documentation=man:fstab(5) man:systemd-fstab-generator(8)
Before=local-fs.target
[Mount]
Where=/
What=/dev/mapper/almalinux-root
Type=xfs
[donnie@localhost generator]$

好的——我想这已经涵盖了挂载单元的内容。现在让我们切换到 Ubuntu Server 20.04 机器,查看 systemd 的一个向后兼容性功能。

理解向后兼容性

你还可以使用 systemd 生成器来控制来自老式 SysV init 脚本的服务。你在 Red Hat 类型的系统上可能不会看到这种情况,但在 Debian 和 Ubuntu 系统上会看到。(出于某些奇怪的原因,Debian 和 Ubuntu 的维护者仍然没有将所有服务转换为原生的 systemd 服务。)为了演示,可以通过以下方式禁用并停止 Ubuntu 机器上的正常 ssh 服务:

donnie@ubuntu20-04:~$ sudo systemctl disable --now ssh

接下来,安装 Dropbear,它是一个轻量级的替代 OpenSSH 的包。可以通过以下两条命令来安装:

sudo apt update
sudo apt install dropbear

安装完成后,你应该能看到 Dropbear 服务已经启用并正在运行:

donnie@ubuntu20-04:~$ systemctl status dropbear
  dropbear.service - LSB: Lightweight SSH server
     Loaded: loaded (/etc/init.d/dropbear; generated)
     Active: active (running) since Tue 2021-06-15 16:15:40 UTC; 3h 40min ago
. . .
. . .

到目前为止,一切看起来都正常,除了它如何通过 /etc/init.d/dropbearinit 脚本加载服务这一部分。如果你在 /lib/systemd/system/ 目录下查找 dropbear.service 文件,你是找不到的。相反,你会在 /etc/init.d/ 目录下看到 dropbearinit 脚本:

donnie@ubuntu20-04:~$ cd /etc/init.d
donnie@ubuntu20-04:/etc/init.d$ ls -l dropbear
-rwxr-xr-x 1 root root 2588 Jul 27  2019 dropbear
donnie@ubuntu20-04:/etc/init.d$

当 Dropbear 服务启动时,systemd 会在 /run/systemd/generator.late/ 目录下生成一个 dropbear.service 文件,如下所示:

donnie@ubuntu20-04:/run/systemd/generator.late$ ls -l dropbear.service
-rw-r--r-- 1 root root 513 Jun 15 16:16 dropbear.service
donnie@ubuntu20-04:/run/systemd/generator.late$

这个文件并不是永久保存到磁盘的,它仅在系统运行时有效。查看文件,你会看到它只是一个普通的服务单元文件:

图 8.1 – 生成的 Dropbear 服务文件

好吧——也许这并不是 完全 正常。(我不明白为什么它会把 Before=multi-user.target 这一行列出三次。)此外,它缺少 [Install] 部分,因为这个文件实际上是用于静态服务的。

如果你真的想要,你可以通过执行一个普通的 sudo systemctl edit --full dropbear 命令来让系统创建一个正常的 dropbear.service 文件,放在 /etc/systemd/system/ 目录下。删除 [Unit] 部分中的 SourcePath=/etc/init.d/dropbear 行,因为你不再需要它。接着,将以下行插入到 [Service] 部分:

EnvironmentFile=-/etc/default/dropbear

这将允许你在 /etc/default/dropbear 文件中设置某些 Dropbear 参数,该文件已经存在。(查看 Dropbear 手册页以了解可以设置哪些选项。)

然后,添加 [Install] 部分,看起来应该像这样:

[Install]
WantedBy=multi-user.target

保存文件并执行 sudo systemctl daemon-reload 命令。然后,启用 Dropbear 并重新启动虚拟机以验证它是否正常工作。最后,查看 /run/systemd/generator.late/ 目录。你会发现 dropbear.service 文件不再存在,因为 systemd 不再使用 dropbearinit 脚本。相反,它正在使用你刚刚在 /etc/systemd/system/ 目录下创建的 dropbear.service 文件。如果需要,你现在可以像编辑其他服务文件一样编辑这个服务文件。

总结

是的,女士们,先生们,我们再次涵盖了很多内容并看到了很酷的东西。我们从 SysV 和systemd的启动过程概述开始,然后了解了一些分析启动性能的方法。接着,我们探讨了 Ubuntu Server 启动配置的一个特殊情况。最后,我们总结了systemd生成器的两个用途。

在下一章,我们将使用一些systemd工具来设置某些系统参数。到时候见。

问题

  1. systemd如何处理仍然使用传统init脚本的服务?

    a. 它直接使用init脚本。

    b. 它在/etc/systemd/system/目录下创建并保存一个服务单元文件。

    c. 它会在/run/systemd/generator.late/目录中动态生成一个服务单元文件。

    d. 它不会运行只有init脚本的服务。

  2. systemd系统上,推荐的磁盘分区配置方式是什么?

    a. 为每个分区手动创建挂载单元文件。

    b. 按照通常的方式编辑/etc/fstab文件。

    c. 手动在/dev/目录下创建分区设备文件。

    d. 使用mount工具。

  3. 以下哪个文件代表根文件系统?

    a. root.mount

    b. -.mount

    c. /.mount

    d. rootfs.mount

  4. 以下哪个命令可以显示每个服务在启动过程中需要的时间?

    a. systemctl blame

    b. systemctl time

    c. systemd-analyze

    d. systemd-analyze time

    e. systemd-analyze blame

答案

  1. c

  2. b

  3. b

  4. e

深入阅读

D-Bus 文档:

www.freedesktop.org/wiki/Software/dbus/

AccountsService文档:

www.freedesktop.org/wiki/Software/AccountsService/

清理 Linux 启动过程:

www.linux.com/topic/desktop/cleaning-your-linux-startup-process/

第九章:设置系统参数

在本章中,我们将介绍如何使用systemd实用程序设置某些以前必须通过编辑配置文件或创建符号链接来设置的参数。我们还将查看在使用这些实用程序时涉及的服务。

在本章中,我们将涵盖以下主题:

  • 设置地区设置参数

  • 设置时间和时区参数

  • 设置主机名和机器信息

如果您准备好了,让我们开始吧!

技术要求

技术要求与前几章相同。因此,启动您的 Ubuntu 服务器和 AlmaLinux 虚拟机,并跟随进行操作。

查看以下链接以查看代码演示视频:bit.ly/3xKA7K0

设置地区设置参数

计算机被全世界的人们使用,他们来自许多不同的文化和语言背景。(我知道你已经知道这一点,但我还是要告诉你。)幸运的是,所有主要的操作系统都有方法来适应几乎地球上所有语言的用户。在 Unix 和 Linux 系统上,locale参数集帮助我们处理这个问题。让我们开始仔细看一看。

理解地区设置

locale是一组定义了对用户可能重要的许多内容的参数。这些参数包括用户的首选语言、字符编码、货币格式以及其他几个内容。

通常在安装操作系统时设置locale,之后就不必再操作它了。Linux 安装程序没有一个专门的屏幕显示“选择您的 locale”,但它们有一个供您选择键盘布局和另一个供您选择时区的屏幕。在我的情况下,我会选择美国英语键盘布局和美国东部时区。通过这些信息,安装程序可以确定我想要使用一个设置为美国的 locale。

在 Debian/Ubuntu 和 Red Hat 系统上实现locale时存在一些差异。一个差异在于locale的定义位置。在 Red Hat 类型的系统上,例如我的 Alma 虚拟机,以及我用来撰写本文的 openSUSE 主机,locale设置在/etc/locale.conf文件中,正如我们在这里看到的:

[donnie@localhost ~]$ cd /etc
[donnie@localhost etc]$ ls -l locale.conf
-rw-r--r--. 1 root root 19 May  6 19:06 locale.conf
[donnie@localhost etc]$

locale.conf文件中,我们只看到这一行:

LANG="en_US.UTF-8"

因此,localeLANG=参数设置,由两部分组成。第一部分(en_US)定义了我想使用的语言和地区,而第二部分(UTF-8)定义了我想使用的字符集。

好的,语言和地区部分是显而易见的。那么,字符集是什么呢?它其实就是操作系统能够显示的字符集合。字符集包括字母数字字符、标点符号和其他各种特殊字符。就像计算机上的其他一切一样,字符——也叫做码点——由一串 0 和 1 组成。字符集定义了这些 0 和 1 的组合,从而构成每个字符。早期的字符集,比如旧的 EBCDIC 和 ASCII 集,在它们能够显示的字符数量上是有限的。更糟糕的是,EBCDIC 集的设计有缺陷,导致程序员难以使用。UTF-8 就是为了弥补这些不足而设计的。

在 Ubuntu Server 中,locale设置保存在/etc/default/locale文件中。如果Ubuntu 安装程序工作正常——稍后我会详细说明——那么该文件应该看起来像这样:

donnie@ubuntu2:~$ cat /etc/default/locale
LANG=en_US.UTF-8
donnie@ubuntu2:~$

唯一的区别是,在 Ubuntu 的文件中,locale的规范没有像 Alma 机器中那样被双引号括起来。

接下来,让我们看看locale设置中包含了什么。我们可以使用locale工具来查看,方法如下:

[donnie@localhost ~]$ locale
LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC=en_US.UTF-8
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
. . .
. . .
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=
[donnie@localhost ~]$

要了解这些设置的具体内容,你需要查阅locale的手册页面。唯一的难点是,实际上有多个locale手册页面,正如你在这里看到的:

[donnie@localhost ~]$ whatis locale
locale (7)           - description of multilanguage support
locale (1)           - get locale-specific information
locale (5)           - describes a locale definition file
locale (1p)          - get locale-specific information
locale (3pm)         - Perl pragma to use or avoid POSIX locales for built-in operations
[donnie@localhost ~]

在这种情况下,我们需要的是数字5的手册页面。可以通过以下方式打开:

[donnie@localhost ~]$ man 5 locale

仅仅因为我能读懂你的想法,我已经知道你接下来的问题是什么了。这些 locale 设置究竟影响什么呢? 啊,很高兴你问了。(希望我能读懂你心思的事不会让你觉得太诡异。)

各种locale设置会影响awkgrepsort等工具如何显示它们的输出。在桌面计算机上,它们可能会被显示管理器用于登录目的。最后,正如我将要演示的,它们还被某些 Shell 编程函数所使用。我将在 Alma 机器上演示,因为它已经有多个不同的 locale 可供使用。Bash shell 的printf函数为我们提供了一个完美的演示。(注意,你可能需要根据你自己的 locale 对这个演示做一些修改。)

在 Alma 机器的命令行上,我们来看看默认的en_US.UTF-8 locale 是如何与printf一起工作的,试着打印一个十进制数字:

[donnie@localhost ~]$ printf "%.2f\n" 3.14
3.14
[donnie@localhost ~]$

之所以有效,是因为在美国,句点(.)是小数点符号。而在大多数欧洲国家,逗号(,)则是小数点符号。我们可以仅更改一个单独的locale设置,所以我们暂时将LC_NUMERIC设置改为欧洲格式,看看会发生什么:

[donnie@localhost ~]$ export LC_NUMERIC="en_DK.utf8"
[donnie@localhost ~]$ printf "%.2f\n" 3.14
-bash: printf: 3.14: invalid number
0,00
[donnie@localhost ~]$

这次,printf给出了一个无效数字的错误。这是因为它现在期待看到欧洲的数字格式。如果我们再次尝试使用逗号,它应该能正常工作。我们来看看结果:

[donnie@localhost ~]$ printf "%.2f\n" 3,14
3,14
[donnie@localhost ~]$

是的,它像冠军一样工作。别担心那个LC_NUMERIC设置——它会在你退出终端窗口后消失。

到目前为止,我们只使用了传统的locale工具,它已经存在很久了。为了将这个话题与systemd相关联,让我们使用localectl来查看默认的locale设置:

[donnie@localhost ~]$ localectl
   System Locale: LANG=en_US.UTF-8
       VC Keymap: us
      X11 Layout: us
[donnie@localhost ~]$

它是有效的,但没有提供像传统的locale工具那样多的信息。

接下来,让我们学习如何更改默认的区域设置。

更改 Alma 机器上的默认区域设置

我们将继续使用 Alma 机器进行操作,因为它已经安装了多个区域设置。

在你更改默认区域设置之前,你需要检查你想要的区域设置是否已安装在系统中。你可以通过locale -a命令或者localectl list-locales命令来查看。无论哪种方式,你都会得到相同的输出,内容大致如下:

[donnie@localhost ~]$ localectl list-locales
C.utf8
en_AG
en_AU
en_AU.utf8
. . .
. . .
en_ZA.utf8
en_ZM
en_ZW
en_ZW.utf8
[donnie@localhost ~]$

有两种方法可以更改locale设置。你可以直接打开/etc/locale.conf文件并修改设置,但那样太无趣了,不是吗?相反,让我们用便捷的localectl工具来完成这项工作,像这样:

[donnie@localhost ~]$ sudo localectl set-locale en_CA.utf8
[sudo] password for donnie:
[donnie@localhost ~]$ cat /etc/locale.conf
LANG=en_CA.utf8
[donnie@localhost ~]$

所以,我已经将机器设置为加拿大英语。

奇怪的是,之前存在的双引号现在消失了。所以,我猜它们其实并不需要。另外,请注意,直到我们退出机器并重新登录后,这个设置才会生效。一旦重新登录,我们会看到区域设置已改变,但键盘映射设置并没有变化:

[donnie@localhost ~]$ localectl
   System Locale: LANG=en_CA.utf8
       VC Keymap: us
      X11 Layout: us
[donnie@localhost ~]$

localectl list-keymaps命令可以显示可用的键盘映射设置。假设我想将键盘映射更改为加拿大的,以匹配我的区域设置。我会像这样操作:

[donnie@localhost ~]$ sudo localectl set-keymap ca
[donnie@localhost ~]$ sudo localectl set-x11-keymap ca
[donnie@localhost ~]$

退出并重新登录后,localectl的状态将如下所示:

[donnie@localhost ~]$ localectl
   System Locale: LANG=en_CA.utf8
       VC Keymap: ca
      X11 Layout: ca
[donnie@localhost ~]$

每当你使用localectl更改任何设置时,执行操作的是systemd-localed.service。由于格式问题,我无法展示完整的systemd-localed.service文件,所以我只会展示相关部分:

[Unit]
Description=Locale Service
Documentation=man:systemd-localed.service(8) man:locale.conf(5) man:vconsole.conf(5)
Documentation=https://www.freedesktop.org/wiki/Software/systemd/localed
[Service]
ExecStart=/usr/lib/systemd/systemd-localed
BusName=org.freedesktop.locale1
. . .
. . .
SystemCallArchitectures=native
LockPersonality=yes
ReadWritePaths=/etc

我希望你注意到这里的两点。首先,在[Service]部分,注意到BusName=org.freedesktop.locale1这一行。没有Type=dbus这一行,但没关系。只要有BusName=这一行,它就自动成为一种dbus类型的服务。另外,请注意没有[Install]部分,这使得它成为一个静态类型的服务,我们无法启用它。相反,每当你使用localectl更改设置时,localectl会通过发送dbus消息来启动该服务。

一旦你看到你想看的内容,就可以随时返回到正常设置。

接下来,让我们看看在 Ubuntu 上,这个过程是如何不同的。

更改 Ubuntu 上的默认区域设置

Ubuntu 提供了多种locale定义,但你必须先构建它们才能使用。在/etc/locale.gen文件中,你会看到一个可以构建的区域设置列表。以下是文件顶部的内容:

donnie@ubuntu20-04:/etc$ cat locale.gen
# This file lists locales that you wish to have built. You can find a list
# of valid supported locales at /usr/share/i18n/SUPPORTED, and you can add
# user defined locales to /usr/local/share/i18n/SUPPORTED. If you change
# this file, you need to rerun locale-gen.
# aa_DJ ISO-8859-1
# aa_DJ.UTF-8 UTF-8
# aa_ER UTF-8
# aa_ER@saaho UTF-8
# aa_ET UTF-8
# af_ZA ISO-8859-1
. . .
. . .

如果Ubuntu 安装程序正常工作,你应该看到文件中的所有locale列表都被注释掉了,只有一个没有被注释,它就是在你的系统上构建的那个。在我之前展示的 Ubuntu 机器上,en_US.UTF-8是唯一一个没有被注释掉的,如我们在这里看到的:

. . .
. . .
# en_US ISO-8859-1
# en_US.ISO-8859-15 ISO-8859-15
en_US.UTF-8 UTF-8
# en_ZA ISO-8859-1
# en_ZA.UTF-8 UTF-8
# en_ZM UTF-8
. . .
. . .

所以,只有这个语言环境被构建出来。

现在,记住,我说过这将是如果Ubuntu 安装程序正常工作的情况。这台虚拟机是我为本章设置的第二台 Ubuntu 机器,这次安装程序确实正常工作。当我设置第一台 Ubuntu 虚拟机时,安装程序没有正常工作,且没有构建任何语言环境。当我查看/etc/default/locale文件时,我看到的是:

donnie@ubuntu20-04:~$ cat /etc/default/locale
LANG=C.UTF-8
donnie@ubuntu20-04:~$

在这个/etc/locale.gen文件中,所有的语言环境列表都被注释掉了,这告诉我没有构建任何语言环境。所以,这台 Ubuntu 机器默认使用的是通用的C语言环境,这可能在所有情况下都不适用。为了更改它,我将打开/etc/locale.gen文件,在文本编辑器中删除en_US.UTF-8 UTF-8行前面的注释符号。接下来,我将生成语言环境,像这样:

donnie@ubuntu20-04:~$ sudo locale-gen
Generating locales (this might take a while)...
  en_US.UTF-8... done
Generation complete.
donnie@ubuntu20-04:~$

最后,我将像在 Alma 机器上做的那样设置默认的语言环境和键盘映射设置:

donnie@ubuntu20-04:~$ sudo localectl set-locale en_US.UTF-8
donnie@ubuntu20-04:~$ sudo localectl set-keymap us
donnie@ubuntu20-04:~$ sudo localectl set-x11-keymap us
donnie@ubuntu20-04:~$

在我登出并重新登录后,一切恢复正常,就像我们在这里看到的:

donnie@ubuntu20-04:~$ localectl
   System Locale: LANG=en_US.UTF-8
       VC Keymap: us
      X11 Layout: us
donnie@ubuntu20-04:~$

现在,我得说,我完全不知道为什么 Ubuntu 安装程序没有为这台机器正确设置语言环境。我只知道,使用 Ubuntu 时,我已经习惯了偶尔会遇到一些奇怪的问题。

好吧,我认为locale部分大致讲完了。现在,让我们来谈谈设置时间和时区信息。

设置时间和时区参数

在计算机的石器时代,保持计算机的准确时间并不是那么重要。为了设置我那台老旧的 8088 处理器驱动的三洋 PC 克隆机的时间,我只是输入手表上的时间。那并不是最准确的方式,但也没关系。设置计算机时间的唯一真正理由就是为了给我创建的文件加上相对准确的时间戳。

如今,计算机上的准确时间保持对许多原因来说至关重要。幸运的是,我们现在有了systemd套件,它包括了timedatectl工具和systemd-timedated.service来帮助我们。在这一章中,我们将讨论timedatectl,但我们会将systemd-timedated.service的讨论留到第十七章理解 systemd 和引导加载程序

要查看你机器的时间保持状态,只需使用timedatectl,正如我在这里展示的那样:

[donnie@localhost ~]$ timedatectl
               Local time: Sun 2021-06-20 17:34:08 EDT
           Universal time: Sun 2021-06-20 21:34:08 UTC
                 RTC time: Sun 2021-06-20 21:33:06
                Time zone: America/New_York (EDT, -0400)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no
[donnie@localhost ~]$

Local time是我所在时区的时间,即东部夏令时EDT)。这是操作系统中设置的时间。

Universal timeUTC)是格林威治(英国)的时间,这是作为全球参考的时区。(UTC 以前被称为格林威治标准时间,或GMT。)

RTC,即实时时钟,是设置在计算机硬件时钟中的时间。你可以看到 RTC 时间与 UTC 时间几乎一致。两者中,UTC 通常更为准确,因为它会定期从互联网上或本地网络上的时间服务器获取当前时间。操作系统会在较少的时间间隔内从 UTC 更新时间 RTC。虽然可以将 RTC 时间配置为从本地时间更新,但这样会导致设置时区和判断何时切换夏令时出现问题。

我们还可以看到系统时钟已同步,NTP 服务处于活动状态,并且 RTC设置为本地时间。

timedatectl的手册页中,你会找到如何手动更改系统时间的说明。多年前,当我刚接触 Linux 时,我曾经需要经常手动设置时间,因为当机器的时钟偏差超过几分钟时,旧版的 NTP 服务不会自动设置时间。现代的 NTP 服务运行得要好得多。现在,只要启动机器时有可用的 NTP 服务器,NTP 服务就会正确地设置时间,无论机器时钟有多偏差。因此,你很可能永远不需要手动设置时间。

你也可能永远不需要手动设置时区,因为这通常是在安装操作系统时自动设置的。然而,可能会有一些情况需要手动设置时区,比如如果你需要将服务器从一个时区迁移到另一个时区。为此,你需要查看可用时区的列表,如下所示:

[donnie@localhost ~]$ timedatectl list-timezones
Africa/Abidjan
Africa/Accra
Africa/Addis_Ababa
Africa/Algiers
Africa/Asmara
. . .
. . .

这是一长串列表,所以让我们使用好朋友grep来缩小范围。假设我只对美国的时区感兴趣。那么我的grep过滤器会是这样:

[donnie@localhost ~]$ timedatectl list-timezones | grep 'America'
America/Adak
America/Anchorage
America/Anguilla
America/Antigua
America/Araguaina
America/Argentina/Buenos_Aires
America/Argentina/Catamarca
America/Argentina/Cordoba
America/Argentina/Jujuy
. . .
. . .

好的,这样的描述并没有像我想的那样缩小范围,因为America包含了北美和南美的多个时区。而且,我们不仅要选择具体的时区名称,还必须选择一个位于目标时区的城市。在我的情况下,尽管我位于美国乔治亚州的东南角,但我必须选择America/New_York作为我的时区,因为纽约恰好处于东部时区。假设出于某种疯狂的原因,我决定搬到西海岸。要更改我计算机的时区,我需要做如下操作:

[donnie@localhost ~]$ sudo timedatectl set-timezone America/Los_Angeles
[sudo] password for donnie:
[donnie@localhost ~]$

状态现在将显示如下:

[donnie@localhost ~]$ timedatectl
               Local time: Sun 2021-06-20 15:13:41 PDT
           Universal time: Sun 2021-06-20 22:13:41 UTC
                 RTC time: Sun 2021-06-20 22:12:40
                Time zone: America/Los_Angeles (PDT, -0700)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no
[donnie@localhost ~]$

所以,我现在已经设置为太平洋夏令时。

timedatectl的手册页中,你会看到一些命令在 Ubuntu 上有效,但在 Alma 上不起作用。这是因为 Ubuntu 被配置为使用systemd-timesyncd.service作为其时间同步服务,而 Alma 被配置为使用chronyd。在 Ubuntu 机器上,你可以像这样查看时间同步服务的状态:

donnie@ubuntu20-04:~$ timedatectl timesync-status
       Server: 91.189.94.4 (ntp.ubuntu.com)
Poll interval: 34min 8s (min: 32s; max 34min 8s)
         Leap: normal
      Version: 4
      Stratum: 2
    Reference: 83BC03DC
    Precision: 1us (-23)
Root distance: 52.680ms (max: 5s)
       Offset: -417us
        Delay: 116.531ms
       Jitter: 5.671ms
 Packet count: 14
    Frequency: -5.210ppm
donnie@ubuntu20-04:~$

这基本涵盖了时间同步的内容。接下来我们来设置hostname和机器信息。

设置主机名和机器信息

在计算机上设置一个合适的主机名在商业世界中非常有用。它允许计算机在IPv6地址中注册。) 在我们了解如何设置这些信息之前,先来看看如何查看这些信息。(注意,这个主机名可以通过使用动态域名服务DDNS)或使用像 Puppet、Chef 或 Ansible 这样的编排工具自动注册。)

查看信息

计算机的主机名设置在/etc/hostname文件中,正如我们在我的 Alma 机器上看到的那样:

[donnie@localhost ~]$ cd /etc
[donnie@localhost etc]$ cat hostname 
localhost.localdomain
[donnie@localhost etc]$

大多数 Linux 操作系统的安装程序允许你在系统安装过程中设置自定义的主机名。我在这台虚拟机上没有这么做,所以它使用了默认的localhost.localdomain。在这种情况下,localhost部分是实际的主机名,而localdomain部分是计算机所属的网络域名。在实际网络中,域名部分对每台计算机来说是相同的,而主机名部分对于每台计算机来说是唯一的。在家庭网络或任何不需要完整 FQDN 的情况下,你可以只使用一个没有域名的主机名。

在一台旧的 SysV 机器上,你所拥有的只是设置在/etc/hostname文件中的主机名或 FQDN。而在systemd中,事情要复杂得多。在 Alma 机器上,让我们看看hostnamectl命令给我们提供的额外信息:

[donnie@localhost ~]$ hostnamectl
   Static hostname: localhost.localdomain
         Icon name: computer-vm
           Chassis: vm
        Machine ID: 3a17f34dc2694acda37caa478a339408
           Boot ID: 37c1204df0ea439388727dce764f322f
    Virtualization: oracle
  Operating System: ]8;;https://almalinux.org/AlmaLinux 8.3 (Purple Manul)]8;;
       CPE OS Name: cpe:/o:almalinux:almalinux:8.3:GA
            Kernel: Linux 4.18.0-240.22.1.el8_3.x86_64
      Architecture: x86-64
[donnie@localhost ~]$

下面是详细信息:

  • 静态主机名:这是设置在/etc/hostname文件中的主机名或 FQDN。

  • 图标名称:某些图形应用程序会将计算机表示为图标。图标名称是显示在此计算机图标上的名称。我没有在这台虚拟机上设置图标名称,所以它默认为computer-vm。(系统自动检测到这是虚拟机,这也解释了vm部分的含义。)

  • 机箱:这表示我们使用的计算设备类型。我也没有设置这个,所以它默认为vm,即虚拟机。(大多数时候,systemd可以自动检测正确的机箱类型。)

  • 机器 ID:这个十六进制数字是一个唯一的数字,它在系统安装或第一次启动时分配给计算机。根据machine-id手册页,这个 ID 号应该视为机密信息,不应暴露给不受信任的网络。这个 ID 号存储在/etc/machine-id文件中。

  • 启动 ID:这个数字每次启动计算机时都会变化。hostnamectl命令从/proc/sys/kernel/random/boot_id文件中提取这个数字。

  • 虚拟化:这一行只会出现在虚拟机中。我在 Oracle VirtualBox 下运行这台虚拟机,所以此参数显示为oracle

  • 操作系统:操作系统信息来自/etc/os-release文件。

  • CPE OS Name:这是操作系统的名称,采用通用平台枚举CPE)格式。(我在这里不会深入讲解 CPE 的细节,但你可以通过在进一步阅读部分中点击链接来了解相关信息,这个链接位于本章的末尾。)

  • Kernel:这是正在运行的 Linux 内核的版本。你可以使用uname -r命令查看相同的信息。

  • Architecture:这显示了计算机中的 CPU 类型。你可以使用uname -m命令查看相同的信息。

只是为了好玩,让我们看看我主机的hostnamectl输出,这台主机运行的是 openSUSE 15.2:

donnie@localhost:~> hostnamectl
   Static hostname: n/a
Transient hostname: localhost.localdomain
         Icon name: computer-desktop
           Chassis: desktop
        Machine ID: 3d824afd08e94e34afeefca4f6fe0c95
           Boot ID: 32a49640e4fb4bc293b7cf312b80a2d7
  Operating System: openSUSE Leap 15.2
       CPE OS Name: cpe:/o:opensuse:leap:15.2
            Kernel: Linux 5.3.18-lp152.78-default
      Architecture: x86-64
donnie@localhost:~>

这里有一些差异,我希望你注意。首先,没有Static hostname值。与 AlmaLinux 安装程序不同,如果在安装过程中没有分配主机名,openSUSE 安装程序不会将任何内容写入/etc/hostname文件。文件存在,但里面什么也没有,正如你在这里看到的:

donnie@localhost:~> cat /etc/hostname 
donnie@localhost:~>

我们没有Static hostname值,而是有一个Transient hostname值。这个Transient hostname值是由 Linux 内核维护的动态主机名。它通常从/etc/hostname文件中设置的Static hostname属性中提取。如果hostname文件中没有内容,Transient hostname将默认为localhost.localdomain,除非由 DHCP 或 mDNS 服务器分配主机名。

最后,Icon nameChassis反映了这是一台桌面计算机,运行在裸机上。(今天,这台裸机是一台 2009 年款的惠普工作站,配有一对 AMD Opteron 四核处理器。虽然它已经很旧,但仍然能够完成任务。)同样,系统自动检测到了正确的Chassis值,正如它在虚拟机中所做的那样。

注意

要查看其他可用的Chassis类型,请查看machine-info手册页面。

现在我们已经查看了如何查看主机名和机器信息,接下来让我们学习如何设置它。

设置信息

systemd机器上有三种类型的主机名,你已经看到其中的两种。除了Static hostnameTransient hostname,还有Pretty hostname。为了说明pretty概念,让我们看看Static hostnameTransient hostname的标准。

hostname文件的手册页中,你可以通过man 5 hostname命令访问,里面会列出创建主机名的标准。以下是其中的详细内容:

  • 要求:主机名不能超过 64 个字符。

  • 建议

    1. 只使用来自旧的 7 位 ASCII 字符集的字符。(请参阅进一步阅读部分中的链接,查看哪些字符属于该字符集。)

    2. 所有字母应为小写。

    3. 主机名中不能包含空格或点(唯一的点应该位于主机名和域名之间,以及域名的两个部分之间)。

    4. 使用与 DNS 域名标签兼容的格式。

所以,在创建传统主机名时,你有一些限制。现在,借助Pretty hostname,你可以创建一个更加人性化的主机名,比如Donnie's Computer。(好吧,我没有发挥太多想象力,但你明白我的意思。)

当你使用hostnamectl设置主机名时,默认情况下会同时设置所有三种hostname类型。例如,假设我想让我的电脑命名为Donnie's Computer。我会使用如下命令:

[donnie@localhost ~]$ sudo hostnamectl set-hostname "Donnie's Computer"
[sudo] password for donnie: 
[donnie@localhost ~]$

现在,让我们查看hostnamectl的信息:

[donnie@localhost ~]$ hostnamectl
   Static hostname: DonniesComputer
   Pretty hostname: Donnie's Computer
         Icon name: computer-vm
           Chassis: vm
        Machine ID: 3a17f34dc2694acda37caa478a339408
           Boot ID: 7dae067e901a489580025ebdbec19211
    Virtualization: oracle
  Operating System: ]8;;https://almalinux.org/AlmaLinux 8.3 (Purple Manul)]8;;
       CPE OS Name: cpe:/o:almalinux:almalinux:8.3:GA
            Kernel: Linux 4.18.0-240.22.1.el8_3.x86_64
      Architecture: x86-64
[donnie@localhost ~]$

在这里,你可以看到hostnamectl自动将Pretty hostname转换成了适用于Static hostname的格式,只是它仍然允许使用大写字母。登出并重新登录后,新的Static hostname值将在命令提示符中显示,效果如下:

[donnie@localhost ~]$ exit
logout
Connection to 192.168.0.9 closed.
donnie@localhost:~> ssh donnie@192.168.0.9
donnie@192.168.0.9's password: 
Last login: Wed Jun 23 13:31:57 2021 from 192.168.0.222
[donnie@DonniesComputer ~]$

好的,这对于家用电脑来说很棒,但对于企业网络来说就不太适用了。这次,我们来创建一个适合 DNS 使用的 FQDN。假设我的本地网络已经设置为tevault.com域,并且我想将这台电脑命名为development-1。我用来创建 FQDN 的命令如下所示:

[donnie@DonniesComputer ~]$ sudo hostnamectl set-hostname development-1.tevault.com
[sudo] password for donnie: 
[donnie@DonniesComputer ~]$

再次使用hostnamectl,你会看到Pretty hostname现在已经消失:

[donnie@DonniesComputer ~]$ hostnamectl
   Static hostname: development-1.tevault.com
         Icon name: computer-vm
           Chassis: vm
        Machine ID: 3a17f34dc2694acda37caa478a339408
           Boot ID: 7dae067e901a489580025ebdbec19211
    Virtualization: oracle
  Operating System: ]8;;https://almalinux.org/AlmaLinux 8.3 (Purple Manul)]8;;
       CPE OS Name: cpe:/o:almalinux:almalinux:8.3:GA
            Kernel: Linux 4.18.0-240.22.1.el8_3.x86_64
      Architecture: x86-64
[donnie@DonniesComputer ~]$

登出并重新登录后,我将在命令提示符中看到正确的 DNS 友好型主机名:

[donnie@DonniesComputer ~]$ exit
logout
Connection to 192.168.0.9 closed.
donnie@localhost:~> ssh donnie@192.168.0.9
donnie@192.168.0.9's password: 
Last login: Wed Jun 23 14:34:24 2021 from 192.168.0.222
[donnie@development-1 ~]$

如果我只想设置一个Pretty hostname值,我可以这样做:

[donnie@development-1 ~]$ sudo hostnamectl set-hostname --pretty "Development 1"
[sudo] password for donnie: 
[donnie@development-1 ~]$

你可以使用hostnamectl设置其他几个选项,具体内容可以查看machine-info的手册页。hostnamectl的手册页向你展示了设置这些附加参数的命令。例如,让我们将这个虚拟机的位置设置为我现在的位置,也就是人声鼎沸的乔治亚州圣玛丽斯:

[donnie@development-1 ~]$ sudo hostnamectl set-location "Saint Marys GA"
[donnie@development-1 ~]$

现在,hostnamectl的输出中将显示位置。此外,当你第一次使用hostnamectl添加这些附加参数时,它会创建一个/etc/machine-info文件,该文件之前并不存在。以下是我添加了Pretty hostname值和位置后文件的样子:

[donnie@development-1 ~]$ cat /etc/machine-info 
PRETTY_HOSTNAME="Development 1"
LOCATION="Saint Marys GA"
[donnie@development-1 ~]$

很酷。乔治亚州圣玛丽斯现在已经是世界闻名了。

正如我们之前在localectl中看到的,使用hostnamectl更改参数时,会调用一个 dbus 类型的服务。在这种情况下,是systemd-hostnamed.service。它的样子如下:

图 9.1 - systemd-hostnamed.service文件

请注意,许多与安全相关的参数已经设置,这使得此服务几乎获得了与优秀的强制访问控制系统(如 SELinux)相同的保护。ProtectHome=yes 行和 ProtectSystem=strict 行使得大部分机器的文件系统对该服务不可访问,但底部的 ReadWritePaths=/etc 行提供了一个例外。ReadWritePaths=/etc 允许该服务读取或写入 /etc/ 目录中的文件。(我们在 更改 Alma 机器的默认区域设置 部分中查看过的 systemd-localed.service 就是这样设置的,但当时我没有指出这一点。)

好的,我想这章差不多到此为止了。我们总结一下,然后继续前进。

总结

正如我们所看到的,在本章中有一些很酷的内容。我们首先了解了什么是区域设置,以及如何在 systemd 系统中设置默认区域设置。接着,我们学习了如何设置时间和时区,并最终通过设置主机名和机器信息来结束这一部分。下一章,我们将给大脑稍作休息,讨论一些更简单的内容——即我们将探讨关机或重启系统的各种方法。我们下章见!

问题

  1. 以下哪条命令可以显示系统上安装了哪些区域设置?

    1. systemctl list-locales

    2. locale list-locales

    3. localectl list-locales

    4. localectl -a

  2. 当你使用 localectlhostnamectl 设置参数时,调用的是哪种服务?

    1. dbus

    2. oneshot

    3. notify

    4. forking

  3. 如果计算机的 hostname 文件中没有任何内容,会发生什么?

    1. 它将默认的 Static hostname 值设置为 localhost.localdomain 或从本地 DHCP 或 mDNS 服务器获得的任何值。

    2. 计算机将没有主机名。

    3. 它将 Pretty hostname 设置为 localhost.localdomain

    4. 它将默认的 Transient hostname 值设置为 localhost.localdomain 或从本地 DHCP 或 mDNS 服务器获得的任何值。

  4. 计算机的硬件时钟通常显示什么时间?

    1. 本地时间

    2. UTC 时间

答案

  1. C

  2. A

  3. D

  4. B

进一步阅读

要了解本章所涉及的主题,请查看以下资源:

第十章:理解关机和重启命令

到了你职业生涯的这个阶段,你很可能已经知道如何使用基本命令来关闭或重启文本模式的 Linux 服务器。在本章中,我们将探讨一些更具体的 systemd 方法。所以,请耐心等待。你可能会学到一些以前不知道的内容。

本章的具体内容包括以下几个主题:

  • 使用 systemctl 关机

  • 使用 systemctl 停止系统

  • 使用 systemctl 重启

  • 使用 shutdown 而非 systemctl

如果你准备好了,我们开始吧。

技术要求

虽然你的虚拟机都能同样有效地工作,但你只需要一个文本模式虚拟机,因此如果不需要,完全可以不启动 Alma 桌面虚拟机。在本章的后面,我们将使用一些 shell 脚本。如果你不想手动输入这些脚本,可以直接从我们的 Git 仓库下载它们。

查看以下链接,观看《Code in Action》视频:bit.ly/3G6nbkD

注意

在本书中,我一直使用 AlmaLinux 8 来替代即将被停用的 CentOS 8。(当然,根据你阅读本书的时间,CentOS 8 可能已经被停用。)

在我开始写这章之前的几天,Rocky Linux 8 的稳定版本终于发布了。使用它就像使用 AlmaLinux,或者任何其他 RHEL 8 克隆一样。不过,如果你关注安全性,Rocky 确实有一个巨大的优势。与其他 RHEL 8 克隆不同,Rocky 自带一套 OpenSCAP 配置文件,你可以在安装操作系统时或安装后应用这些配置文件。现在来看,Rocky Linux 是唯一完全支持 OpenSCAP 的 RHEL 8 克隆。(如果你想了解更多关于 OpenSCAP 的内容,一定要查看我另一本书《Mastering Linux Security and Hardening》,也是由 Packt Publishing 出版的。)

好的,如果你准备好了,我们就开始吧。

使用 systemctl 关机

使用 systemd 关机其实非常简单,但有一些你可能不知道的选项。我们从关闭并关机的基本命令开始,命令如下:

donnie@ubuntu20-04:~$ sudo systemctl poweroff

那么,究竟发生了什么呢?如果你打开 systemctl 的手册页面,并向下滚动到 poweroff 项目,你会看到这个命令启动了 poweroff.target,其内容如下:

[Unit]
Description=Power-Off
Documentation=man:systemd.special(7)
DefaultDependencies=no
Requires=systemd-poweroff.service
After=systemd-poweroff.service
AllowIsolate=yes
JobTimeoutSec=30min
JobTimeoutAction=poweroff-force
[Install]
Alias=ctrl-alt-del.target

[Unit]部分,你可以看到它要求systemd-poweroff.service,这意味着该服务将会启动。在[Unit]部分的底部,你会看到两个新参数。JobTimeoutSec=30min这一行给了 systemd 足够的时间来优雅地关闭所有正在运行的服务,然后再关闭电源。JobTimeoutAction=poweroff-force这一行意味着,如果所有服务在 30 分钟的时间窗口内没有优雅地关闭,那么 systemd 将强制关闭电源。在[Install]部分,我们看到了Alias=ctrl-alt-del.target这一行。这看起来有点奇怪,因为Ctrl + Alt + Del按键组合是用来重启机器的,而不是用来关闭机器的。此次不仅仅是 Ubuntu 的奇怪之处——在 Alma 机器上也是一样的。不过,这个问题其实很好解释。只是如果系统在关闭过程中挂起,按下Ctrl + Alt + Del组合键 7 次(在 2 秒内)将迫使机器重启。你然后可以直接启动到 GRUB 命令提示符,从那里关闭机器。(你可以在systemd的手册页中了解更多关于Ctrl + Alt + Del的信息。)

请记住,使用Ctrl + Alt + Del重启机器不需要管理员权限。这通常不是问题,因为关键任务的服务器应该被锁在一个安全的房间内,只有授权人员才能接触到它们。即便如此,你可能还是希望对重启服务器的权限进行一些限制。如果是这种情况,可以通过屏蔽ctrl-alt-del.target来禁用Ctrl + Alt + Del重启功能。执行此操作的命令是:

donnie@ubuntu20-04:~$ sudo systemctl mask ctrl-alt-del.target

现在,你可以无限次地按Ctrl + Alt + Del键组合,什么也不会发生。

在带有图形界面的桌面计算机上,Ctrl + Alt + Del键序列由桌面配置控制,在不同的桌面环境中可能有所不同。在 Alma 机器上,使用 Gnome 3 环境,按下Ctrl + Alt + Del会弹出正常的关机菜单,如下所示:

图 10.1 – Alma 机器上的 Gnome 3 关机菜单

在桌面计算机上屏蔽ctrl-alt-del.target并不会影响这一行为。

systemd-poweroff.service只有一个[Unit]部分,具体如下:

[Unit]
Description=Power-Off
Documentation=man:systemd-halt.service(8)
DefaultDependencies=no
Requires=shutdown.target umount.target final.target
After=shutdown.target umount.target final.target
SuccessAction=poweroff-force

该服务依赖于shutdown.targetumount.targetfinal.target。如果我们查看这些目标的单元文件,可以看到它们似乎什么也不做。例如,下面是shutdown.target文件的内容:

[Unit]
Description=Shutdown
Documentation=man:systemd.special(7)
DefaultDependencies=no
RefuseManualStart=yes

我们的好朋友strings显示,shutdown.targetsystemd可执行文件中有定义,如下所示:

donnie@ubuntu20-04:/lib/systemd$ strings systemd | grep 'shutdown.target'
shutdown.target
donnie@ubuntu20-04:/lib/systemd$

同样的情况也适用于umount.target,如下所示:

donnie@ubuntu20-04:/lib/systemd$ strings systemd | grep 'umount.target'
umount.target
donnie@ubuntu20-04:/lib/systemd$

最后,我们看到了final.target,如下所示:

[Unit]
Description=Final Step
Documentation=man:systemd.special(7)
DefaultDependencies=no
RefuseManualStart=yes
After=shutdown.target umount.target

虽然它看起来也没有做任何事情,但它并没有在systemd可执行文件中定义,如下所示:

donnie@ubuntu20-04:/lib/systemd$ strings systemd | grep 'final.target'
donnie@ubuntu20-04:/lib/systemd$

所以,我不知道 final.target 是在哪里定义的,但没关系。对于我们现在讨论的内容,这不重要。我也没找到关于 final.target 实际作用的任何信息,除了在 systemd.special 手册页面中的这段简短描述:

“一个特殊的目标单元,在关机逻辑中使用,并且在所有正常服务终止并且所有挂载卸载后,可能会用来拉入一些延迟的服务。”

我不知道那些 延迟服务 是什么,但没关系。对于我们当前的讨论,这不重要。

根据 systemctl 手册页面,systemctl poweroff 命令应该会向所有已登录系统的用户发送一个 wall 消息。然而,这是错误的。没有消息被发送出去,也没有选项可以让它发生。

通常,systemctl poweroff 命令会以有序的方式关闭正在运行的服务并卸载所有挂载的文件系统。使用 --force 选项则会在不先关闭服务的情况下直接关机。如果你有一个挂起的服务无法正常停止,这个选项会非常有用。使用 --force 选项 两次 会在不关闭服务或卸载任何挂载的文件系统的情况下直接关机。当然,除非是绝对紧急的情况,否则不推荐这样做,因为这可能会损坏文件系统并导致数据丢失。另一方面,如果 systemd 进程崩溃,使用 --force --force 可能会很有用。因为 --force --force 允许 systemctl 可执行文件在不与 systemd 进程联系的情况下关机。

好吧,让我们 结束 关于 poweroff 的讨论。接下来,我们简单聊一下如何停止系统。

使用 systemctl 停止

使用 sudo systemctl halt 命令会停止操作系统,但不会关闭计算机。因为我能读懂你的心思,我知道你现在一定在想,但 Donnie,我为什么要停止操作系统却让计算机继续运行呢? 嗯,我不知道。这是我在整个 Linux 生涯中从未弄明白的事。

但说实话,halt.target 的作用几乎和 poweroff.target 一样。所以,如果你真的感兴趣,我把相关单元文件的查阅留给你。

好吧,让我们 重新启动 这个话题,聊聊重启的事情。

使用 systemctl 重启

你永远猜不到用来重启系统的命令是什么。好吧,如果你说是 sudo systemctl reboot,那你赢得了今天的大奖。(可惜,这个大奖什么都没有,除了给出正确答案的成就感。)

再次提醒,我留给你去查看相关的reboot.target文件,因为它的工作方式与poweroff.target非常相似。唯一需要注意的不同之处是,这次在[Install]部分的Alias=ctrl-alt-del.target行实际上为我们做了些事情。在文本模式机器上,在本地终端按下Ctrl + Alt + Del组合键会重启机器。所以,是的,那个老三指礼仍然伴随着我们。(你甚至不需要输入管理员密码就能让它生效。所以,幸运的是,从远程终端按Ctrl + Alt + Del是不会生效的。)如果你想在你的 VirtualBox 虚拟机上尝试这个,你需要点击虚拟机的输入菜单,选择键盘,然后点击插入 Ctrl-Alt-Del

再次,我读懂了你的心思。我知道你在想,但是,Donnie,这些 systemctl 命令没有给我们提供以前shutdown命令中的那些酷选项。是的,你说得对,这也是为什么我仍然使用旧的shutdown命令。接下来,让我们谈谈这些命令,好吗?

使用shutdown代替systemctl

我们在 SysV 系统上使用的旧shutdown命令有一些很酷的选项。我们可以安排将来的关机或重启,取消已安排的关机,向所有已登录的用户广播即将关机或重启的消息。而使用systemctl命令时,你无法执行这些操作。幸运的是,旧的shutdown选项依然存在,通过一个符号链接指向systemctl可执行文件,正如我们在这里看到的:

donnie@ubuntu20-04:~$ cd /usr/sbin/
donnie@ubuntu20-04:/usr/sbin$ ls -l shutdown
lrwxrwxrwx 1 root root 14 May 27 11:16 shutdown -> /bin/systemctl
donnie@ubuntu20-04:/usr/sbin$

即使你不能在systemctl中使用旧的shutdown选项,你仍然可以通过指向systemctlshutdown链接使用它们。(奇怪,但是真的。)现在,我知道你这些老手可能已经知道这些shutdown命令了,没关系。如果你只是想略过这部分,也不会让我觉得不开心。另一方面,如果你能耐心看到最后,你会发现一些你目前可能还不知道的酷东西。如果你是 Linux 新手,这里几乎可以保证有一些有用的信息。所以,走吧。

每当你执行shutdown命令时,都可以指定关闭的时间。例如,要立即关闭机器,只需执行:

donnie@ubuntu20-04:~$ sudo shutdown now

在旧的 SysV 系统上,这个命令只会停止操作系统。要关闭机器,你需要使用-h选项。在一个 systemd 系统上,这个命令会关闭机器,因此不再需要-h选项开关。(奇怪的是,-h选项仍然在shutdown的手册页面中提到,尽管它现在已经不再起作用。)

你还可以指定关闭操作的时间,使用 24 小时制时间格式。例如,要在下午 6:00 关闭机器,可以执行以下命令:

donnie@ubuntu20-04:~$ sudo shutdown 18:00
Shutdown scheduled for Sun 2021-06-27 18:00:00 EDT, use 'shutdown -c' to cancel.
donnie@ubuntu20-04:~$

在计划关机时间前大约 25 分钟,系统会开始向所有已登录的用户发送广播消息,如我们在 Goldie 的例子中所见:

goldie@ubuntu20-04:~$
Broadcast message from root@ubuntu20-04 on pts/0 (Sun 2021-06-27 17:50:00 EDT):
The system is going down for poweroff at Sun 2021-06-27 18:00:00 EDT!
Broadcast message from root@ubuntu20-04 on pts/0 (Sun 2021-06-27 17:51:00 EDT):
The system is going down for poweroff at Sun 2021-06-27 18:00:00 EDT!

系统将继续发送广播消息,直到实际关机发生。消息发送的频率取决于关机计划发生的时间。如果在最后十分钟内,消息会每分钟发送一次。幸运的是,Goldie 只需按下Enter键,就能重新获得命令提示符,这样她就可以完成她正在做的事情。(顺便说一下,Goldie是我的最小猫咪的名字。你永远猜不到她是什么颜色的。)

如果你改变主意并想取消关机,可以使用-c选项开关,如下所示:

donnie@ubuntu20-04:~$ sudo shutdown -c
donnie@ubuntu20-04:~$

你还可以通过在shutdown命令后简单地添加自定义广播消息来发送你的消息:

donnie@ubuntu20-04:~$ sudo shutdown 18:45 "At 6:45 PM, this server will go down for maintenance. So, get your work done and log off."
Shutdown scheduled for Sun 2021-06-27 18:45:00 EDT, use 'shutdown -c' to cancel.
donnie@ubuntu20-04:~$

在计划关机时间前五分钟,nologin文件将在/run/目录下创建,如我们所见:

donnie@ubuntu20-04:/run$ ls -l no*
-rw-r--r-- 1 root root 121 Jun 27 18:40 nologin
donnie@ubuntu20-04:/run$

这将阻止其他用户登录。下次启动系统时,nologin文件会被删除。

每次你安排未来的关机任务时,都会在/run/systemd/shutdown/目录中创建一个scheduled文件。打开文件,你会看到类似这样的内容:

donnie@ubuntu20-04:/run/systemd/shutdown$ cat scheduled
USEC=1624917600000000
WARN_WALL=1
MODE=poweroff
donnie@ubuntu20-04:/run/systemd/shutdown$

USEC=行指定了计划关机的时间,采用 Unix 时间戳格式。如果你不知道系统计划关机的时间,并且想查找,可以使用一个 shell 脚本将其转换为人类可读的格式。下面是一个使用perl命令进行实际转换的脚本示例:

#!/usr/bin/bash
if [ -f /run/systemd/shutdown/scheduled ]; then
        perl -wne 'm/^USEC=(\d+)\d{6}$/ and printf("Shutting down at: %s\n", scalar localtime $1)' < /run/systemd/shutdown/scheduled
else
        echo "No shutdown is scheduled."
fi
exit

将文件保存为scheduled_shutdown_1.sh,并设置可执行权限,如下所示:

donnie@ubuntu20-04:~$ chmod u+x scheduled_shutdown_1.sh
donnie@ubuntu20-04:~$

你可以根据需要安排关机时间,然后运行脚本。输出结果应该类似如下:

donnie@ubuntu20-04:~$ ./scheduled_shutdown_1.sh
Shutting down at: Tue Jun 29 19:05:00 2021
donnie@ubuntu20-04:~$

如果系统上没有安装perl,或者你不想使用perl,你也可以使用awk来执行转换。使用awk的脚本如下所示:

#/bin/bash
if [ -f /run/systemd/shutdown/scheduled ]; then
        date -d "@$( awk -F '=' '/USEC/{ $2=substr($2,1,10); print $2 }' /run/systemd/shutdown/scheduled )"
else
        echo "No shutdown is scheduled."
fi
exit

这两个脚本都设置为查找scheduled文件,并且只有在scheduled文件存在时才会运行转换命令。如果文件不存在,脚本会通知你并优雅地退出。

要重启机器,只需使用带有-r选项的shutdown命令,像这样:

donnie@ubuntu20-04:~$ sudo shutdown -r now

你也可以像安排关机操作一样安排重启并发送自定义广播消息。

注意

你们这些老前辈可能记得,在 SysV 系统上,有一个 f 选项可以和 -r 选项一起使用。执行 sudo shutdown -rf now 命令时,系统会重启,并在挂载文件系统之前执行 fsck 操作。这个 f 选项现在已经不再使用了,因为 systemd 系统默认每次启动时都会对所有支持的文件系统执行 fsck 操作。这是因为如果检测到任何支持的文件系统,systemd-fsckd.service 会作为启动过程的一部分运行。(这里所说的支持的文件系统,是指 fsck 适用于 ext4 文件系统,这是 Ubuntu 的默认文件系统,但不适用于 xfs 文件系统,这是 RHEL 及其衍生版的默认文件系统。所以,如果你看到 systemd-fsckd.service 在你的 Alma 系统上没有运行,也不必太失望。)

第七章理解 systemd 定时器 中,我们学习了如何设置一个在你启动计算机时自动运行的任务。现在,让我们看看如何设置一个在关机时运行的任务。

在关机前运行任务

假设你想要每次关机时自动运行某个任务。(我让你发挥想象力,想想这个任务可能是什么。)为了实现这一点,只需创建一个自定义服务,并将其设置为WantedBy the shutdown.target。让我们来看一下如何操作。

我们将通过创建一个虚拟的 shell 脚本来演示这一点,该脚本与我们的新服务配合使用。在 /usr/local/bin/ 目录下,创建一个 script.sh 文件,内容如下:

#!/bin/bash
# Run script with systemd only at shutdown, and not for reboot.
systemctl list-jobs | egrep -q 'reboot.target.*start' && echo "Testing myscript.service for reboot" > /root/reboot_test.txt
systemctl list-jobs | egrep -q 'shutdown.target.*start' && echo "Testing myscript.service for shutdown" > /root/shutdown_test.txt

第一个 systemctl list-jobs 命令会在正在运行的任务列表中查找 reboot.target.*start* 这个文本字符串。如果找到了这个文本字符串,&& 运算符将触发 echo 命令的运行。echo 输出将会写入 /root/ 目录下的 reboot_test.txt 文件。然而,这实际上永远不会发生,因为该服务仅会由 shutdown.target 激活,而不是由 reboot.target 激活。下一行是相同的,只不过它是查找 shutdown.target.*start* 这个文本字符串。如果找到了该字符串,echo 命令的输出将会发送到 /root/shutdown_test.txt 文件。保存文件后,设置可执行权限,方法是:

donnie@ubuntu20-04:/usr/local/bin$ sudo chmod u+x script.sh

接下来,使用 sudo systemctl edit --full --force myscript.service 创建该服务。添加以下内容:

[Unit]
Description=Run this service only when shutting down
DefaultDependencies=no
Conflicts=reboot.target
Before=poweroff.target halt.target shutdown.target
Requires=poweroff.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/script.sh
RemainAfterExit=yes
[Install]
WantedBy=shutdown.target

ExecStart= 行激活我们的 shell 脚本。在 [Install] 部分,我们看到该服务的 WantedByshutdown.target

一旦保存了这个文件,进行正常的 sudo systemctl daemon-reloadsudo systemctl enable myscript.service 操作。为了测试,首先关机,然后重新启动。你应该能在 /root/ 目录下看到 shutdown_test.txt 文件:

donnie@ubuntu20-04:~$ sudo ls -l /root/
[sudo] password for donnie:
total 8
-rw-r--r-- 1 root root   38 Jun 29 18:19 shutdown_test.txt
drwxr-xr-x 3 root root 4096 Jun  3 20:20 snap
donnie@ubuntu20-04:~$ sudo cat /root/shutdown_test.txt
Testing myscript.service for shutdown
donnie@ubuntu20-04:~$

接下来,重新启动机器。这次您会发现没有文件被创建,证明此服务仅在关机时运行,而不是在重启时运行。

好了,我想这就是全部了。让我们结束这一章节。

总结

和往常一样,我们在本章看到了一些很酷的东西。我们开始查看用于关闭、停机或重启机器的systemctl命令。然后我们看到旧式的shutdown命令仍然有效,并且您可以继续使用您一直习惯使用的调度和消息功能。最后,我们创建了一个服务,该服务会在您关闭机器时运行一个作业。

我意识到你们中很多人可能已经熟悉我在本章中呈现的内容,但我在最后展示了一些很酷的东西,希望你们喜欢。

这就是掌握 systemd第一部分的结束。在第二部分中,我们将深入探讨cgroups的奥秘。到那时见。

问题

  1. Linux 机器重启命令是什么?

    a) sudo shutdown now

    b) sudo systemctl -r now

    c) sudo systemctl reboot now

    d) sudo systemctl reboot

    e) sudo shutdown --reboot

  2. 在预定关机时间的 5 分钟前会发生什么?

    a) 在/run/systemd/shutdown/目录中创建了一个scheduled文件。

    b) 在/run/目录中创建了一个scheduled文件。

    c) 在/run/systemd/shutdown/目录中创建了一个nologin文件。

    d) 在/run/目录中创建了一个nologin文件。

  3. 当您安排将来的关机时,以下哪些情况会发生?

    a) 在/run/systemd/shutdown/目录中创建了一个nologin文件。

    b) 在/run/systemd/shutdown/目录中创建了一个scheduled文件。

    c) 在/run/systemd/目录中创建了一个nologin文件。

    d) 在/run/目录中创建了一个scheduled文件。

  4. 如果执行sudo systemctl poweroff --force --force命令会发生什么?

    a) 使用两次--force选项会导致错误消息。

    b) 系统会忽略第二个--force选项。

    c) 您可能会损坏文件系统,这可能导致数据丢失。

    d) 如果第一个--force选项不能确保power off命令生效,那么第二个选项肯定会。

答案

  1. d

  2. d

  3. b

  4. c

进一步阅读

/tmp/目录,在每次关闭机器时都会被清空:

www.golinuxcloud.com/run-script-with-systemd-at-shutdown-only-rhel/

如何检查延迟关机的时间:您可以通过简单的 DuckDuckGo 搜索找到很多关于 Linux 管理的答案,就像我在这里做的那样。学习如何使用 DuckDuckGo。它可能是 Linux 管理员的好朋友:

unix.stackexchange.com/questions/229745/systemd-how-to-check-scheduled-time-of-a-delayed-shutdown

第二部分:理解 cgroups

在这一部分,你将学习什么是 cgroups,它们如何帮助控制资源使用并增强安全性。

本书的这一部分包括以下章节:

  • 第十一章**,理解 cgroups 版本 1

  • 第十二章**,使用 cgroups 版本 1 控制资源使用

  • 第十三章**,理解 cgroups 版本 2

第十一章:理解 cgroups 版本 1

在本章中,我们将介绍cgroups。 (更具体地说,我们将关注 cgroups 版本 1。) 你将学习什么是 cgroups,它们是如何构建的,以及你如何通过利用它们来获得好处。我们还将简要回顾 cgroups 的历史。

现在,我必须告诉你,讨论 cgroups 可能变得相当复杂和曲折。你可能已经见过一些在线的 cgroups 教程,它们让你只觉得头痛。我的目标是尽可能剥去复杂性,为你提供足够的信息,帮助你管理 systemd 机器上的资源。

具体主题包括:

  • 理解 cgroups 的历史

  • 理解 cgroups 的目的

  • 理解 cgroups 版本 1 的结构

  • 理解 cgroup 版本 1 文件系统

好的,如果你准备好了,我们开始吧!

技术要求

为了让内容更加有趣,我们将使用在第五章中设置的同一台 Alma 虚拟机,创建和编辑服务。你可能还记得,在那台虚拟机上,我们设置了一个以系统模式运行的 WordPress 容器服务,和一个以用户模式运行的 WordPress 容器服务。如果你没有那台虚拟机,可以返回到第五章创建和编辑服务,并按照步骤创建 WordPress 容器服务。和往常一样,本章将是动手实践的。所以启动那台虚拟机,我们一起深入研究吧。

请查看以下链接,观看“代码实践”视频:bit.ly/3ltmKsO

理解 cgroups 的历史

这可能会让你震惊,但 cgroups 技术最初并不是 systemd 的一部分,也不是由 Red Hat 发明的。它实际上是 Linux 内核中的一个组件,可以在非 systemd 的 Linux 发行版上运行。一对 Google 工程师早在 2006 年就开始了 cgroups 的开发,早于 Red Hat 工程师开始开发 systemd 四年。第一款包含 cgroups 技术的企业级 Linux 发行版是Red Hat Enterprise Linux 6,它使用的是混合 upstart/SysV 设置,而不是 systemd。在 RHEL 6 中使用 cgroups 是可选的,你必须经历一些复杂的步骤才能设置它们。

如今,cgroups 在所有主要的企业级 Linux 发行版中默认启用,并且与 systemd 紧密集成。RHEL 7 是第一款使用 systemd 的企业级发行版,也是第一款始终启用 cgroups 的企业级发行版。

目前有两个版本的 cgroups 技术。版本 1 在大多数情况下表现良好,但它确实存在一些缺陷,我这里不详细讨论。版本 2 是在 2013 年由 Facebook 的一位工程师开发的。在本章中,我将仅讨论版本 1。尽管版本 2 可能更好,但它仍未得到广泛应用,许多容器技术仍然依赖版本 1。目前所有企业级 Linux 发行版的默认版本都是版本 1。

注意

Fedora、Arch 和 Debian 11 是我所知道的唯一默认运行 cgroups 版本 2 的 Linux 发行版。我还看到一些猜测认为,下一版本的非 LTS 版 Ubuntu,即 Ubuntu 21.10,将会默认配备版本 2。(当然,当你读到这篇文章时,你可能已经知道确切的情况。)那么,你应该学习哪个版本呢?嗯,如果你是负责管理主要企业级 Linux 发行版的管理员,你就应该集中精力学习版本 1。如果你是开发者,你可能希望从版本 2 开始学习,因为版本 2 才是未来的趋势。

现在我们已经介绍了 cgroups 的历史,我想是时候做点历史性工作,解释一下它们是什么以及我们为什么需要它们了。那么,请允许我做一下这个解释。

理解 cgroups 的目的

在单核 CPU 时代,资源管理并不是一件大事。服务器通常配备一到四个单核 CPU,因此它们在能够同时运行的服务数量上本就有限。那时候,我们所需的资源管理工具只是一些简单的工具,如nicereniceulimit

如今,情况完全不同了。服务器现在配备了一个或多个多核 CPU 和大量内存。(目前最强的服务器 CPU 是 AMD Epyc,它现在有一个 64 核版本,能够同时运行 128 个线程。是的,这足以让我们这些极客垂涎三尺。)虽然这看起来似乎有些违反直觉,但在这些强大系统上进行资源管理比旧系统更为重要。这是因为现在一台服务器可以同时运行多个服务、多个虚拟机、多个容器和多个用户账户。一整间只能运行一两个服务的老旧物理服务器,如今可以被一台物理服务器所替代。我们曾经使用的那些简单的资源管理工具仍然有用,但我们现在需要更强大的工具,以确保所有进程和用户能够和谐共处。cgroups 应运而生。

使用 cgroups,管理员可以:

  • 按照进程或用户来管理资源使用。

  • 跟踪多租户系统中用户的资源使用情况,以提供准确的计费。

  • 更容易地将正在运行的进程相互隔离。这不仅能提高安全性,还能使我们拥有比以前更好的容器化技术。

  • 由于更好的资源管理和进程隔离,运行密集堆叠了虚拟机和容器的服务器。

  • 通过确保进程始终在同一 CPU 核心或一组 CPU 核心上运行,从而提高性能,而不是让 Linux 内核将它们移动到不同的核心。

  • 白名单或黑名单硬件设备。

  • 设置网络流量整形。

现在我们已经了解了 cgroups 的目的,我的目标是向你展示 cgroups 的结构。

理解 cgroups 版本 1 的结构。

要理解 cgroups 的结构,你需要了解一些 cgroups 的术语。我们从几个你需要知道的术语开始:

  • cgroupscgroup一词有两种不同的含义。我们最关心的是,cgroup 是一个进程集合。每个 cgroup 中的进程都受限于在cgroup 文件系统中定义的限制和参数。(稍后我们会更详细地讨论 cgroup 文件系统。)cgroup这个词还可以指代实现 cgroups 技术的 Linux 内核代码。

  • httpd.serviceapache2.service。(好吧,你已经知道这一点了,但我还是再告诉你一次。)

  • scopes:scope 是由某些外部方式启动的进程组。虚拟机、容器和用户会话都是 scope 的例子。

  • -.slice:这是 slice,它是整个 slice 层次结构的根。通常,它不会直接包含其他单元。不过,你可以用它来为整个 slice 树创建默认设置。

  • system.slice:默认情况下,由 systemd 启动的系统服务会在这里运行。

  • user.slice:默认情况下,用户模式服务会在这里运行。每个登录用户会自动分配一个隐式的 slice。

  • machine-slice:如果你运行的是容器或虚拟机,它们的服务会出现在这里。

此外,系统管理员可以定义自定义 slice,并将范围和服务分配到它们。

为了更直观地展示这一切,可以使用systemd-cgls命令作为普通用户。为了好玩,我们来看一下我们用来创建 WordPress 容器的 Alma 8 虚拟机,出自第五章创建和编辑服务systemd-cgls的输出应该像这样:

Control group /:
-.slice
├─user.slice
│ └─user-1000.slice
│   ├─user@1000.service
│   │ ├─wordpress-noroot.service
│   │ │ ├─ 918 /usr/bin/podman
│   │ │ ├─1013 /usr/bin/slirp4netns --disable-host-loopback --mtu 65520 --enabl>
│   │ │ ├─1019 containers-rootlessport
. . .
. . .

我在这台虚拟机上没有安装桌面环境,所以我们看不到在安装了桌面环境的机器上会看到的 Gnome 界面。不过,我们可以看到我们在第五章中创建的用户模式 WordPress 容器服务,创建和编辑服务。(如果你的虚拟机上安装了 Gnome,那也没关系。那只是意味着你需要稍微滚动一下才能看到你的 WordPress 容器服务。)

systemd-cgls 工具展示了系统上运行的 cgroups 的层次结构。列出的第一项是 / cgroup,这是根 cgroup 的标识。第二行开始列出根 slice(-.slice),直接下面是 user.slice。接下来,我们可以看到 user-1000.slice,这是 user.slice 的子级。在这种情况下,我是唯一登录到系统的用户,所以这个 slice 属于我。user-1000.slice 的名称对应于我的用户 ID 号,1000。之后,我们可以看到在我的 slice 中运行的服务,稍后我们会详细讲解。

注意

如果你想查看用户 slices,你需要从 cgroup 文件系统 外部 运行 systemd-cgls 命令。如果你进入 /sys/fs/cgroup/ 目录,你将看不到用户 slices。越深入 cgroup 文件系统,你通过 systemd-cgls 能看到的内容就越少。

user.slice 是由 /lib/systemd/system/user.slice 单元文件定义的,该文件内容如下:

[Unit]
Description=User and Session Slice
Documentation=man:systemd.special(7)
Before=slices.target

在这里,我们可以看到,这个 slice 必须先完成启动,才能启动 slices.targetslices.target 文件内容如下所示:

[Unit]
Description=Slices
Documentation=man:systemd.special(7)
Wants=-.slice system.slice
After=-.slice system.slice

根据 systemd.special 手册页面,slices.target 负责在启动机器时设置将要运行的 slices。默认情况下,它会启动 system.slice 和根 slice(-.slice),正如我们在 Wants= 行和 After= 行中看到的那样。我们还可以将更多的 slices 添加到该列表中,正如我们刚刚在 user.slice 文件中看到的。稍后我们将查看 -.slicesystem.slice。现在,让我们回到 user.slice

在我的 user-1000.slice 中,列出的第一个服务是 user@1000.service。这个服务负责管理所有在我的 slice 中运行的其他服务。它是通过 user@.service 模板设置的。user@.service 文件的 [Unit] 部分如下所示:

[Unit]
Description=User Manager for UID %i
After=systemd-user-sessions.service
After=user-runtime-dir@%i.service
Requires=user-runtime-dir@%i.service

当该服务运行时,%i 变量将被替换为用户 ID 号码。文件中的 [Service] 部分如下所示:

[Service]
User=%i
PAMName=systemd-user
Type=notify
ExecStart=-/usr/lib/systemd/systemd --user
Slice=user-%i.slice
KillMode=mixed
Delegate=pids memory
TasksMax=infinity
TimeoutStopSec=120s

以下是详细信息:

  • ExecStart=:这一行使 systemd 为每个登录的用户启动一个新的 systemd 会话。

  • Slice=:这一行为每个用户创建一个独立的 slice。

  • TasksMax=:这一行设置为无限制,意味着用户可以运行的进程数量没有上限。

  • Delegate=:我们将在 第十二章 中讨论这一指令,使用 cgroups 版本 1 控制资源使用

systemd-cgls 输出的下一个内容中,我们看到在我的用户 slice 中运行的所有服务都是 user@1000.service 的子级。当我向下滚动时,最终会跳过服务列表,看到我的登录会话的 scope。在这种情况下,我在本地终端的登录会话被标识为 session-2.scope,而远程登录会话则被标识为 session-3.scope。其内容如下所示:

. . .
. . .
├─session-2.scope
│   │ ├─ 794 login -- donnie
│   │ └─1573 -bash
│   └─session-3.scope
│     ├─ 1644 sshd: donnie [priv]
│     ├─ 1648 sshd: donnie@pts/0
│     ├─ 1649 -bash
│     ├─11493 systemd-cgls -l
│     └─11494 systemd-cgls -l
. . .
. . .

根据 systemd.scope 手册页,作用域不能通过创建单元文件来创建。相反,它们是在运行时通过编程创建的。所以,不要指望在 /lib/systemd/system/ 目录中看到任何 .scope 文件。

systemd-cgls 输出的进一步部分,我们终于跨越了我的用户切片。接下来,我们可以看到我的用户切片后面是 init.scopesystem.slice,如我们所见:

. . .
├─init.scope
│ └─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 18
├─system.slice
│ ├─rngd.service
│ │ └─732 /sbin/rngd -f --fill-watermark=0
│ ├─systemd-udevd.service
│ │ └─620 /usr/lib/systemd/systemd-udevd
│ ├─wordpress-container.service
│ │ └─1429 /usr/bin/conmon --api-version 1 -c cc06c35f21cedd4d2384cf2c048f01374>
│ ├─polkit.service
│. . .

在这里,我们可以看到与我的用户会话无关的系统服务。我们在这里看到的一个服务是以系统模式运行的 WordPress 容器服务。

我有一个系统模式的容器服务在运行,这意味着在 machine.slice 中有一些内容,如我们所见:

. . .
. . .
└─machine.slice
  └─libpod-cc06c35f21cedd4d2384cf2c048f013748e84cabdc594b110a8c8529173f4c81.sco>
    ├─1438 apache2 -DFOREGROUND
    ├─1560 apache2 -DFOREGROUND
    ├─1561 apache2 -DFOREGROUND
    ├─1562 apache2 -DFOREGROUND
    ├─1563 apache2 -DFOREGROUND
    └─1564 apache2 -DFOREGROUND

libpod 分支代表了我们的 podman-docker 容器。(注意,用户模式的容器服务只会直接出现在用户切片下,而不会出现在这里的 machine 切片下。)

好的,让我们切换回运行 Gnome 桌面的 Alma 机器。如我们所见,systemd-cgls 的输出显示了更多的内容:

Control group /:
-.slice
├─user.slice
│ └─user-1000.slice
│   ├─user@1000.service
│   │ ├─gvfs-goa-volume-monitor.service
│   │ │ └─2682 /usr/libexec/gvfs-goa-volume-monitor
│   │ ├─xdg-permission-store.service
│   │ │ └─2563 /usr/libexec/xdg-permission-store
│   │ ├─tracker-store.service
│   │ │ └─3041 /usr/libexec/tracker-store
│   │ ├─evolution-calendar-factory.service
│   │ │ ├─2725 /usr/libexec/evolution-calendar-factory
. . .
. . .

在任何桌面机器上,你总是会有比严格文本模式机器更多的正在运行的服务。

接下来,为 Frank 创建一个新的用户账户。然后,让 Frank 通过远程 SSH 会话登录到这台机器。现在 systemd-cgls 的输出的顶部部分如下所示:

Control group /:
-.slice
├─user.slice
│ ├─user-1001.slice
│ │ ├─session-10.scope
│ │ │ ├─8215 sshd: frank [priv]
│ │ │ ├─8250 sshd: frank@pts/1
│ │ │ └─8253 -bash
│ │ └─user@1001.service
│ │   ├─pulseaudio.service
│ │   │ └─8248 /usr/bin/pulseaudio --daemonize=no --log-target=journal
│ │   ├─gvfs-daemon.service
. . .
. . .

现在,Frank 有了自己的用户切片,即 user-1001.slice。我们看到他已经远程登录,以及他用来登录的虚拟终端的名称。(如果你在想,Frank 是我曾经野生的 Flame Point 暹罗猫,他已经和我在一起很多年了。直到刚才,他还躺在我键盘应该放置的位置上睡觉,这让我打字时相当不方便。)

如果你不想查看整个 cgroups 树,你可以使用 systemctl status 来查看它的一部分。例如,要仅查看 user.slice,我会执行 systemctl status user.slice。输出将如下所示:

图 11.1 – 在 Gnome 桌面的 Alma Linux 上的 user.slice

在这里,我们看到 Frank 已经注销,而我现在是唯一登录的用户。(毕竟,Frank 是只猫,这意味着他大部分时间都在睡觉。)我们还可以查看其他切片的信息,以及作用域的信息。例如,执行 systemctl status session-3.scope 会显示我在用户切片下运行的会话作用域信息,看起来像这样:

图 11.2 – Alma Linux 上的会话范围

好的,这基本涵盖了 cgroups 的基本结构。现在,让我们继续深入看看 cgroup 文件系统。

理解 cgroup 文件系统

在任何运行 cgroups 的系统上,你都会在 /sys/fs/ 虚拟文件系统下看到一个 cgroup 目录,如下所示:

[donnie@localhost ~]$ cd /sys/fs
[donnie@localhost fs]$ ls -ld cgroup/
drwxr-xr-x. 14 root root 360 Jul  3 15:52 cgroup/
[donnie@localhost fs]$

与所有虚拟文件系统一样,cgroup 文件系统仅在运行时存在于内存中,并在关机时消失。它没有在机器的硬盘上有永久副本。

当你查看 /sys/fs/cgroup/ 目录时,你会看到类似这样的内容:

图 11.3 – Alma Linux 上的 cgroupfs

这些目录中的每一个都表示一个 cgroup 子系统。(你也会看到它们被称为 控制器资源控制器。)在这些目录中,每个都有一组文件,表示 cgroup 的 可调参数。这些文件包含有关你将设置的任何资源控制或调优参数的信息。(我们将在 第十二章中详细讨论,“使用 cgroups 版本 1 控制资源使用”。)例如,下面是 blkio 目录中的内容:

图 11.4 – blkio 文件系统

这些文件中的每一个都表示一个可以自定义调优的参数,以获得最佳性能。往下看,我们还会看到 init.scopemachine.slicesystem.sliceuser.slice 目录。每个目录都有一组可调参数。

当我们使用 mount 命令并通过 grep 管道过滤时,我们会看到每个资源控制器都挂载在其独立的虚拟分区上。它们的样子是这样的:

[donnie@localhost ~]$ mount | grep 'cgroup'
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,seclabel,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
. . .
. . .
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)
[donnie@localhost ~]$

好的,我想这应该足以完成我们对 cgroups 版本 1 的基本介绍了。所以,让我们结束这一章并继续前进!

总结

在这一章中,我们回顾了 cgroups 技术的历史、cgroups 是什么,以及我们为什么需要它们。接着,我们看了 cgroups 的结构以及 cgroup 文件系统。

学习 cgroup 技术的一个主要挑战是,它没有很多可用的文档。我的意思是,你会看到很多博客文章和 YouTube 视频,但其中许多要么不够全面,要么已经过时。希望我能让你对 cgroup 技术以及它如何与 systemd 协同工作有更好的理解。

在下一章中,我们将探讨如何使用 cgroups 版本 1 控制资源使用情况。我会在那儿见。

问题

  1. cgroup 文件系统的默认位置是什么?

    a. /proc/fs/cgroup/

    b. /sys/cgroup/

    c. /sys/fs/cgroup/

    d. /proc/cgroup/

  2. 你必须做什么才能使用 systemd-cgls 查看用户 slice?

    a. 仅从本地终端运行该命令。

    b. 仅在 cgroup 文件系统外部运行该命令。

    c. 使用 root 权限运行该命令。

    d. 你不能这么做。用户 slice 永远不会显示出来。

  3. 如何创建你自己的 cgroup scope?

    a. 使用 systemctl edit --full --force,就像你操作其他 systemd 单元一样。

    b. 手动在 /etc/systemd/system/ 目录中创建单元文件。

    c. 你不能这么做。Scopes 是程序化创建的,并且没有 .scope 单元文件。

    d. 手动在 /lib/systemd/system/ 目录中创建单元文件。

  4. slice 的作用是什么?

    a. 它直接管理用户模式进程。

    b. 它直接管理系统模式进程。

    c. 它管理处于作用域或服务中的进程。

    d. 它管理用户登录会话。

答案

  1. c

  2. b

  3. c

  4. c

进一步阅读

请查看以下链接,了解有关 cgroups 的更多信息:

第十二章:使用 cgroups Version 1 控制资源使用

现在我们已经了解了cgroups是什么以及它们是如何结构化的,接下来该看看如何实际使用它们了。本章我们将覆盖以下具体内容:

  • 了解资源控制器

  • 控制 CPU 使用

  • 控制内存使用

  • 控制blkio的使用

  • 了解pam_limitsulimit

学习如何使用cgroups控制资源使用可以帮助你使数据中心运行得更安全、更高效。所以,系好安全带,让我们开始吧。

技术要求

为了最大限度地利用本章内容,你将需要一台较新的主机计算机,最好配有多核 CPU 和充足的内存。以我为例,我正在使用一台较新的戴尔工作站,配备了六核 Xeon CPU 和 32GB 内存。启用了超线程技术,这样我总共有 12 个 CPU 核心可以使用。

设置你的虚拟机使用至少两个 CPU 核心和适量的内存。我设置的是四个核心,如下所示:

图 12.1 – 在 VirtualBox 中设置 CPU 核心

我还将我的虚拟机设置为使用 8GB 的内存,如下所示:

图 12.2 – 在 VirtualBox 中设置 RAM

像往常一样,我将使用我的 Ubuntu Server 20.04 和 AlmaLinux 8 虚拟机进行演示。

请查看以下链接,观看《代码实战》视频:bit.ly/3xJ61qi

现在我们已经完成了所有的设置,接下来让我们深入探讨。

了解资源控制器

对于这个 cgroups 功能,有一些不同的名称。我更倾向于使用资源控制器这个术语。在其他文档中,你可能会看到这些资源控制器被称为子系统或仅称为控制器。这些术语都指的是同一件事,即 cgroups 技术,它允许我们控制各种正在运行的进程的资源使用情况。在我们开始深入了解之前,先来看看我们有哪些资源控制器。

检查资源控制器

查看我们有哪些资源控制器的最好方法是安装一些 cgroup 工具。在 Ubuntu 机器上,执行以下命令:

donnie@ubuntu2004:~$ sudo apt install cgroup-tools

在 Alma 机器上,执行以下命令:

[donnie@localhost ~]$ sudo dnf install libcgroup-tools

在任何一台机器上,我们现在都可以使用lssubsys来查看我们的活动资源控制器,像这样:

donnie@ubuntu2004:~$ lssubsys
cpuset
cpu,cpuacct
blkio
memory
devices
freezer
net_cls,net_prio
perf_event
hugetlb
pids
rdma
donnie@ubuntu2004:~$

这里是每个资源控制器的简要描述:

  • cpuset:如果你在运行多个 CPU 核心的系统上,这可以让你将一个进程分配到特定的 CPU 核心或一组 CPU 核心。这通过强制进程使用已被填充了该进程需要的数据和指令的 CPU 缓存来提高性能。默认情况下,Linux 内核调度程序可以将进程从一个 CPU 核心移动到另一个核心,或从一组 CPU 核心移动到另一组。每当发生这种情况时,正在运行的进程必须访问主系统内存来重新填充 CPU 缓存。这会消耗额外的 CPU 周期,可能会影响性能。

  • cpu,cpuacct:以前,cpucpuacct有两个独立的控制器。现在,它们被合并成一个控制器。这个控制器让你控制进程或用户的 CPU 使用情况。在多租户系统中,它允许你监控用户的 CPU 使用情况,这对于计费非常有用。

  • blkio:这是块输入/输出的缩写。此控制器允许你设置进程和用户从块设备读取或写入数据的速度限制。(块设备是像硬盘或硬盘分区这样的设备。)

  • memory:正如你可能猜到的,这一项允许你设置进程或用户可以使用的系统内存限制。

  • devices:此项允许你控制对系统设备的访问。

  • freezer:这个名字听起来很奇怪,但它的用途很简单。它允许你挂起 cgroup 中运行的进程。当你需要将进程从一个 cgroup 移动到另一个时,这非常有用。准备好后,只需恢复进程。

  • net_cls,net_prio:此项允许你为网络数据包添加类标识符(classid)标签。Linux 流量控制器和 Linux 防火墙可以使用这些标签来控制和优先处理不同 cgroup 的网络流量。

  • perf_event:此项允许你使用perf工具监控 cgroup。

  • hugetlb:此项允许你的 cgroup 使用巨大页虚拟内存,并对其使用设置限制。(这有点超出了本书的范围,所以我们不再深入讨论。)

  • pids:此项允许你为在 cgroup 中运行的进程数量设置限制。

  • rdma远程直接内存访问允许一台计算机直接访问另一台计算机的内存,而无需涉及任何一台计算机的操作系统。这主要用于并行计算集群,这也超出了本书的范围。

cgroups手册页中,你会在版本 1 控制器部分看到对这些控制器的简短提及。要查看它们的详细描述,你需要查看与 Linux 内核源代码一起打包的文档。在 Alma 机器上,你可以通过安装一个单独的文档包来获得这些文档,方法是:

[donnie@localhost ~]$ sudo dnf install kernel-doc

/usr/share/doc/kernel-doc-4.18.0/Documentation/cgroup-v1/目录中,你现在可以找到包含有关资源控制器的更详细解释的文本文件。(我也曾在 Ubuntu 机器上查找该文档包,但没能找到。)当然,必须提醒你,这些文档页面主要是为 Linux 内核程序员编写的,所以你可能不会从中获得太多信息。但谁知道呢?不妨快速浏览一下,看看是否有能帮助你的内容。(你也可能发现它们是很好的助眠工具,尤其在你失眠严重的夜晚。)

当你查看/sys/fs/cgroup/目录时,你会发现每个资源控制器都有自己的目录。以下是 Ubuntu 机器上的情况:

图 12.3 – Ubuntu 上的资源控制器

现在,我们暂时忽略屏幕底部的两个目录。(systemd目录是用于 root cgroup 的,unified目录是用于 Version 2 控制器的。)尽管我们在这里运行的是 cgroups Version 1,但仍然可以使用 Version 2 控制器。(你在 Alma 机器上看不到unified目录,因为 RHEL 8 类型的发行版默认没有启用 Version 2 控制器。)请注意,本章将只讨论 Version 1 控制器。

此外,注意我们有四个符号链接指向两个不同的目录。这是因为cpucpuacct控制器曾经是两个独立的控制器,但现在已经合并为一个控制器。net_clsnet_prio控制器也一样。这些符号链接为我们提供了一些向后兼容性。

注意

由于篇幅有限,我无法详细介绍所有这些资源控制器。所以我们将重点关注你最可能使用的三大控制器。它们分别是cpumemoryblkio控制器。也正好,因为在 cgroups Version 1 中,这三个是你可以通过systemd直接配置的唯一资源控制器。(要使用其他 Version 1 控制器,你需要绕一些弯路并使用一些非 systemd 的管理工具。)

好了,理论部分先讲到这里。让我们开始动手操作吧。

为演示做准备

在前几个演示中,我们将使用stress-ng工具来模拟一些真实世界中的问题。在 Ubuntu 机器上,通过以下命令安装它:

donnie@ubuntu2004:~$ sudo apt install stress-ng

要在 Alma 机器上安装它,首先需要安装 EPEL 仓库。如果你还没有安装,请通过以下命令进行安装:

[donnie@localhost ~]$ sudo dnf install epel-release

然后,通过以下命令安装stress-ng包:

[donnie@localhost ~]$ sudo dnf install stress-ng

接下来,创建一个新的非特权用户账户。(我为 Vicky 创建了一个账户,她是我家的灰色小猫。)

然后,在主机上打开一个终端,并让新用户登录到虚拟机的远程会话。在主机上打开第二个终端,并以你自己的账户登录到虚拟机。把虚拟机的本地终端放到一边,因为你也会用到它。

现在我们已经准备好了,让我们来讨论一下cpu资源控制器。

控制 CPU 使用

你可以通过使用systemctl set-property命令或者编辑 systemd 单元文件来控制资源使用。对于第一个演示,我们将让 Vicky 给虚拟机的 CPU 施加一些压力。我们将通过使用systemctl set-property来配置cpu资源控制器来处理它。

控制 Vicky 的 CPU 使用

默认情况下,Linux 系统中的所有用户都可以无限制地使用系统资源。在多用户系统中,这可能会成为问题。任何用户都可能决定占用所有资源,这实际上可能导致对所有其他用户的拒绝服务(DoS)情况。在实际操作中,用户可能会通过做一些看似无害的事情,比如渲染一个大视频文件,来造成问题。授权用户也可能通过做一些不该做的事情,比如利用服务器资源进行加密货币挖矿,来造成拒绝服务(DoS)。无论如何,我们都希望限制用户可以使用的资源。我们将通过给用户的切片分配资源限制来实现这一点。

假设 Vicky 远程登录并占用了其他用户的所有 CPU 时间。通过让 Vicky 执行以下操作来模拟这一情况:

vicky@ubuntu2004:~$ stress-ng -c 4

该命令中的-c 4选项表示 Vicky 正在对 CPU 的四个核心进行压力测试。将该数字更改为您为自己的虚拟机分配的核心数。

在远程终端中,您已登录到自己的账户,打开top工具。它应该看起来像这样:

图 12.4 – 带有 Vicky 压力测试的top显示

top显示的顶部,我们看到 Vicky 几乎占用了所有四个 CPU 核心的 100%。我还需要告诉你这不好吗?

保持top在您的远程终端上运行,然后转到虚拟机的本地终端。为了获得正确的结果,确保您不在/sys/fs/cgroup/文件系统中。使用systemd-cgls来查找 Vicky 的用户切片,应该像这样:

图 12.5 – Vicky 的用户切片

我们看到她的用户编号是1001,我们想让她知道谁才是这里的老板。我们不会让她继续这样占用 CPU 资源。所以,在本地终端上,通过以下命令将她的CPUQuota减少到10%

donnie@ubuntu2004:~$ sudo systemctl set-property user-1001.slice CPUQuota=10%

该命令会在/etc/systemd/目录下创建一些新文件,这意味着您需要执行sudo systemctl daemon-reload,就像创建新的单元文件时一样。现在您应该看到 Vicky 的 CPU 使用率几乎降到零,正如我们在这里看到的:

图 12.6 – 减少 Vicky 的 CPU 配额后

好吧,也许把 Vicky 的CPUQuota降到只有10%有点激进了。在实际生活中,你可以根据需要调整CPUQuota。在具有多个核心的机器上,有一个你应该知道的技巧。就是无论你给 Vicky 多少配额,都会分配到所有可用的 CPU 核心上。所以,在这种情况下,我们并不是给 Vicky 每个核心的 10%。相反,我们把这 10%分配到四个核心上,这使得她只能消耗每个核心大约 2.5%的 CPU 周期,如你在图 12.6中所见。另外,将 Vicky 的CPUQuota设置为100%并不会让她每个核心都使用 100%。相反,她只会使用每个核心约 25%的性能。要让她每个核心使用 50%,将CPUQuota设置为200%。在这台有四个核心的机器上,我们能设定的最大值是400%,这将让她每个核心使用 100%。

注意

请记住,我刚给你提供的数据是基于将四个核心分配给虚拟机。如果你分配了不同数量的核心给你自己的虚拟机,这些数据会有所不同。

当你第一次执行systemctl set-property命令时,你将在/etc/systemd/目录下创建system.control/目录,它看起来像这样:

 donnie@ubuntu2004:/etc/systemd$ ls -ld system.control/
drwxr-xr-x 3 root root 4096 Jul 14 19:59 system.control/
donnie@ubuntu2004:/etc/systemd$

在那个目录下,你会看到一个用于 Vicky 用户分片的目录。在她的用户分片目录下,你会看到 Vicky 的CPUQuota配置文件,如你所见:

donnie@ubuntu2004:/etc/systemd/system.control$ ls -l
total 4
drwxr-xr-x 2 root root 4096 Jul 14 20:25 user-1001.slice.d
donnie@ubuntu2004:/etc/systemd/system.control$ cd user-1001.slice.d/
donnie@ubuntu2004:/etc/systemd/system.control/user-1001.slice.d$ ls -l
total 4
-rw-r--r-- 1 root root 143 Jul 14 20:25 50-CPUQuota.conf
donnie@ubuntu2004:/etc/systemd/system.control/user-1001.slice.d$

在这里你可以看到,我刚刚把 Vicky 的配额设置到了200%

donnie@ubuntu2004:/etc/systemd/system.control/user-1001.slice.d$ cat 50-CPUQuota.conf 
# This is a drop-in unit file extension, created via "systemctl set-property"
# or an equivalent operation. Do not edit.
[Slice]
CPUQuota=200%
donnie@ubuntu2004:/etc/systemd/system.control/user-1001.slice.d$

现在,请注意,当你第一次创建这个文件时,只需执行daemon-reload命令。任何后续使用systemctl set-property命令对此文件的更改将立即生效。

在 cgroup 文件系统中,在 Vicky 的用户分片目录下,你会看到她当前的CPUQuota设置在cpu.cfs_quota_us文件中。当设置为200%时,它看起来像这样:

donnie@ubuntu2004:/sys/fs/cgroup/cpu/user.slice/user-1001.slice$ cat cpu.cfs_quota_us 
200000
donnie@ubuntu2004:/sys/fs/cgroup/cpu/user.slice/user-1001.slice$

要得到实际的 200%数字,只需去掉200000中的最后三个零。

好了,我们完成了这个演示。在 Vicky 的窗口中,按Ctrl + C来停止压力测试。

接下来,让我们看看如何限制服务的 CPU 使用。

控制服务的 CPU 使用

对于这个演示,请在虚拟机的本地终端执行命令,并在你自己的远程终端上保持top命令运行。

这个演示的第一步是在虚拟机的本地终端创建cputest.service,就像这样:

donnie@ubuntu2004:~$ sudo systemctl edit --full --force cputest.service

文件的内容将如下所示:

[Unit]
Description=CPU stress test service
[Service]
ExecStart=/usr/bin/stress-ng -c 4

你看,这里没有什么花哨的东西。这足以完成工作。就像之前一样,修改-c选项以反映你为自己的虚拟机分配的核心数。接下来,执行daemon-reload,然后启动服务:

donnie@ubuntu2004:~$ sudo systemctl daemon-reload
donnie@ubuntu2004:~$ sudo systemctl start cputest.service 
donnie@ubuntu2004:~$

在顶部的显示中,你应该看到cputest.service占用了 100%的 CPU:

图 12.7 – 无限制的 cputest.service

接下来,让我们从命令行为这个服务设置CPUQuota

从命令行设置 CPUQuota

为服务设置 CPUQuota 和为用户设置没有什么不同。假设我们只想为这个服务设置 90% 的 CPUQuota,我们可以像为 Vicky 设置一样,通过命令行来设置:

donnie@ubuntu2004:~$ sudo systemctl set-property cputest.service CPUQuota=90%
[sudo] password for donnie: 
donnie@ubuntu2004:~$

这样做会在 /etc/systemd/system.control/ 目录中创建另一个目录:

donnie@ubuntu2004:/etc/systemd/system.control$ ls -l
total 8
drwxr-xr-x 2 root root 4096 Jul 15 19:15 cputest.service.d
drwxr-xr-x 2 root root 4096 Jul 15 17:53 user-1001.slice.d
donnie@ubuntu2004:/etc/systemd/system.control$

/etc/systemd/system.control/cputest.service.d/ 目录下,您会看到 50-CPUQuota.conf 文件,它的设置与我们为 Vicky 创建的文件相同:

donnie@ubuntu2004:/etc/systemd/system.control/cputest.service.d$ cat 50-CPUQuota.conf 
# This is a drop-in unit file extension, created via "systemctl set-property"
# or an equivalent operation. Do not edit.
[Service]
CPUQuota=90%
donnie@ubuntu2004:/etc/systemd/system.control/cputest.service.d$

这使得cputest.service只能使用每个 CPU 核心的约 22.5%,正如我们在这里看到的:

图 12.8 – 配置了 90% CPUQuota 的 cputest

在这里,在 cgroup 文件系统中,我们看到 CPUQuota 确实被设置为 90%

donnie@ubuntu2004:/sys/fs/cgroup/cpu/system.slice/cputest.service$ cat cpu.cfs_quota_us 
90000
donnie@ubuntu2004:/sys/fs/cgroup/cpu/system.slice/cputest.service$

请注意,这个限制只对服务生效,而不对拥有该服务的 root 用户生效。root 用户仍然可以运行其他程序和服务,没有任何限制。

接下来,让我们在 cputest.service 文件中设置 CPUQuota

在服务文件中设置 CPUQuota

首先,停止 cputest.service,像这样:

donnie@ubuntu2004:~$ sudo systemctl stop cputest.service 
donnie@ubuntu2004:~$

接下来,删除您使用 systemctl set-property 命令创建的 cputest.service.d/ 目录:

donnie@ubuntu2004:/etc/systemd/system.control$ sudo rm -rf cputest.service.d/
donnie@ubuntu2004:/etc/systemd/system.control$

执行 systemctl daemon-reload,然后启动 cputest.service。您应该会看到服务现在再次占用了 CPU,就像最初一样。停止服务,然后通过以下方式编辑单元文件:

donnie@ubuntu2004:~$ sudo systemctl edit --full cputest.service

添加 CPUQuota=90% 这一行,文件现在应该看起来像这样:

[Unit]
Description=CPU stress test service
[Service]
ExecStart=/usr/bin/stress-ng -c 4
CPUQuota=90%

保存文件并启动服务。您应该会在 top 显示中看到新的设置已经生效。

就这些,简单吧?

注意

systemd.resource-control 手册页解释了您可以使用的各种指令来控制资源使用。当您阅读它时,请注意哪些指令适用于 cgroups 版本 1,哪些适用于 cgroups 版本 2。同时,请注意标记为弃用的指令。例如,您在网上找到的许多 cgroups 教程会告诉您使用 CPUShares 指令,但在此手册页中,该指令被列为弃用。(在 Linux 术语中,弃用的东西现在仍然有效,但将来某个时候会停止工作。在这种情况下,这些弃用的指令对版本 1 有效,但对版本 2 无效。)

我们不再需要 cputest.service,所以可以停止它。接下来,我们看看如何控制 Vicky 的内存使用情况。

控制内存使用

让我们首先让 Vicky 做一些占用系统内存的事情。和之前一样,我们将使用 stress-ng 工具来模拟,像这样:

vicky@ubuntu2004:~$ stress-ng --brk 4

等待片刻,您将在 top 显示中看到一些相当糟糕的情况:

图 12.9 – Vicky 的内存使用情况的 top 显示

是的,只有 98.9 字节的空闲内存,而且负载平均值超高。事实上,大约 2 分钟后,这台虚拟机对任何命令完全没有响应。哎呀!

现在,要理解的是,我仍然为 Vicky 设置了 200%的CPUQuota。所以,CPU 使用率不是这里的问题。负载平均值表示有多少任务在等待 CPU 的处理。在top显示的顶部,如图 12.9所示,你看到的53.51是 1 分钟的平均值,46.38是 5 分钟的平均值,25.00是 15 分钟的平均值。这些负载平均值是分布在所有可用的 CPU 核心上的。这意味着,你的核心越多,负载平均值可以越高,而不影响系统性能。只有四个核心,我的虚拟机甚至无法处理像这样的负载平均值。通过占用所有的系统内存,Vicky 阻止了 CPU 及时处理任务。

为了在这个无响应的虚拟机上关闭 Vicky 的程序,我不得不通过点击stress-ng会话关闭她的远程终端窗口。我是说,根本没有其他办法。如果这种情况发生在物理服务器的本地终端上,你可能不得不采取极端措施,要么按下电源开关,要么拔掉电源线。即使在这个stress-ng进程上执行kill命令也不起作用,因为系统根本无法执行这个命令。

为了防止这种情况再次发生,让我们为 Vicky 设置一个 1GB 的内存限制,像这样:

donnie@ubuntu2004:~$ sudo systemctl set-property --runtime user-1001.slice MemoryMax=1G
[sudo] password for donnie: 
donnie@ubuntu2004:~$

MemoryMax,嗯?那可能是为我们老年人设计的一种增强记忆的营养补充品的名字。

说实话,你看我在使用--runtime选项,我以前没有用过。这个选项让设置变成临时的,这样当我重启这台机器时,设置就会消失。与其在/etc/systemd/system.control/user-1001.slice.d/目录中创建永久配置文件,这个方便的--runtime选项在/run/systemd/system.control/user-1001.slice.d/目录中创建了一个临时配置文件,内容如下:

donnie@ubuntu2004:/run/systemd/system.control/user-1001.slice.d$ cat 50-MemoryMax.conf 
# This is a drop-in unit file extension, created via "systemctl set-property"
# or an equivalent operation. Do not edit.
[Slice]
MemoryMax=1073741824
donnie@ubuntu2004:/run/systemd/system.control/user-1001.slice.d$

为了使设置永久生效,只需再次运行命令,不带--runtime选项,然后执行daemon-reload

现在,当 Vicky 运行她那邪恶的内存占用程序时,她就不能锁死系统了。

控制 blkio 使用

在这种情况下,Vicky 再次试图为自己独占系统资源。这次,她从系统硬盘中读取了如此多的数据,以至于其他人都无法使用它。在我们进入这一点之前,你需要在虚拟机上安装iotop,以便你能够测量 Vicky 使用的带宽。在 Ubuntu 机器上,执行:

sudo apt install iotop

在 Alma 机器上,执行:

sudo dnf install iotop

在你运行top的远程登录窗口中,退出top,然后执行:

sudo iotop -o

现在我们已经设置好了,让我们来看看如何为 Vicky 设置blkio限制。

为 Vicky 设置 blkio 限制

在 Vicky 的远程登录窗口中,让她使用我们的好朋友dd来创建一个虚拟文件,像这样:

vicky@ubuntu2004:~$ dd if=/dev/zero of=afile bs=1M count=10000
10000+0 records in
10000+0 records out
10485760000 bytes (10 GB, 9.8 GiB) copied, 17.4288 s, 602 MB/s
vicky@ubuntu2004:~$

很好,Vicky 已经创建了一个全是零的 10GB 文件。接下来,让 Vicky 使用dd将文件内容复制到/dev/null设备,同时在我们自己的远程登录窗口中查看iotop -o显示。命令如下:

vicky@ubuntu2004:~$ dd if=afile of=/dev/null
20480000+0 records in
20480000+0 records out
10485760000 bytes (10 GB, 9.8 GiB) copied, 69.2341 s, 151 MB/s
vicky@ubuntu2004:~$

所以,看起来她以每秒 151 MB 的平均速率读取了这个文件。iotop的显示如下所示:

图 12.10 – Vicky 没有限制时的读取带宽

为了限制她的读取带宽,我们首先需要知道她是从哪里读取文件的。我们可以使用lsblk工具来获取线索,像这样:

donnie@ubuntu2004:~$ lsblk
NAME                      MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
loop0                       7:0    0  99.4M  1 loop /snap/core/11316
. . .
. . .
sda                         8:0    0     1T  0 disk 
├─sda1                      8:1    0     1M  0 part 
├─sda2                      8:2    0     1G  0 part /boot
└─sda3                      8:3    0     1T  0 part 
  └─ubuntu--vg-ubuntu--lv 253:0    0   200G  0 lvm  /
sdb                         8:16   0    10G  0 disk 
└─sdb1                      8:17   0    10G  0 part /media/backup
sr0                        11:0    1  1024M  0 rom  
donnie@ubuntu2004:~$

我们知道 Vicky 的文件在她自己的主目录下。我们在这里看到,/home/目录没有单独挂载。所以,它必须位于根分区,该分区作为逻辑卷挂载在/dev/sda驱动器上。现在,假设我们想将 Vicky 的读取带宽限制为每秒仅 1MB。命令如下:

donnie@ubuntu2004:~$ sudo systemctl set-property user-1001.slice BlockIOReadBandwidth="/dev/sda 1M"
[sudo] password for donnie: 
donnie@ubuntu2004:~$

注意,设备名称和速率限制设置都必须用一对双引号括起来。此外,请注意,我们为整个驱动器设置了带宽限制,而不仅仅是为特定的分区或逻辑卷设置。当然,我们在/etc/systemd/system.control/目录中创建了一个新的设置文件,因此一定要执行daemon-reload

接下来,让 Vicky 重复她的dd if=afile of=/dev/null命令。请注意,凭借她的带宽限制,这将需要一段时间才能完成。在运行时,请注意 Vicky 在iotop窗口中的减少速度:

图 12.11 – Vicky 的带宽限制

是的,她的速度稍低于每秒 1MB,正是我们希望的状态。顺便说一句,如果你想在操作完成前中止它,也不要感到难过。以每秒 1MB 的速率,它将在很长时间后才会完成。

最后,在 Vicky 仍然登录的情况下,查看这个命令在 cgroup 文件系统中修改的属性文件:

donnie@ubuntu2004:/sys/fs/cgroup/blkio/user.slice/user-1001.slice$ cat blkio.throttle.read_bps_device 
8:0 1000000
donnie@ubuntu2004:/sys/fs/cgroup/blkio/user.slice/user-1001.slice$

在这个blkio.throttle.read_bps_device文件中,8:0表示/dev/sda设备的主设备号和次设备号,如下所示:

donnie@ubuntu2004:/dev$ ls -l sda
brw-rw---- 1 root disk 8, 0 Aug 19 14:01 sda
donnie@ubuntu2004:/dev$

为服务设置 blkio 限制

当然,你也可以为服务设置BlockIOReadBandwidth参数。例如,使用set-property选项为 Apache Web 服务器设置它。在 Ubuntu 机器上,命令如下:

donnie@ubuntu2004:~$ sudo systemctl set-property apache2.service BlockIOReadBandwidth="/dev/sda 1M"

在 AlmaLinux 机器上,命令如下:

[donnie@localhost ~]$ sudo systemctl set-property httpd.service BlockIOReadBandwidth="/dev/sda 1M"

如果你想在服务文件中设置这个BlockIOReadBandwidth参数,有一个技巧需要知道。当你在命令行上设置时,你必须将/dev/sda 1M部分用一对双引号括起来。但在服务文件中设置时,你需要将/dev/sda 1M用双引号括起来。为了演示,让我们设置一个 FTP 服务器并对其设置blkio限制。在 Ubuntu 机器上,执行以下命令来安装 FTP 服务器:

donnie@ubuntu2004:~$ sudo apt install vsftpd

在 AlmaLinux 机器上,执行:

[donnie@localhost ~]$ sudo dnf install vsftpd

在任何一台机器上,通过以下操作编辑服务文件:

donnie@ubuntu2004:~$ sudo systemctl edit --full vsftpd

[Service]部分,添加新的参数,但不要使用双引号:

[Service]
. . .
.. .
BlockIOReadBandwidth=/dev/sda 1M

运行daemon-reload并重新启动vsftpd服务。您应该能看到新的设置出现在 cgroup 文件系统中:

donnie@ubuntu2004:/sys/fs/cgroup/blkio/system.slice/vsftpd.service$ cat blkio.throttle.read_bps_device 
8:0 1000000
donnie@ubuntu2004:/sys/fs/cgroup/blkio/system.slice/vsftpd.service$

这里有比我们可以在这里介绍的更多的资源管理指令。要了解更多,请参考systemd.resource-management的 man 页面。

在我们结束本章之前,让我们有点不敬地谈谈pam_limitsulimit,它们与 systemd 或 cgroups 完全无关。

理解 pam_limits 和 ulimit

在 cgroup 和 systemd 技术被发明之前,我们有其他方法来控制资源使用情况。这些方法仍然存在,并且我们可以使用它们来做一些 cgroups 做不到的事情。为了演示,让我们简要看一下这两种较旧的方法。

ulimit 命令

ulimit命令允许我们动态控制 shell 会话和由 shell 会话启动的任何进程的资源使用情况。让我们使用-a选项查看当前 shell 会话的默认设置:

图 12.12 – 默认 ulimit 设置

正如您所看到的,执行ulimit -a命令还会显示我们用于设置各种限制的选项开关。关键在于,您可以作为普通用户设置或降低限制,但如果要增加任何限制,则需要使用sudo特权。例如,假设我们想将任何新文件的大小限制为仅为 10 MB。我们将使用-f选项,并以 1024 字节块的数量指定文件大小。10 MB 相当于 10,240 个块,因此我们的命令如下所示:

donnie@ubuntu2004:~$ ulimit -f 10240
donnie@ubuntu2004:~$

新的限制会显示在ulimit -a的输出中:

donnie@ubuntu2004:~$ ulimit -a
. . .
. . .
file size               (blocks, -f) 10240
. . .
. . .

现在,看看当我尝试增加这个限制时会发生什么:

donnie@ubuntu2004:~$ ulimit -f 20000
-bash: ulimit: file size: cannot modify limit: Operation not permitted
donnie@ubuntu2004:~$

因此,普通用户可以设置以前未设置的限制,但是增加现有限制需要sudo特权。但是,您可以通过关闭终端窗口并打开新窗口或注销并重新登录来将所有内容重置为默认设置。然后,只需设置一个新的限制,使其为所需的任何内容。

现在,当我尝试创建一个大小为 10 MB 的文件时,一切正常:

donnie@ubuntu2004:~$ dd if=/dev/zero of=afile bs=1M count=10
10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.0440278 s, 238 MB/s
donnie@ubuntu2004:~$

但是当我尝试创建一个 11 MB 的文件时,情况就不那么顺利了:

donnie@ubuntu2004:~$ dd if=/dev/zero of=afile bs=1M count=11
File size limit exceeded (core dumped)
donnie@ubuntu2004:~$

对于需要测试新软件的开发人员或需要在 shell 脚本中设置资源限制的任何人来说,ulimit命令非常有用。要详细了解ulimit,请打开bash-builtins的 man 页面并搜索ulimit

接下来,让我们谈谈如何使用配置文件来设置限制。

pam_limits 模块

pam_limits模块是/etc/security/limits.conf文件的一部分,或通过在/etc/security/limits.d/目录中创建新的附加文件来实现。要了解其工作原理,打开/etc/security/limits.conf文件并查看已注释的示例。要获取更详细的说明,请查看limits.conf的 man 页面。

假设我们要阻止 Pogo 创建大于 20 MB 的任何文件。我们可以通过在 /etc/security/limits.conf 文件的底部添加一行来实现,内容如下:

. . .
. . .
#<domain>      <type>  <item>         <value>
#
. . .
. . .
pogo            hard    fsize           20480
# End of file

以 Pogo 用户身份登录,并让他尝试创建一个文件:

pogo@ubuntu2004:~$ dd if=/dev/zero of=afile bs=1M count=19
19+0 records in
19+0 records out
19922944 bytes (20 MB, 19 MiB) copied, 0.0989717 s, 201 MB/s
pogo@ubuntu2004:~$

一直重复这个命令,增加 count= 数量,直到出现错误。

好的,我想这一章的内容就差不多了。让我们结束这部分内容吧。

摘要

在这一章中,我们了解了使用 cgroups 版本 1 控制资源的基础知识。你在网络搜索中看到的很多信息已经过时并且有些混乱。我的目标是为你带来最新的信息,并以易于理解的方式呈现。

我们首先查看了 cgroups 版本 1 控制器,并简要解释了每一个。然后,我们展示了如何控制用户和服务的 CPU 使用、内存使用以及块设备带宽使用。最后,我们通过展示旧的、非 cgroup 的限制设置方法来结束,这种方法依然有用。

在下一章中,我们将探讨 cgroups 版本 2。到时见。

问题

  1. 你的计算机有六个 CPU 核心。如果你想限制 Vicky 每个 CPU 核心的使用率为 16.66%,那么她的 CPUQuota 设置应为多少?

    A. 16.66%

    B. 33.00%

    C. 100%

    D. 200%

  2. 根据 systemd.resource-control 手册页,以下哪个指令代表限制某人内存使用的最现代方法?

    A. MemoryLimit

    B. MemoryMax

    C. LimitMemory

    D. MaxMemory

  3. --runtime 选项对于 systemctl set-property 命令的作用是什么?

    A. 它使新的设置变为永久。

    B. 没有任何影响,因为它已经是默认行为。

    C. 它使新的设置变为临时。

    D. 它使得命令运行更快。

  4. 以下哪个关于 CPU 负载平均值的说法是正确的?

    A. 拥有更多 CPU 核心的机器可以处理更高的 CPU 负载平均值。

    B. CPU 负载平均值与机器拥有多少个 CPU 核心无关。

    C. 过度的内存使用不会导致 CPU 负载平均值过高。

    D. 高 CPU 负载平均值对任何机器没有影响。

答案

  1. C

  2. B

  3. C

  4. A

进一步阅读

第十三章:理解 cgroup Version 2

在本章中,我们将讨论cgroup Version 2。我们将看到它与cgroups Version 1有何不同,以及它如何在 Version 1 的基础上进行改进。之后,我们将简要了解如何使用它。最后,我们将把AlmaLinux机器转换为使用 cgroup Version 2。学习如何使用 cgroup Version 2 将对新软件开发者以及希望为未来做好准备的Linux管理员非常有帮助。

顺便提一下,章节标题中的这个并不是打字错误。Version 2 的一个变化是官方名称的变化。所以,我们有cgroups Version 1 和cgroup Version 2。听起来很奇怪,但是真的。(我之前没有解释过这个,因为我不想引起更多混淆。)

本章中的具体主题包括:

  • 理解 Version 2 的需求

  • 理解 Version 2 的改进

  • 设置无根容器的资源限制

  • 理解cpuset

  • 将 RHEL 8 类发行版转换为 cgroup Version 2

介绍完毕,让我们开始吧。

技术要求

这次,我们将使用一台Fedora 虚拟机,该虚拟机配置为尽可能使用更多的 CPU 核心和内存。(我的虚拟机仍然设置为使用四个 CPU 核心和 8GB 内存。)因此,下载你喜欢的 Fedora 版本,并从中创建虚拟机。

对于理解 cpuset部分,最好使用至少有两个物理 CPU 的主机。我知道不是每个人都有机会使用这样的机器,但没关系。我有这样一台机器,所以我可以向你展示你需要看到的内容。

我们还将使用 AlmaLinux 机器进行几次简短的演示。

好的,让我们开始吧。

查看以下链接以观看《代码实战》视频:bit.ly/3xJNcDx

理解 Version 2 的需求

尽管 cgroups Version 1 已经相当不错,但它确实存在一些相当严重的缺陷。我们来快速看一下。

Version 1 的复杂性

首先,Version 1 拥有过多的资源控制器和每个控制器上过多的属性。很少有人会使用我们在第十二章中介绍的那三个主要的控制器,资源使用控制通过 cgroups Version 1 来实现。Version 2 去除了一些不必要的控制器。

Version 1 层次结构的复杂性也过高,这使得它的使用有些混乱,并且可能影响性能。要理解我的意思,回想一下我们在 Version 1 的cgroup文件系统中看到的内容。你会发现,每个资源控制器都有自己的子目录,就像我们在这里看到的那样:

图 13.1 – Ubuntu 上 Version 1 的资源控制器

第十二章,《使用 cgroups Version 1 控制资源使用》中,我们也看到,当我们为 Vicky 设置CPUQuota时,它出现在她的user-1001.slice子目录下,这个目录位于cpu/user.slice子目录下,像这样:

vicky@ubuntu2004:/sys/fs/cgroup/cpu/user.slice/user-1001.slice$ cat cpu.cfs_quota_us 
200000
vicky@ubuntu2004:/sys/fs/cgroup/cpu/user.slice/user-1001.slice$

然后,当我们设置MemoryMax限制时,它出现在memory子目录下,像这样:

vicky@ubuntu2004:/sys/fs/cgroup/memory/user.slice/user-1001.slice$ cat memory.max_usage_in_bytes 
30994432
vicky@ubuntu2004:/sys/fs/cgroup/memory/user.slice/user-1001.slice$

好的,你绝对猜不到,当我们设置 Vicky 的BlockIOReadBandwidth参数时发生了什么。没错,它出现在blkio子目录下,像这样:

vicky@ubuntu2004:/sys/fs/cgroup/blkio/user.slice/user-1001.slice$ cat blkio.throttle.read_bps_device 
8:0 1000000
vicky@ubuntu2004:/sys/fs/cgroup/blkio/user.slice/user-1001.slice$

所以你看,Vicky 的设置在三个不同的地方,这意味着操作系统必须查看所有三个地方才能获取完整的设置。

注意

我不小心用了 Vicky 的登录窗口,而不是我自己的截图工具,不过没关系。这说明 Vicky 可以查看她自己 cgroup 文件中的设置。当然,她无法更改设置,因为她没有正确的 root 权限。

Version 1 属性文件名

Version 1 的另一个问题是,不同资源控制器的属性文件没有统一的命名规范。例如,在 Version 1 中,设置MemoryMax会将值放入memory.max_usage_in_bytes文件中,就像我们在这里为 Vicky 看到的那样:

donnie@ubuntu2004:/sys/fs/cgroup/memory/user.slice/user-1001.slice$ cat memory.max_usage_in_bytes 
30789632
donnie@ubuntu2004:/sys/fs/cgroup/memory/user.slice/user-1001.slice$

然而,Vicky 的CPUQuota设置出现在cpu.cfs_quota_us文件中,正如我们在这里看到的:

donnie@ubuntu2004:/sys/fs/cgroup/cpu/user.slice/user-1001.slice$ cat cpu.cfs_quota_us 
200000
donnie@ubuntu2004:/sys/fs/cgroup/cpu/user.slice/user-1001.slice$

正如我们稍后会看到的,命名约定在第 2 版中要一致得多。

好的,让我们从真正的根本问题入手,讨论一下无 root 容器

不支持无 root 权限的容器

正如你在第五章《创建和编辑服务》中看到的,我们可以使用podman创建并运行Docker容器,而不需要 root 权限或 docker 组的成员资格。然而,在 cgroups Version 1 中,非特权用户无法在创建容器时设置运行时资源限制。例如,让我们去 AlmaLinux 机器,并为我的朋友 Pogo 创建一个新用户账户,操作如下:

[donnie@localhost ~]$ sudo useradd pogo
[donnie@localhost ~]$ sudo passwd pogo

看看这个可怜的家伙试图创建一个 50%的CPUQuota容器时发生了什么:

[pogo@localhost ~]$ podman run -it --cpu-period=100000 --cpu-quota=50000 ubuntu /bin/bash
Error: OCI runtime error: container_linux.go:367: starting container process caused: process_linux.go:495: container init caused: process_linux.go:458: setting cgroup config for procHooks process caused: cannot set cpu limit: container could not join or create cgroup
[pogo@localhost ~]$

唉,可怜的 Pogo 没有 root 权限。所以,他可以创建和运行podman容器,但无法为它们设置任何资源限制。

注意

事实上,在 cgroups Version 1 中,非特权用户确实可以为无 root 权限的podman容器设置运行时资源限制。但这需要你将此权限委派给非 root 用户。在 cgroups Version 1 中,这构成了安全隐患,因为它可能允许某人创建一个容器,进而冻结你的系统。所以,我们不打算这么做(稍后我们会详细讨论委派问题)。

现在,让我们来对比一下在运行纯 cgroup Version 2 环境的 Fedora 机器上看到的情况。

理解 cgroup Version 2 中的改进

版本 2 更加简化,易于理解。在写这篇文章时,我知道只有 ArchDebian 11 和 Fedora 这三个 Linux 发行版默认运行 cgroup 版本 2(到你读这篇文章时,这个情况可能会有所变化)。

注意

你可以将 RHEL 8 类型的发行版,例如 AlmaRocky,转换为纯版本 2 设置。不幸的是,RHEL 类型的发行版使用的是版本 2 的较旧实现,其中一些我们需要的资源控制器仍未启用。因此,为了查看我们需要看到的所有内容,我们将使用 Fedora。

首先,让我们登录到 Fedora 机器,并为我的朋友 Pogo 创建一个用户账户(Pogo 是那只从我家的猫门进来、晚上来吃猫粮的超棒负鼠——是的,是真的。)。然后,让 Pogo 从远程终端登录(请注意,在 Fedora 上,你可能需要先启动并启用 sshd 服务)。在你本地的终端中,查看 cgroup 文件系统,内容如下所示:

图 13.2 – Fedora 上的 cgroup 文件系统

我们在这里看到的属性文件是用于全局设置的,目前我们暂时不关心这些。真正需要你注意的是 system.sliceuser.slice 子目录下的内容。我们先看一下 user.slice 子目录。

user.slice 子目录下,你会看到许多可以在用户切片级别设置的文件。底部,我们看到了 Pogo 和我自己的子目录,正如这里所示:

[donnie@fedora user.slice]$ ls -l
total 0
-r--r--r--. 1 root root 0 Jul 30 16:55 cgroup.controllers
. . .
. . .
-rw-r--r--. 1 root root 0 Jul 30 16:55 pids.max
drwxr-xr-x. 5 root root 0 Jul 30 17:24 user-1000.slice
drwxr-xr-x. 4 root root 0 Jul 30 17:09 user-1001.slice
[donnie@fedora user.slice]$

每个用户切片子目录都包含所有资源控制器的属性文件,如我们在这里看到的:

图 13.3 – 用户-1001.slice 的资源控制器

所以现在,特定用户的所有适用设置都将包含在该用户的 user slice 目录中。操作系统现在只需要在一个地方查找该用户的所有设置。

接下来,让 Pogo 从远程终端登录。然后,在你自己的终端窗口中,为 Pogo 设置 CPUQuota。好消息是,执行此操作的命令和版本 1 完全相同。如果你不记得了,命令是:

[donnie@fedora ~]$ sudo systemctl set-property user-1001.slice CPUQuota=40%
[sudo] password for donnie: 
[donnie@fedora ~]$

然后,执行 daemon-reload。完成后,查看 Pogo 用户切片目录中的 cpu.max 文件,其内容应该如下所示:

[donnie@fedora ~]$ cd /sys/fs/cgroup/user.slice/user-1001.slice/
[donnie@fedora user-1001.slice]$ cat cpu.max
40000 100000
[donnie@fedora user-1001.slice]$

40000 表示 40% 的 CPUShare,而 100000 表示测量 CPUShare 的时间间隔。默认的时间设置是 100 毫秒(你可以更改这个时间间隔,但你很可能永远不需要这样做)。

你也可以像设置版本 1 一样设置 Pogo 的内存限制,如我们在这里所示:

[donnie@fedora user-1001.slice]$ sudo systemctl set-property user-1001.slice MemoryMax=1G
[sudo] password for donnie: 
[donnie@fedora user-1001.slice]$

这次,设置出现在 Pogo 的 memory.max 文件中,如我们在这里看到的:

[donnie@fedora user-1001.slice]$ cat memory.max
1073741824
[donnie@fedora user-1001.slice]$

现在,理解这个 MemoryMax 设置是一个硬限制。换句话说,Pogo 绝对不能使用超过 MemoryMax 分配的内存。如果你查看 systemd.resource-control 的 man 页面,你会看到版本 2 中可用的一些其他选项,而版本 1 中没有。(请注意,该 man 页面总是将 cgroup 版本 2 称为 统一控制组层次结构。)其中一个参数是 MemoryHigh,它更像是一个软限制。如果不可避免,MemoryHigh 允许 Pogo 超过其内存分配,但他的进程会被限制,直到他的内存使用降回到分配的范围内。这使得系统更容易处理任何给定进程或用户的内存使用临时峰值。

版本 2 还具有 MemoryLowMemoryMin 参数,当受保护进程的空闲内存降至指定阈值时,这些参数会导致进程从未受保护进程中回收内存。如果你想控制交换内存的使用,版本 2 允许你通过 MemorySwapMax 参数来实现。

设置 block I/O 使用限制有些不同,因为参数名称已经发生了变化。为了限制 Pogo 的读取带宽,我们首先使用 df 查看我们有哪些驱动器设备,如下所示:

[donnie@fedora ~]$ df -h | grep -v tmpfs
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda2        21G  2.6G   18G  13% /
/dev/sda2        21G  2.6G   18G  13% /home
/dev/sda1       976M  256M  654M  29% /boot
[donnie@fedora ~]$

Fedora 的桌面版本现在默认使用 btrfs 文件系统,这就是为什么我们看到的是常规的驱动器分区而不是逻辑卷。(使用 btrfs 时无需使用逻辑卷,因为它具有内置的驱动器池机制。)如果你使用的是 ext4 和逻辑卷,情况就不一样了。不管怎样,我们看到 /home/ 目录挂载在 /dev/sda 驱动器上,当然这就是 Pogo 的主目录所在。(正如我们在版本 1 中看到的,你可以对整个驱动器设置速率限制,但不能对该驱动器的特定分区设置限制。)

现在我们将使用 IOReadBandwidthMax 参数来限制 Pogo 文件传输的速率,像这样:

[donnie@fedora ~]$ sudo systemctl set-property user-1001.slice IOReadBandwidthMax="/dev/sda 1M"
[sudo] password for donnie: 
[donnie@fedora ~]$

请注意,由于 /dev/sda 1M 参数中有空格,因此在从命令行设置时,必须将其用一对双引号("")括起来。

接下来,查看 Pogo 用户切片目录中的 io.max 文件,应该是这样的:

[donnie@fedora user-1001.slice]$ cat io.max
8:0 rbps=1000000 wbps=max riops=max wiops=max
[donnie@fedora user-1001.slice]$

在这里,我们看到了使用版本 2 的另一个好处。与版本 1 中有四个单独的属性文件来存放四个可用的参数设置不同,版本 2 将 IOReadBandwidthMaxIOWriteBandwidthMaxIOReadIOPSMaxIOWriteIOPSMax 设置放在了一个文件中。

另外,注意我们在 io.max 文件中看到的行首的 8:0,它表示整个 sda 驱动器的主设备号和次设备号,如下所示:

[donnie@fedora dev]$ pwd
/dev
[donnie@fedora dev]$ ls -l sd*
brw-rw----. 1 root disk 8, 0 Jul 31 14:30 sda
brw-rw----. 1 root disk 8, 1 Jul 31 14:30 sda1
brw-rw----. 1 root disk 8, 2 Jul 31 14:30 sda2
[donnie@fedora dev]$

好的,如果你真的想要,你可以像在 第十二章 中为 Vicky 所做的那样,尝试使用 stress-ng 来测试 Pogo,使用 cgroups 版本 1 控制资源使用,但我不会在这里重复相关的操作步骤。

设置服务限制时需要知道的主要事项是,每个系统服务在 /sys/fs/cgroup/system.slice/ 目录下都有自己的子目录,正如我们在这里看到的:

[donnie@fedora system.slice]$ pwd
/sys/fs/cgroup/system.slice
[donnie@fedora system.slice]$ ls -l
total 0
drwxr-xr-x. 2 root root 0 Jul 31 14:30  abrtd.service
drwxr-xr-x. 2 root root 0 Jul 31 14:30  abrt-journal-core.service
drwxr-xr-x. 2 root root 0 Jul 31 14:30  abrt-oops.service
drwxr-xr-x. 2 root root 0 Jul 31 14:30  abrt-xorg.service
drwxr-xr-x. 2 root root 0 Jul 31 14:30  alsa-state.service
drwxr-xr-x. 2 root root 0 Jul 31 14:31  atd.service
drwxr-xr-x. 2 root root 0 Jul 31 14:30  auditd.service
. . .
. . . 

在这些子目录中,你会看到与 Pogo 相同的属性文件。此外,设置服务限制的过程与 Version 1 相同,所以我也不会重复这些内容。

注意

请注意,一些你习惯在 cgroups Version 1 下使用的参数,在 cgroups Version 2 中已经被重命名。具体来说,Version 1 中的 CPUSharesStartupCPUSharesMemoryLimit 参数已经分别被 CPUWeightStartupCPUWeightMemoryMax 替代。此外,所有以 BlockIO 前缀命名的 Version 1 参数,已被以 IO 前缀命名的参数替代。

好的,现在我们了解了 cgroup Version 2 文件系统,让我们看看能否让 Pogo 在无根容器上设置一些资源限制。

在无根容器上设置资源限制

刚才,我向你介绍了 委派 的概念。通常,你需要根权限才能设置任何资源限制。然而,你可以将这项工作委派给非特权用户。最好的消息是,与 cgroups Version 1 中的委派不同,cgroups Version 2 中的委派是完全安全的。

要查看默认设置,请打开 /lib/systemd/system/user@.service 文件,并在 [Service] 部分中查找 Delegate= 行。相关行应该像这样:

[Service]
. . .
. . .
Delegate=pids memory
. . .
. . .

默认情况下,Fedora 只允许非特权用户设置内存和最大运行进程数的资源限制。我们需要编辑这个配置,以包括 cpucpusetio 资源控制器,像这样:

[donnie@fedora ~]$ sudo systemctl edit --full user@.service

编辑 Delegate= 行,使其看起来像这样:

Delegate=pids memory io cpu cpuset

保存文件并执行 daemon-reload。请注意,如果有用户已登录,他们可能需要注销并重新登录才能使其生效。

保持 Pogo 原来的登录窗口打开,然后为他再打开一个新的窗口。他将在一个窗口中创建一个容器,在第二个窗口中查看该容器信息。让 Pogo 创建一个 Ubuntu 容器,像这样:

[pogo@fedora ~]$ podman run -it --cpu-period=100000 --cpu-quota=50000 ubuntu /bin/bash
root@207a59e45e9b:/#

Pogo 正在设置一个 CPUQuota50% 的限制,时间间隔为 100 毫秒。在 Pogo 的另一个登录窗口中,让他查看他的容器信息。他会首先执行 podman ps,像这样:

[pogo@fedora ~]$ podman ps
CONTAINER ID  IMAGE                            COMMAND     CREATED         STATUS             PORTS       NAMES
207a59e45e9b  docker.io/library/ubuntu:latest  /bin/bash   55 minutes ago  Up 55 minutes ago              funny_zhukovsky
[pogo@fedora ~]$

Pogo 没有为这个容器指定名称,因此 podman 随机分配了名称 funny_zhukovsky。(记住,Pogo 是一只负鼠,所以不要因为他忘记指定名称而太苛责他。)现在,让 Pogo 使用生成的容器名称来检查这个容器的内部工作:

[pogo@fedora ~]$ podman inspect funny_zhukovsky

这里输出很多内容,但你只需要关注两行。继续向下滚动,你应该能找到它们。它们应该像这样:

"CpuPeriod": 100000,
"CpuQuota": 50000,

到目前为止,一切都还好。但问题是,这个容器的属性文件深藏在 cgroup 文件系统中,难以找到。幸运的是,Pogo 比我想象的更聪明,所以他找到了一个作弊的方法。他知道50000这个文本字符串只会出现在他用户切片目录下的一个属性文件中,因此他使用grep命令来找到它,像这样:

[pogo@fedora ~]$ cd /sys/fs/cgroup/user.slice/user-1001.slice/
[pogo@fedora user-1001.slice]$ grep -r '50000' *
user@1001.service/user.slice/libpod-207a59e45e9b14c3397d9904b41ba601dc959d85962e6ede45a1b54463ae731b.scope/container/cpu.max:50000 100000
[pogo@fedora user-1001.slice]$

最后,Pogo 找到了属性文件:

[pogo@fedora container]$ pwd
/sys/fs/cgroup/user.slice/user-1001.slice/user@1001.service/user.slice/libpod-207a59e45e9b14c3397d9904b41ba601dc959d85962e6ede45a1b54463ae731b.scope/container
[pogo@fedora container]$ cat cpu.max
50000 100000
[pogo@fedora container]$

这就是关于无根容器的全部内容。接下来,我们来讨论cpuset

理解 cpuset

当你处理一个运行大量容器和进程的服务器时,有时将某个容器或进程分配给特定的 CPU 核心或一组 CPU 核心会更有益。对于具有多个物理 CPU 的机器,分配内存节点也可能会有所帮助。为了理解我说的内容,可以在你的 Fedora 机器上安装numactl,像这样:

[donnie@fedora ~]$ sudo dnf install numactl

使用-H选项查看硬件列表,像这样:

[donnie@fedora ~]$ numactl -H
available: 1 nodes (0)
node 0 cpus: 0 1 2 3
node 0 size: 7939 MB
node 0 free: 6613 MB
node distances:
node   0 
  0:  10 
[donnie@fedora ~]$

有一个NUMA节点,即节点 0,并且它与四个 CPU 相关联。实际上,只有一个 CPU,它有四个 CPU 核心。我们还可以看到分配给这个节点的内存量。

所以,现在你可能会说,但是 Donnie,什么是 NUMA?我为什么要关心它? 好吧,NUMA 代表 非一致性内存访问。它与操作系统如何处理具有多个物理 CPU 的机器上的内存有关。在只有一个 CPU 的系统上,比如你的 Fedora 虚拟机,NUMA 对我们没有任何作用,因为只有一个内存节点。在具有多个 CPU 的机器上,每个 CPU 都有自己关联的内存节点。例如,看看这是我一块废旧主板的照片:

图 13.4 – 一块双 CPU 主板

有两个 CPU 插槽,每个插槽都有自己的一组内存插槽。每组内存构成一个 NUMA 节点。现在,我们来看一下我正在运行的多 CPU 系统,这是一个旧的惠普工作站,配备了两颗四核AMD Opteron处理器,并运行Fedora 34

[donnie@fedora-teaching ~]$ numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 2 4 6
node 0 size: 7959 MB
node 0 free: 6982 MB
node 1 cpus: 1 3 5 7
node 1 size: 8053 MB
node 1 free: 7088 MB
node distances:
node   0   1 
  0:  10  20 
  1:  20  10 
[donnie@fedora-teaching ~]$

这次,我们看到了两个 NUMA 节点。偶数号的 CPU 核心被分配给节点 0,而奇数号的 CPU 核心被分配给节点 1

默认情况下,大多数进程在启动时会随机选择一个 CPU 核心或一组 CPU 核心运行。有时,操作系统可能会将正在运行的进程从一个核心或一组核心移到另一个核心。像我这里的普通工作站,这并不重要。但如果是在运行大量进程的服务器上,这可能很重要。你可能通过将某些进程分配到专用的 CPU 核心和 NUMA 节点来提高效率和性能。如果你使用的是 cgroups 版本 1,那么你需要做很多繁琐的工作来使其生效,因为版本 1 的 cpuset 控制器无法直接与 systemd 配合使用。而使用 cgroup 版本 2,这就轻松多了。你只需要使用 systemctl set-property 来设置 AllowedCPUs=AllowedMemoryNodes= 参数,或者在服务文件的 [Service] 部分中进行设置。

现在,即使你只有一个 CPU 的 Fedora 虚拟机,你仍然可以尝试这样做,看看效果如何。首先,通过以下命令安装 Apache 网络服务器:

[donnie@fedora ~]$ sudo dnf install httpd
[donnie@fedora ~]$ sudo systemctl enable --now httpd

接下来,将 Apache 服务分配给 CPU 核心 02,像这样:

sudo systemctl set-property httpd.service AllowedCPUs="0 2"

提醒

像之前一样,记得将任何包含空格的参数集用一对双引号括起来。

现在,假设这台虚拟机有多个 NUMA 节点,将 Apache 服务分配给 NUMA 节点 0,像这样:

sudo systemctl set-property httpd.service AllowedMemoryNodes=0

这两个命令将影响 cpuset.cpuscpuset.mems 属性文件,如你所见:

[donnie@fedora httpd.service]$ pwd
/sys/fs/cgroup/system.slice/httpd.service
[donnie@fedora httpd.service]$ cat cpuset.cpus
0,2
[donnie@fedora httpd.service]$ cat cpuset.mems
0
[donnie@fedora httpd.service]$

在我可靠的双 CPU 惠普电脑上,我改动了 httpd.service 文件,添加了这两个参数。新增的两行如下:

. . .
. . .
[Service]
. . .
. . .
AllowedCPUs=0 2
AllowedMemoryNodes=0
. . .
. . .

所以,在这两个例子中,我允许 Apache 使用 CPU 核心 0 和 2,它们都与 NUMA node 0 相关联。

提示

你可以用逗号或空格分隔列表中的核心编号,或者使用连字符(-)列出 CPU 核心的范围。另外,注意,当你在单元文件中添加 AllowedCPUs= 参数时,不需要将 0 2 括在双引号中。

在执行 daemon-reload 并重新启动 Apache 服务后,我们应该能看到相应的属性文件出现在 /sys/fs/cgroup/system.slice/httpd.service/ 目录下。再看一下 cpuset.cpus 文件的样子:

[donnie@fedora-teaching httpd.service]$ cat cpuset.cpus
0,2
[donnie@fedora-teaching httpd.service]$

很好。Apache 正在 02 CPU 核心上运行,正如我们所希望的那样。现在,让我们查看 cpuset.mems 文件:

[donnie@fedora-teaching httpd.service]$ cat cpuset.mems
0
[donnie@fedora-teaching httpd.service]$

再次确认,这正是我们想要看到的。Apache 现在只能使用 NUMA node 0。因此,得益于 cgroup 版本 2,我们通过最小的努力就实现了酷炫的效果。

注意

NUMA 并不意味着在一个 CPU 上运行的进程无法访问另一个 CPU 所在 NUMA 节点的内存。默认情况下,任何进程都可以访问所有 NUMA 节点上的所有系统内存。你可以使用 AllowedMemoryNodes 参数来更改这一点。

所以,现在你可能在想,"我能在我的 RHEL 8 类型机器上使用 cgroup 版本 2 吗?"。好吧,我们来看看。

将 RHEL 8 类型的发行版转换为 cgroup 版本 2

将 Red Hat Enterprise Linux 8 类型的发行版转换为 cgroup Version 2 是一件简单的事情。第一步是编辑你 AlmaLinux 机器上的 /etc/default/grub 文件。找到以 GRUB_CMDLINE_LINUX= 开头的行,并在该行的末尾添加 systemd.unified_cgroup_hierarchy=1。整个行现在应该看起来像这样:

GRUB_CMDLINE_LINUX="crashkernel=auto resume=/dev/mapper/vl-swap rd.lvm.lv=vl/root rd.lvm.lv=vl/swap rhgb quiet systemd.unified_cgroup_hierarchy=1"

接下来,像这样重建 GRUB 配置:

[donnie@localhost ~]$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg

重启机器后,查看 /sys/fs/cgroup/ 目录。你现在应该会看到与 Fedora 机器上相同的文件系统。不过,如果你不能让之前的实验都成功运行,不要太失望。这是因为 RHEL 8 类型的发行版都使用较旧版本的 Linux 内核,尚未启用所有 cgroup 资源控制器。它们会在 RHEL 8 发行版中启用吗?也许吧。Red Hat 的政策是为每个主要的 RHEL 版本在整个十年的生命周期内坚持使用一个固定的内核版本。因此,所有 RHEL 8 类型的发行版将一直停留在旧的内核版本 4.18,直到它们在 2029 年达到生命周期终结。Red Hat 有时会将新内核的特性回溯到它们的旧版 RHEL 内核中,但不能保证会对任何更新的 cgroup Version 2 代码做同样的事。无论如何,一旦你在 AlmaLinux 机器上看到所需的内容后,可以删除你在 grub 文件中所做的修改,并重建 GRUB 配置。这将把机器转换回使用 cgroup Version 1。

那么,你已经了解足够的 cgroup 了吗?希望还没有,因为这是一个值得深入学习的内容。不过,还有很多内容需要我们继续讲解,接下来就让我们继续吧。

总结

在本章中,我们学习了关于 cgroup Version 2 的许多内容。我们从讨论 cgroups Version 1 的不足开始,并介绍了 cgroup Version 2 是如何改进的。接着,我们探讨了如何允许非特权用户在其容器上设置资源限制,以及如何使用 cpuset 资源控制器。最后,我们简要了解了如何将 RHEL 8 类型的机器转换为使用 cgroup Version 2。

再次提醒,你一定在想,如果 cgroup Version 2 如此优秀,为什么它还没有被广泛采用呢?其实是因为某些关键程序和服务,尤其是容器化服务,仍然是硬编码为使用 Version 1。幸运的是,情况正在改善,可以大胆预测,Version 2 会在我们有生之年成为标准。

好的,这就结束了本书的第二部分。让我们从第三部分开始,讨论 journald。下次见!

问题

  1. 以下哪一项说法是正确的?

    A. 你可以在 cgroup Version 1 下安全地使用 podman 设置无根容器的资源限制。

    B. 你可以在 cgroup Version 2 下安全地使用 podman 设置无根容器的资源限制。

    C. 你不能为 podman 容器设置资源限制。

    D. 设置无根podman容器的资源限制无需特殊权限。

  2. MemoryMaxMemoryHigh有什么区别?

    A. MemoryMax是硬性限制,MemoryHigh是软性限制。

    B. MemoryHigh是硬性限制,MemoryMax是软性限制。

    C. 它们都执行相同的操作。

    D. 它们都没有任何作用。

  3. 以下哪项关于委托的说法是正确的?

    A. 对于 cgroup 版本 1 和 cgroup 版本 2 都是完全安全的。

    B. 仅对 cgroup 版本 1 是安全的。

    C. 使用委托永远不安全。

    D. 仅对 cgroup 版本 2 是安全的。

  4. 将 RHEL 8 类型系统转换为 cgroup 版本 2 的第一步是什么?

    A. 编辑/etc/grub.cfg文件。

    B. 编辑/etc/default/grub文件。

    C. 编辑/boot/grub2/grub.cfg文件。

    编辑/boot/grub文件。

答案

  1. B

  2. A

  3. D

  4. B

进一步阅读

一篇关于 cgroup 版本 2 的 Red Hat 博客文章:

www.redhat.com/en/blog/world-domination-cgroups-rhel-8-welcome-cgroups-v2

一篇Oracle博客文章,讲述为什么版本 2 比版本 1 更好:

blogs.oracle.com/linux/post/cgroup-v2-checkpoint

比较版本 1 和版本 2:

chrisdown.name/talks/cgroupv2/cgroupv2-fosdem.pdf

使用 cgroup 版本 2 进行无根 Docker 容器管理:

rootlesscontaine.rs/getting-started/common/cgroup2/

cgroup v2 在容器中的当前采用状态:

medium.com/nttlabs/cgroup-v2-596d035be4d7

第三部分:日志管理、时间管理、网络管理与启动

在本节中,我们将探讨 systemd 生态系统中的日志管理、时间管理和网络组件,并将其与非 systemd 组件进行对比。接着,我们将讨论如何使用 GRUB2 和 systemd-boot 启动加载程序,最后介绍如何使用 systemd-logind 和 polkit 来帮助管理用户。

本书的这一部分包含以下章节:

  • 第十四章**,使用 journald

  • 第十五章**,使用 systemd-networkd 和 systemd-resolved

  • 第十六章**,理解 systemd 的时间管理

  • 第十七章**,理解 systemd 和启动加载程序

  • 第十八章**,理解 systemd-logind

第十四章:使用 journald

本章将关注一种新的日志记录方式。虽然 journald 日志系统不是 systemd 的 init 系统的一部分,但它是 systemd 生态系统的一部分。journald 系统相较于旧的 rsyslog 系统有其优势。然而,仍有几个主要原因使得我们还没有完全过渡到 journald。尽管如此,journald 仍然是一个重要工具,可以帮助繁忙的 Linux 管理员轻松查看系统发生了什么。

本章的具体主题包括:

  • 理解 rsyslog 的优缺点

  • 理解 journald 的优缺点

  • 理解 Ubuntu 中的 journald

  • 使用 journalctl

  • 密封 journald 日志文件以确保安全

  • 使用 journald 设置远程日志记录

技术要求

本章所需的只是你正常的 Ubuntu Server 20.04 和 Alma Linux 8 虚拟机。(你需要这两者,因为 journald 在每个系统中的实现方式不同。)现在,我们先来看看旧版的 rsyslog

查看以下链接以观看 Code in Action 视频:bit.ly/3dbJmJR

理解 rsyslog 的优缺点

Fedora 是第一个将 rsyslog 作为默认日志系统的 Linux 发行版,早在 2007 年就推出了这一功能。它相比旧版 syslog 有不少改进,最终取代 syslog 成为 Linux、Unix 及类 Unix 操作系统上的标准日志系统。尽管现在有了 journaldrsyslog 仍然在我们身边,正如我们接下来所看到的。

rsyslog 的一个最佳特点也恰恰是它最大的弱点。也就是它以纯文本格式存储日志文件。从某种角度来说,这样很好,因为你可以使用常规的文本搜索和查看工具来查看日志文件,找到你需要的信息。less、head、tail、awk 和 grep 工具都是你处理这些纯文本日志文件时的好帮手。这也使得编写 shell 脚本来自动提取和解析信息变得非常简单。

但是使用纯文本日志文件还是存在一些问题。第一个问题是纯文本文件可能变得相当大,并最终消耗大量磁盘空间。为了解决这个问题,所有 Linux 发行版都配有 logrotate 系统,它会自动删除所有日志文件,只保留过去四周的文件。如果你需要保留比这更长时间的日志文件,你就需要编辑 /etc/logrotate.conf 文件,或者在 logrotate 自动删除它们之前将旧日志文件转移到其他地方。这也可能让查找变得有些尴尬。如果你在日志轮换发生后不久就搜索当前的日志文件,你会发现文件大部分是空的。然后,你就得通过归档文件查找你需要的内容。

第二个问题,至少根据 journald 的开发者所说,是 rsyslog 日志文件没有内置的方式来结构化显示,如以下 AlmaLinux 消息文件中的片段所示:

Jul 30 16:31:02 localhost rsyslogd[1286]: [origin software="rsyslogd" swVersion=
"8.1911.0-7.el8_4.2" x-pid="1286" x-info="https://www.rsyslog.com"] rsyslogd was
 HUPed
Jul 30 17:10:55 localhost journal[3086]: Could not delete runtime/persistent state file: Error removing file /run/user/1000/gnome-shell/runtime-state-LE.:0/screenShield.locked: No such file or directory
Jul 30 17:10:56 localhost NetworkManager[1056]: <info>  [1627679456.0025] agent-manager: agent[24ffba3d1b1cfac7,:1.270/org.gnome.Shell.NetworkAgent/1000]: agent registered
Jul 30 17:26:17 localhost systemd[1]: Starting dnf makecache...
Jul 30 17:26:17 localhost dnf[4951]: Metadata cache refreshed recently.
Jul 30 17:26:17 localhost systemd[1]: dnf-makecache.service: Succeeded.

你会看到这里每一条日志条目都以 journaldrsyslog 兼容格式显示日志数据开始。然而,journald 还包括一个 API,允许开发者为日志消息定义自定义字段,journalctl 工具则允许你以多种方式查看日志数据。

反对 rsyslog 的另一个论点是它的安全性。任何黑客如果突破系统并获得 root 权限,就可以轻松篡改纯文本日志文件,删除任何有关他或她恶意行为的记录。理论上,journald 创建的二进制日志文件更难被攻击者篡改。不过,这不意味着不可能吗?嗯,也许不是,因为稍后我们将看到如何验证 journald 的日志文件是否被篡改。另外,lastlogutmpwtmpbtmp 这些二进制日志文件在 Linux 中已经存在很多年,而且是可以被篡改的。可能稍微难一些,但并非不可能。

rsyslog 的最终问题是没有内置的搜索功能。是的,能够使用像 awk、grep 和 less 的内建搜索功能来搜索文本字符串或文本字符串模式是很好的。但如果有一个内置的功能可以让这些搜索变得更容易,不是很好吗?正如我们很快将看到的,journald 提供了这种能力。

我必须提到,并不是每个人都认为 rsyslog 不具备搜索和格式化功能是一个问题。journald 的开发者们当然认为这是一个问题。但很多文本处理和搜索工具几乎在每个 Linux 发行版上都已预装,使用起来也不难。即便是一个 shell 脚本初学者,也能轻松编写一个脚本,自动化从纯文本日志文件中查找和格式化相关信息的过程。例如,几年前,我写了一个这样的 shell 脚本,帮助一个朋友。(它是用来处理 Apache 日志文件的,但原则上仍然适用。)无论如何,你可以在这里查看我写的相关文章:

beginlinux.com/blog/2009/08/detect-cross-site-scripting-attacks-with-a-bash-shell-script/

(文章顶部写着是 Mike 发布的,但如果你向下滚动,你会看到是我写的。)

话虽如此,我还应该提到,有些事情仍然用纯文本文件处理起来有点困难,比如将它们转换为 JSON 格式。正如我们稍后将看到的那样,journald 在这方面要好得多。

rsyslog的另一个优点是,非常容易设置一个中央的rsyslog服务器,可以从网络上的其他计算机接收日志文件。当然,配置服务器以将来自不同机器的文件分离到自己的日志文件集中有点麻烦,但是一旦你知道如何做,它并不复杂。

理解journald的利弊

rsyslog相比,journald将其日志文件存储为二进制格式。这使我们能够在更小的磁盘空间中存储更多的数据,从而减少了不断轮换日志文件的需求。减少轮换需求使我们能够长期保留日志文件,而不必担心将其移动到其他位置。

使用二进制文件还可以增加一点安全性。攻击者更难改变二进制文件,并且还有一种方法可以查看文件是否被更改。

journalctl实用程序具有内置的过滤和查看功能。我们甚至可以以 JSON 格式查看日志信息,这样可以更轻松地将日志数据导出到其他日志解析程序中。

关于journald的另一个很酷的事情是,它将系统日志文件和用户日志文件分开存储。每个用户都有自己的一套日志文件。具有管理员权限的用户可以查看系统和所有用户的文件,非特权用户只能查看自己的日志文件。而对于rsyslog,只有具有管理员权限的用户才能查看这些日志文件中的任何一个。

不幸的是,这片云的银边确实有点被玷污。可以设置一个中央日志服务器来接收来自其他机器的journald日志,但这个功能仍在开发中,尚未被视为生产就绪。因此,由于大多数第三方日志聚合工具仍然期望看到明文的rsyslog文件,rsyslog仍然存在,并且可能还会长期存在。在撰写本文时,我不知道任何已完全过渡到journald的 Linux 发行版。我所知道的每个 Linux 发行版都同时运行journaldrsyslog

注意

刚才有人指出,Fedora 团队最初曾让 Fedora 21 只运行journald。然而,他们收到了很多关于此问题的投诉,因此他们进行了更新,重新引入了rsyslog。我不知道这一点,因为我几年前就因为其不稳定性问题而放弃了 Fedora。直到 Fedora 23 解决了不稳定性问题后,我才重新开始使用它。无论如何,你可以在这里详细了解一下这个事件的情况:

www.linuxquestions.org/questions/linux-newbie-8/where-are-var-log-dmesg-and-var-log-messages-4175533513/.

然而,各个发行版在处理这个问题上存在差异。在下一节中,我们将看看 Ubuntu 是如何处理的。

理解 Ubuntu 上的 journald

在 Ubuntu 系统上,journaldrsyslog服务默认都会启用,它们各自作为完全独立的实体运行。journald的日志是持久性的,这意味着它们会永久存储在磁盘上,而不是在每次关机时就被删除。rsyslog日志文件也会存在,并且每周都会进行轮换。

有两件事让journald日志文件保持持久性。首先,是/etc/systemd/journald.conf文件中的第一个配置选项,内容如下:

[Journal]
#Storage=auto
. . .
. . .

当你查看整个文件时,你会看到其中的每一行都被注释掉了。这仅仅意味着所有这些选项都被设置为默认值。如果要更改某些内容,只需取消注释该行并修改值即可。然而,我们不需要对#Storage=auto这一行做任何处理。这里的auto意味着,如果/var/log/journal/目录存在,journald就会将日志文件永久存储在那里。如果/var/log/journal/目录不存在,则会在每次启动机器时,在/run/log/journal/目录中创建一组临时日志文件。关机时,这些临时日志文件会被删除。在 Ubuntu 机器上,/var/log/journal/目录已经为你创建好,这意味着日志文件是持久性的。为了展示journald日志文件有多持久,我们来快速看看我用来写这篇文章的主机上有什么。这台机器运行的是Lubuntu 18.04,这只是带有替代桌面环境的 Ubuntu。我将使用没有任何选项的journalctl命令,像这样:

donnie@siftworkstation: ~
$ journalctl
-- Logs begin at Wed 2018-11-21 17:38:02 EST, end at Mon 2021-08-09 16:42:10 EDT. --
Nov 21 17:38:02 lubuntu1a kernel: microcode: microcode updated early to revision 0x713, date = 2018-01-26
. . .
. . .

在这里,我们可以看到日志始于 2018 年 11 月,差不多是三年前的事了。(我写这篇文章时是 2021 年 8 月。)三年的日志肯定会占用大量磁盘空间,对吧?嗯,让我们用journalctl --disk-usage命令来查明:

donnie@siftworkstation: ~
$ journalctl --disk-usage
Archived and active journals take up 904.1M in the file system.
donnie@siftworkstation: ~
$

所以,三年的journald日志文件甚至没有占用整整一个 GB 的磁盘空间。这比三年积累下来的rsyslog文本文件所需的空间要少得多。要查看实际的journald日志文件,可以进入/var/log/journal/目录。在那里,你会看到一个目录,目录名是一个相当长的十六进制数字,像这样:

donnie@siftworkstation: ~
$ cd /var/log/journal/
donnie@siftworkstation: /var/log/journal
$ ls
92fe2206f513462da1869220d8191c1e
donnie@siftworkstation: /var/log/journal
$ 

在该目录下,你会看到日志文件:

donnie@siftworkstation: /var/log/journal
$ cd 92fe2206f513462da1869220d8191c1e/
donnie@siftworkstation: /var/log/journal/92fe2206f513462da1869220d8191c1e
$ ls -l
total 925888
-rw-r-----+ 1 root systemd-journal 16777216 Nov 28  2018 system@00057bbf27f5178e-5ed563c9fd14588f.journal~
. . .
. . .
-rw-r-----+ 1 root systemd-journal  8388608 Aug  9 13:12 user-1000.journal
donnie@siftworkstation: /var/log/journal/92fe2206f513462da1869220d8191c1e

我们可以使用wc -l命令来轻松统计文件数量,像这样:

donnie@siftworkstation: /var/log/journal/92fe2206f513462da1869220d8191c1e
$ ls -l | wc -l
75
donnie@siftworkstation: /var/log/journal/92fe2206f513462da1869220d8191c1e
$ 

所以,这台机器上有 75 个journald日志文件。原因是journald配置为将信息存储在多个较小的文件中,而不是将所有内容存储在一个庞大的文件中。

注意

当我们南方人说某样东西是big honkin'(超级大的)时,我们的意思是它真的很大

最酷的部分是,当你使用journalctl查看日志文件时,它会根据需要自动打开所有这些文件,而不是让你通过单独的命令打开每个文件。(这是journald相较于rsyslog的另一个巨大优势。)

如果你确实需要限制journald用于日志文件存储的磁盘空间,可以在/etc/systemd/journald.conf文件中设置适当的参数。(有关详细信息,请参见journald.conf的手册页。)另外,journalctl的手册页告诉你如何旋转日志文件,并使用--vacuum-size=, --vacuum-time=, 和--vacuum-files=选项删除旧的归档日志文件。让我们来看一个如何操作的示例。

首先,关闭虚拟机并为其拍摄快照。(你将需要大量日志文件来进行接下来的演示。)然后,重启虚拟机并确保持久化日志与瞬态日志是同步的,像这样:

donnie@ubuntu2004:~$ sudo journalctl --flush
donnie@ubuntu2004:~$

接下来,结合使用--rotate--vacuum-time选项,将当前日志文件归档,创建新的空日志文件,并删除所有超过五天的旧归档日志文件,像这样:

donnie@ubuntu2004:~$ sudo journalctl --rotate --vacuum-time=5d
Deleted archived journal /var/log/journal/55520bc0900c428ab8a27f5c7d8c3927/system@a2d77617383f477da0a4d539e137b488-0000000000000001-0005b8317f856d88.journal (8.0M).
Deleted archived journal /var/log/journal/55520bc0900c428ab8a27f5c7d8c3927/user-1000@d713d47989e84072bc1445c9829a0c1f-0000000000000415-0005b8318153a5b1.journal (8.0M)
. . .
. . .
Vacuuming done, freed 2.1G of archived journals from /var/log/journal/55520bc0900c428ab8a27f5c7d8c3927.
Vacuuming done, freed 0B of archived journals from /var/log/journal.
Vacuuming done, freed 0B of archived journals from /run/log/journal.
donnie@ubuntu2004:~$

最后,关闭虚拟机,恢复快照,并重启虚拟机。

注意

如果你不希望你的 Ubuntu 机器持久存储journald日志文件,可以删除/var/log/journal/目录,或者进入/etc/systemd/journald.conf文件,将#Storage=auto行改为Storage=volatile

关于 Ubuntu,我最后想提到的是,当你安装操作系统时,安装程序创建的用户会同时成为sudoadm组的成员。你可能已经知道,sudo组的成员拥有完全的sudo权限。在大多数其他发行版中,你需要使用sudo来查看系统日志文件。而在 Ubuntu 机器上,adm组的成员可以在没有sudo权限的情况下查看所有rsyslogjournald日志。

好的,关于 Ubuntu 上的journald就讲到这里。现在,我们继续来看 RHEL 类型系统上的journald

理解 RHEL 类型系统上的 journald

在 Red Hat 世界中,操作方式有一些显著的差异。首先,在你的 AlmaLinux 机器上,你会发现没有/var/log/journal/目录,这意味着journald日志文件只会在/run/log/journal/目录下创建,并且每次关闭或重启机器时都会消失。如果你想改变这一点,只需创建该日志子目录,像这样:

[donnie@localhost ~]$ sudo mkdir /var/log/journal

你会立即看到,journald日志文件现在变成持久存储了。

注意

在将journald日志文件设置为生产机器上的持久存储之前,评估是否真的需要这样做。

另一个重大区别是,在 RHEL 类型的系统上,journaldrsyslog 是协同工作的,而不是独立工作的。不是同时由 journaldrsyslog 收集来自操作系统其余部分的信息,而是只有 journald 在做这件事。然后,rsyslogjournald 获取信息并将其存储在正常的 rsyslog 文本文件中。我们可以看到在 /etc/rsyslog.conf 文件的顶部部分是如何启用这一功能的:

#### MODULES ####
. . .
. . .
module(load="imjournal"             # provides access to the systemd journal
       StateFile="imjournal.state") # File to store the position in the journal
. . .
. . .

与旧版的 syslog 服务(被 rsyslog 替代)不同,你可以通过添加新模块来扩展 rsyslog 的功能。在这里,我们可以看到 imjournal 模块,它使得 rsyslog 能够从 journald 服务接收数据。所以,在 RHEL 8 类型的机器上,journald 收集来自整个系统的数据并将其传递给 rsyslog

最终的一个重大区别是,在 RHEL 类型的系统上,你需要适当的 sudo 权限才能查看所有的日志文件。虽然有 adm 组,但将自己添加到该组并不会对你有任何帮助。

现在我们已经了解了 Ubuntu 和 RHEL 上 journald 配置的差异,接下来我们来看一下如何使用 journalctl,它在所有发行版中都相同。

使用 journalctl

journalctl 工具非常有用,因为它具有很大的灵活性。让我们首先看看如何搜索和显示日志数据。我们将在 Ubuntu 机器上进行演示,因为 Ubuntu 的持久 journald 日志能提供更多内容供我们查看。

使用 journalctl 搜索和查看日志数据

查看日志文件的最简单命令就是 journalctl。正如我们所看到的,它会显示你在打开普通的 rsyslog 文件时通过 less 查看时几乎相同的信息。你还会看到 journalctl 的输出会自动传递到 less 中:

donnie@ubuntu2004:~$ journalctl
-- Logs begin at Tue 2021-01-05 20:46:55 EST, end at Tue 2021-08-10 14:23:17 ED>
Jan 05 20:46:55 ubuntu2004 kernel: Linux version 5.4.0-59-generic (buildd@lcy01>
Jan 05 20:46:55 ubuntu2004 kernel: Command line: BOOT_IMAGE=/vmlinuz-5.4.0-59-g>
Jan 05 20:46:55 ubuntu2004 kernel: KERNEL supported cpus:
Jan 05 20:46:55 ubuntu2004 kernel:   Intel GenuineIntel
Jan 05 20:46:55 ubuntu2004 kernel:   AMD AuthenticAMD
Jan 05 20:46:55 ubuntu2004 kernel:   Hygon HygonGenuine
. . .
. . .
Jan 05 20:46:55 ubuntu2004 kernel: BIOS-e820: [mem 0x00000000fffc0000-0x0000000>
Jan 05 20:46:55 ubuntu2004 kernel: BIOS-e820: [mem 0x0000000100000000-0x0000000>
Jan 05 20:46:55 ubuntu2004 kernel: NX (Execute Disable) protection: active
Jan 05 20:46:55 ubuntu2004 kernel: SMBIOS 2.5 present.
lines 1-26

与你习惯使用的 rsyslog 不同的一个大区别是,长行不会换行显示。相反,它们会超出可视窗口的右侧。要查看这些行的其余部分,你需要使用右方向键。除此之外,你可以使用通常在 less 工具中使用的搜索和导航命令。例如,要直接跳转到 journalctl 输出的底部,只需按下 Shift + G 键组合。(不过,要有耐心,因为 journalctl 需要读取我之前展示过的所有文件,这需要一些时间。)要跳转到特定的行,只需输入行号后跟小写的 g。要搜索文本字符串,按 / 键并输入搜索词。完成后,只需按 Q 键退出。

注意

我应该指出,由于我正在使用 Ubuntu 虚拟机,在其中我已经是 adm 组的成员,所以不需要 sudo 权限就能查看所有系统日志。如果你决定在 AlmaLinux 机器上尝试,你需要使用 sudo。否则,你只能看到你的用户日志。

如果你查看 journalctl 的手册页,你会看到很多显示和搜索选项。我无法向你展示所有的选项,但我们可以看一些示例。

在一台具有持久 journald 日志文件的机器上,你可能只想查看当前启动会话的日志条目。为了让它生效,重启你的机器几次,这样你就有更多日志可供查看。然后,使用 journalctl -b,像这样:

donnie@ubuntu2004:~$ journalctl -b
-- Logs begin at Tue 2021-01-05 20:46:55 EST, end at Tue 2021-08-10 15:03:43 ED>
Aug 10 14:20:38 ubuntu2004 kernel: Linux version 5.4.0-80-generic (buildd@lcy01>
Aug 10 14:20:38 ubuntu2004 kernel: Command line: BOOT_IMAGE=/vmlinuz-5.4.0-80-g>
. . .
. . .

在这里,你可以看到日志始于 2021 年 1 月,但它显示的第一个条目是当前日期 8 月 10 日下午 2:20,即我最后一次启动虚拟机时的时间。要查看上一次启动会话的日志条目,只需添加 -1,像这样:

donnie@ubuntu2004:~$ journalctl -b -1
-- Logs begin at Tue 2021-01-05 20:46:55 EST, end at Tue 2021-08-10 15:07:27 ED>
Aug 10 13:36:44 ubuntu2004 kernel: Linux version 5.4.0-80-generic (buildd@lcy01>
Aug 10 13:36:44 ubuntu2004 kernel: Command line: BOOT_IMAGE=/vmlinuz-5.4.0-80-g>
. . .
. . .

所以,看起来我今天下午 1:36 已经启动过这台机器。你也可以通过指定不同的数字来查看早期启动会话的文件。例如,你可以使用 -2 显示两次启动会话前的文件,使用 -10 显示 10 次启动会话前的文件,依此类推。

要查看所有启动的列表,使用 --list-boots 选项:

donnie@ubuntu2004:~$ journalctl --list-boots
-46 7c611db8974b4cdb897853c4367048cf Tue 2021-01-05 20:46:55 EST—Tue 2021-01-05 20:57:29 EST
-45 643f70296ebf4b5c8e798f8f878c0ac5 Thu 2021-02-11 16:16:06 EST—Thu 2021-02-11 20:03:42 EST
-44 139f7be3bc3d43c69421c68e2a5c76b8 Mon 2021-03-15 15:36:01 EDT—Mon 2021-03-15 16:42:32 EDT
. . .
. . .
-1 9a4781c6b0414e6e924cc391b6129185 Wed 2021-08-25 17:24:56 EDT—Wed 2021-08-25 21:48:06 EDT
  0 354575e3e3d747039f047094ffaaa0d2 Mon 2021-08-30 16:28:29 EDT—Mon 2021-08-30 16:39:49 EDT
donnie@ubuntu2004:~$

看起来自从我创建这台虚拟机以来,我已经启动了 47 次。你在第二个字段中看到的长十六进制数字是启动会话的 ID 号。

-g 选项允许你使用 grep 查找特定的文本字符串或 Perl 兼容的正则表达式。如果你的搜索词仅包含小写字母,则搜索将不区分大小写。如果搜索词包含任何大写字母,则搜索是区分大小写的。例如,使用 journalctl -g fail 将显示所有包含某种形式的fail的条目,无论 fail 是小写、大写还是大小写混合。但如果你使用 journalctl -g Fail,你只会看到包含具体字符串 Fail 的条目。如果你的搜索词仅包含小写字母,而你想使搜索区分大小写,只需使用 --case-sensitive= 选项,像这样:

donnie@ubuntu2004:~$ journalctl -g fail --case-sensitive=true

journald 日志消息的优先级等级与 ryslog 日志消息相同。唯一的区别是你现在可以在搜索中使用优先级的名称或数字。按重要性降序排列,优先级等级如下:

  • 0emerg:这是紧急级别,用于处理像内核崩溃之类的情况。(希望你很少会看到这些。)

  • 1alert:这些不算紧急,但依然是不好的消息。

  • 2crit:如果你看到一些关键消息,不要太惊讶。这些可能是由于用户输入密码时犯了小错误造成的。也可能是有人试图暴力破解密码,所以值得留意这些信息。

  • 3err:这些可能是由无法启动的服务、内存不足的程序、无法访问硬件设备等问题引起的。

  • 4warning:你会看到很多这样的消息,但大多数不用担心。通常,它们大多数是从启动计算机时生成的内核消息。

  • 5notice:这些不属于紧急消息,但你仍然需要注意它们。(看看我这么说的意思了吗?)

  • 6info:你看到的大多数日志消息应该是 info 级别的。

  • 7debug:这是最低的优先级。默认情况下,rsyslog 上没有启用,但 journald 上启用了。

现在,假设你只想查看 emerg(紧急)消息。使用 -p 选项,后面跟优先级的数字或名称,如下所示:

donnie@ubuntu2004:~$ journalctl -p 0
-- Logs begin at Tue 2021-01-05 20:46:55 EST, end at Tue 2021-08-31 14:50:38 EDT. --
-- No entries --
donnie@ubuntu2004:~$ journalctl -p emerg
-- Logs begin at Tue 2021-01-05 20:46:55 EST, end at Tue 2021-08-31 14:50:39 EDT. --
-- No entries --
donnie@ubuntu2004:~$

所以,没有紧急消息,这是一件好事。那么 alert 消息呢?我们来看一下:

donnie@ubuntu2004:~$ journalctl -p 1
-- Logs begin at Tue 2021-01-05 20:46:55 EST, end at Tue 2021-08-31 14:51:35 EDT. --
-- No entries --
donnie@ubuntu2004:~$

很好。也没有这些。但是从优先级 2crit)开始往下的情况就不一样了,正如我们在这里看到的:

donnie@ubuntu2004:~$ journalctl -p 2
-- Logs begin at Tue 2021-01-05 20:46:55 EST, end at Tue 2021-08-31 14:51:38 EDT. --
Apr 03 17:09:22 ubuntu2004 sudo[185694]: pam_unix(sudo:auth): auth could not identify password for [donnie]
-- Reboot --
Jul 23 15:16:14 ubuntu2004 sudo[48285]: pam_unix(sudo:auth): auth could not identify password for [donnie]
-- Reboot --
Jul 27 15:27:54 ubuntu2004 sudo[156593]: pam_unix(sudo:auth): auth could not identify password for [donnie]
-- Reboot --
Aug 17 17:10:51 ubuntu2004 sudo[222044]: pam_unix(sudo:auth): auth could not identify password for [donnie]
donnie@ubuntu2004:~$

所以,存在一些 critical 消息。现在,事情变得有些复杂。因为当你指定一个优先级时,你会看到从该优先级到所有较高优先级的消息。所以,如果我现在指定等级 3err),我也会看到等级 2crit)的消息。如果只想看到等级 3 的消息,请指定一个范围,将等级 3 作为起点和终点,如下所示:

donnie@ubuntu2004:~$ journalctl -p 3..3

奇怪的是,journalctl 的手册页告诉你可以指定范围,但它并没有告诉你必须使用两个点。为了搞明白这点,我不得不进行一次 DuckDuckGo 搜索。

另一个来自 rsyslog 的概念是 设施。不同的 Linux 子系统生成不同类型的消息或设施。标准的设施有:

  • auth:由授权系统、登录、su 等生成的消息。

  • authpriv:由授权系统生成的消息,但仅限选定用户可读。

  • cron:由 cron 服务生成的消息。

  • daemon:由所有系统守护进程(如 sshdftpd 等)生成的消息。

  • ftp:由文件传输协议FTP)服务生成的消息。

  • kern:由 Linux 内核生成的消息。

  • lpr:由行打印机排队系统生成的消息。

  • mail:由操作系统的内部邮件系统生成的消息。

  • mark:这些是可以插入到日志中的周期性时间戳。

  • news:这个功能处理来自 Usenet 新闻组服务的消息,这些服务也几乎已经消失了。所以,你很可能永远也看不到这些消息。

  • syslog:由 rsyslog 生成的消息。

  • user:由用户生成的消息。

  • uucp:来自 Unix 到 Unix 复制系统的消息。这个系统基本上已经废弃了,因此你可能永远看不到这些消息。

  • local0local7:你可以使用这些来定义自定义设施。

要查看来自特定设施的消息,请使用 --facility 选项,如下所示:

donnie@ubuntu2004:~$ journalctl --facility uucp
-- Logs begin at Tue 2021-01-05 20:46:55 EST, end at Tue 2021-08-31 15:46:58 EDT. --
-- No entries --
donnie@ubuntu2004:~$

好的,我确实告诉过你,可能没有uucp消息。为了一个更现实的示例,我们来看看authauthpriv消息,并比较它们之间的差异:

donnie@ubuntu2004:~$ journalctl --facility authpriv
. . .
. . .
donnie@ubuntu2004:~$ journalctl --facility auth

对于我们的最后一个示例,让我们做得更花哨一些。让我们查看自昨天以来优先级为4daemon消息:

donnie@ubuntu2004:~$ journalctl --facility daemon -p 4..4 -S yesterday

好的,关于优先级和设施部分就讲到这里。

注意

要查看所有可用设施的列表,只需执行:

journalctl --facility=help

你还可以查看某个用户的日志条目。为此,你需要获取该用户的 UID,像这样:

donnie@ubuntu2004:~$ id frank
uid=1002(frank) gid=1002(frank) groups=1002(frank)
donnie@ubuntu2004:~$

所以,Frank 的 UID 是1002。现在,我们来看一下他的日志条目:

donnie@ubuntu2004:~$ journalctl _UID=1002
-- Logs begin at Tue 2021-01-05 20:46:55 EST, end at Tue 2021-08-10 15:47:17 ED>
Jul 23 15:10:01 ubuntu2004 systemd[40881]: Reached target Paths.
Jul 23 15:10:01 ubuntu2004 systemd[40881]: Reached target Timers.
. . .
. . .

好的,没问题。但我只想查看他今天的日志条目。我可以使用-S--since选项来做到这一点:

donnie@ubuntu2004:~$ journalctl _UID=1002 -S today
-- Logs begin at Tue 2021-01-05 20:46:55 EST, end at Tue 2021-08-10 15:49:27 ED>
-- No entries --
lines 1-2/2 (END)

好吧,Frank 今天没有登录,这也不奇怪。记住,Frank 是一只猫,这意味着他大部分时间都在睡觉。

现在,假设你想查看关于 Apache Web 服务器服务的信息,但你想以 JSON 格式查看。好吧,我们来做这个:

donnie@ubuntu2004:~$ journalctl -u apache2 -o json
{"_EXE":"/usr/lib/systemd/systemd","SYSLOG_IDENTIFIER":"systemd","_CMDLINE":"/s>
{"__CURSOR":"s=a2d77617383f477da0a4d539e137b488;i=37181;b=88df3ae40cb9468a8d13a>
{"_HOSTNAME":"ubuntu2004","JOB_ID":"3311","__CURSOR":"s=a2d77617383f477da0a4d53>
. . .
. . .

这个输出不够美观吗?没问题,我们可以让它更美观:

donnie@ubuntu2004:~$ journalctl -u apache2 -o json-pretty
{
        "_UID" : "0",
        "_CMDLINE" : "/sbin/init maybe-ubiquity",
        "_CAP_EFFECTIVE" : "3fffffffff",
        "JOB_TYPE" : "start",
        "_PID" : "1",
        "_GID" : "0",
        "CODE_FILE" : "src/core/job.c",
        "__CURSOR" : "s=a2d77617383f477da0a4d539e137b488;i=3717c;b=88df3ae40cb9>
. . .
. . .

请注意,JSON 格式中显示的字段比标准默认格式中更多。这是因为journalctl的默认输出格式旨在模拟标准的rsyslog格式。

现在,假设你只想查看昨天的 Apache 信息,并且你希望将其保存为 JSON 文件,并同时在屏幕上查看输出。--no-pager选项允许你将journalctl的输出管道传输到另一个实用程序,就像我们在这里用 tee 实用程序所做的那样:

donnie@ubuntu2004:~$ journalctl -u apache2 -S yesterday -U today -o json --no-pager | tee apache2.json

你还可以使用--no-pager将输出传送到标准的 Linux 文本过滤和处理工具,例如 grep 或 awk。如果你需要编写用于 Nagios 或 Icinga 插件的 shell 脚本,这会很有用。

我可以给你更多的示例,但你应该已经明白了。再说,这也是自由软件世界中少数几个可以直接使用其他人编写的文档而无需改进的情况之一。所以,如果你想查看更多内容,我会把你引导到journalctl的手册页和进一步阅读部分的资源。

为了安全封印 journald 日志文件

我已经告诉你,恶意人员篡改文本模式下的rsyslog文件以删除他们的恶意活动是多么容易。而journald日志文件因为是二进制格式,已经更难篡改。通过封印它们,我们可以让篡改变得更加困难。(当然,前提是你有持久的journald日志。)

第一步是创建一组前向安全封印FSS)密钥,像这样:

donnie@ubuntu2004:~$ sudo journalctl --setup-keys

此命令创建了两个密钥。封印密钥名为fss,并存储在与journald日志文件相同的目录中,如下所示:

donnie@ubuntu2004:~$ cd /var/log/journal/55520bc0900c428ab8a27f5c7d8c3927/
donnie@ubuntu2004:/var/log/journal/55520bc0900c428ab8a27f5c7d8c3927$ ls -l fss
-rw-------+ 1 root systemd-journal 482 Aug 10 16:50 fss
donnie@ubuntu2004:/var/log/journal/55520bc0900c428ab8a27f5c7d8c3927$

验证密钥仅以文本字符串的形式显示在你的屏幕上,如下所示:

Figure 14.1 – Creating the Forward Secure Sealing (FSS) keys

It says to write this key down, but I'd rather cheat by copying and pasting it into a text file that I can store in a secure location. (For this demo, you can just store the text file in your home directory.)

Now, you can periodically run a verify operation to ensure that nobody has tampered with your log files. Just copy and paste the verification key into the command, like this:

donnie@ubuntu2004:~$ sudo journalctl --verify --verify-key=43e654-62c3e2-519f3b-7d2850/1b9cb3-35a4e900
PASS: /var/log/journal/55520bc0900c428ab8a27f5c7d8c3927/user-1000@3ebae2fd52f7403bac3983eb8a90d2ef-0000000000052181-0005beefb66cbef3.journal
PASS: /var/log/journal/55520bc0900c428ab8a27f5c7d8c3927/user-1000@53aedefe543040f48dd89ba98d7f9aae-00000000000a33c1-0005c704cacbdd12.journal
. . .
. . .

That's about it for sealing your log files. Now, let's talk very briefly about setting up remote logging with journald.

Setting up remote logging with journald

Sometimes, it's handy to set up a central log collection server and have all the other machines on the network send their log files to it.

As I've already said, journald remote logging is still in a proof-of-concept phase and isn't considered ready for production use. Also, most third-party log-aggregation utilities are still set up to use plaintext rsyslog files. So, if you have remote logging on your site or if you need to set up remote logging, you'll most likely use rsyslog.

However, I do realize that some of you might be interested in playing around with a remote journald logging setup. If that's the case, I'd like to direct your attention to the procedure that's linked in the Further reading section. However, be aware that you'll need to install security certificates on the journald log server and all of the clients. This procedure has you install certificates from Let's Encrypt, which requires you to have your machines in a domain that's registered on the public Domain Name Service (DNS) servers. If the Let's Encrypt installer can't find your machines on a public DNS server, the install operation will abort.

Fortunately, if you just want to set up centralized journald logging for an internal LAN, you can modify the procedure so that it uses certificates that you create locally from a local Certificate Authority server. (Showing you how to set up a local Certificate Authority is beyond the scope of this book, but you can read about it in my other book, Mastering Linux Security and Hardening.)

Well, I think that that should do it for journald. Let's wrap things up and move on.

Summary

In this chapter, we covered the journald logging system and compared it to the tried-and-true rsyslog. First, we looked at the pros and cons of both rsyslog and journald. Then, we saw how the two logging systems are implemented in both Ubuntu and RHEL distros. After that, we saw the various viewing, searching, and formatting options that we can use with the journalctl utility. We wrapped up by learned how to make our journald log files more tamper-resistant and briefly discussed setting up a centralized journald log server.

In the next chapter, we'll look at using systemd's own network services. I'll see you there!

Questions

请回答以下问题,测试你对本章内容的理解:

  1. journaldrsyslog 之间的主要区别是什么?

    A. journald 以纯文本格式存储文件,而 rsyslog 以二进制格式存储文件。

    B. 没有区别。

    C. rsyslog 以纯文本格式存储文件,而 journald 以二进制格式存储文件。

  2. 以下哪项陈述是正确的?

    A. 基于现代 systemd 的 Linux 发行版仅包含 rsyslogjournald,但不会同时包含两者。

    B. journaldrsyslog 总是彼此独立工作。

    C. 基于现代 systemd 的 Linux 发行版同时包含 rsyslogjournald

    D. journaldrsyslog 永远不能独立工作。

  3. journald 在 Ubuntu 和 RHEL 上的实现方式有什么两大区别?(选择 2 项。)

    A. 在 RHEL 上,journald 日志是持久的,但在 Ubuntu 上不是。

    B. 在 RHEL 上,journald 完全独立于 rsyslog 工作;在 Ubuntu 上,它们是一起工作的。

    C. 在 RHEL 上,journaldrsyslog 一起工作;在 Ubuntu 上,它们是独立工作的。

    D. 在 Ubuntu 上,journald 日志是持久的;在 RHEL 上,则不是。

    E. 没有区别。

  4. 以下哪个命令可以删除所有 journald 日志文件,保留最新的 1GB?

    A. sudo journalctl --vacuum-size=1G

    B. sudo journalctl --rotate

    C. sudo journalctl --size=1G

    D. sudo journalctl --clean=1G

答案

  1. C

  2. C

  3. C, D

  4. A

进一步阅读

超快的 syslog 服务器(rsyslog 项目页面):

第十五章:使用 systemd-networkd 和 systemd-resolved

systemd 生态系统有其自己的网络组件。使用这些组件完全是可选的,甚至你可能会发现自己根本不需要使用它们。然而,有时候使用 systemd 的网络组件可以帮助你做一些传统 Linux 的 NetworkManager 做不到的事情。

本章将覆盖以下主题:

  • 理解networkdresolved

  • 理解 Ubuntu 上的 Netplan

  • 理解 RHEL 类型机器上的 networkd 和 resolved

  • 使用networkctlresolvectl

  • 查看 networkd 和 resolved 单元文件

好的,让我们开始吧!

技术要求

我们将从 Ubuntu Server 虚拟机开始。(请注意,你需要使用 Ubuntu Server,因为 Ubuntu 的桌面版本仍然默认使用旧版的 NetworkManager。)在本章的后面,我们将与 AlmaLinux 一起操作。

那么,让我们从简单介绍 networkd 和 resolved 开始吧。

查看以下链接,观看《代码实战》视频:bit.ly/31mmXXZ

理解 networkd 和 resolved

传统的 NetworkManager 已经存在了相当长的时间,它仍然是大多数 Linux 桌面和笔记本的最佳解决方案。Red Hat 开发它的主要原因是为了使得 Linux 笔记本能够在有线和无线网络之间,或者从一个无线域切换到另一个无线域时立即切换。NetworkManager 对于普通 Linux 服务器来说也仍然运行良好。所有 RHEL 类型的发行版和所有 Ubuntu 桌面版本仍然默认使用 NetworkManager。

注意

我并不总是会输入systemd-networkdsystemd-resolved。除非我输入的是实际命令,否则我会将它们简写为networkdresolved,这也是大多数人通常做的事。

你已经知道我有一个很怪异的习惯,就是读懂你的心思。所以,我知道你在想,但是 Donnie,如果 NetworkManager 这么好,为什么我们还需要 networkd 和 resolved 呢? 啊,我很高兴你问了这个问题。其实,使用networkdresolved你可以做一些 NetworkManager 做不到的事。例如,你可以使用 networkd 为运行容器设置一个桥接网络,这样你可以直接给容器分配 IP 地址,让它们能够从外部直接访问。而使用 resolved,你可以设置分割 DNS 解析,从 DHCP 服务器或 IPv6 路由广告中获取 DNS 服务器地址,并使用 DNSSEC、MulticastDNS 和 DNS-over-TLS。另一方面,由于 NetworkManager 能在需要时即时切换网络,它依然是最适合普通桌面和笔记本使用的解决方案。

在运行纯 networkd 环境的系统上,网络配置会存储在 /etc/systemd/network/ 目录中的一个或多个 .network 文件中。然而,正如我之前所说,RHEL 系统默认并不使用 networkd,所以目前在 AlmaLinux 机器上没有 .network 文件可供展示。Ubuntu Server 默认使用 networkd,但 Ubuntu 工程师做了一些使事情变得更加有趣的工作。他们并没有按照常规方式配置 networkd 或 NetworkManager,而是创建了 Netplan,我们接下来会详细看一下。

理解 Ubuntu 上的 Netplan

Netplan 是 Ubuntu 新的网络配置工具。在桌面机器上,它的作用不大,仅仅是告诉系统使用 NetworkManager。在服务器上,你会在 /etc/netplan/ 目录下创建一个 .yaml 文件来配置 networkd。Netplan 会将这个 .yaml 文件的内容转换成 networkd 格式。

查看安装程序生成的 Netplan 配置

首先,我想给你展示一下 Ubuntu 桌面机器上的默认配置。(是的,我知道,我没有告诉你需要一个 Ubuntu 桌面虚拟机,但没关系。这是唯一一次我们需要它,所以你只需要看我在这里展示的内容。)在 /etc/netplan/ 目录下,我们有一个在我创建虚拟机时生成的默认配置文件:

donnie@donald-virtualbox:~$ cd /etc/netplan/
donnie@donald-virtualbox:/etc/netplan$ ls -l
total 4
-rw-r--r-- 1 root root 104 Feb  3  2021 01-network-manager-all.yaml
donnie@donald-virtualbox:/etc/netplan$

01-network-manager-all.yaml 文件中,我们有如下内容:

# Let NetworkManager manage all devices on this system
network:
  version: 2
  renderer: NetworkManager

这里是详细说明:

  • network: 这一行与屏幕左边对齐,这意味着这是一个新节点。接下来的两行缩进了一个空格,这意味着它们是该节点定义的一部分。

  • version: 2 这一行的含义不明确。netplan 手册页表示它是一个 网络映射,但并没有进一步解释,只是指出它 可能 与 Netplan 使用的 YAML 版本有关。或者,它可能是 netplan 配置的语法版本。由于似乎没有文档能解决这个谜团,所以很难确定。无论如何,手册页确实表明,这个 version: 2 这一行在定义 network: 节点时始终必须存在。

  • renderer: 这一行告诉系统使用 NetworkManager,所有其他的配置则是在普通的 NetworkManager 文件中完成的。由于这是台桌面机器,大多数人会使用 GUI 管理工具来重新配置网络。(大多数 GUI 类型的工具都是显而易见的,所以我们不会再多说了。)如果没有 renderer: 这一行,系统则会使用 networkd 而非 NetworkManager。

    注意

    创建或编辑 .yaml 文件时,记住正确的缩进非常重要。如果缩进不正确,配置将无法正常工作。而且 .yaml 文件中不允许使用制表符,因此你需要使用空格键来进行缩进。

相比之下,Ubuntu Server 被配置为使用 networkd。因此,由 Ubuntu Server 安装程序创建的 Netplan 配置看起来是这样的:

donnie@ubuntu2004:/etc/netplan$ ls
00-installer-config.yaml
donnie@ubuntu2004:/etc/netplan$ cat 00-installer-config.yaml 
# This is the network config written by 'subiquity'
network:
  ethernets:
    enp0s3:
      dhcp4: true
  version: 2
donnie@ubuntu2004:/etc/netplan$

network: 节点总是顶级节点。在它下面,我们可以看到 ethernets: 节点,它定义了网络接口。(在这个例子中,接口的名称是 enp0s3。)dhcp4: true 这一行告诉系统从 DHCP 服务器获取 IPv4 地址。(在这种情况下,DHCP 服务器内置在我的互联网网关路由器中。)

当网络启动时,Netplan 会将这个 .yaml 文件转换成 networkd 格式。然而,它并不会存储 .network 文件的永久副本。相反,它会在 /run/systemd/network/ 目录下创建一个临时的 .network 文件,就像我们在这里看到的:

donnie@ubuntu2004:/run/systemd/network$ ls -l
total 4
-rw-r--r-- 1 root root 102 Aug 13 15:33 10-netplan-enp0s3.network
donnie@ubuntu2004:/run/systemd/network$

在这个文件中,我们可以看到如下内容:

[Match]
Name=enp0s3
[Network]
DHCP=ipv4
LinkLocalAddressing=ipv6
[DHCP]
RouteMetric=100
UseMTU=true

最终,我们终于可以看到一个实际的 networkd 配置文件是什么样子的,并且我们看到它被划分为 [Match][Network][DHCP] 部分。如我之前所提到的,这个文件并不是永久性的。它会在你关机或重启时消失,并在机器重新启动时重新出现。(在不使用 Netplan 的非 Ubuntu 系统中,你会在 /etc/systemd/network/ 目录下找到这个文件的永久副本。)这个文件大部分内容是自解释的,但也有一些有趣的地方。在 [Network] 部分,我们看到的是 IPv6 .yaml 文件。在 [DHCP] 部分,我们看到此网络链路的 最大传输单元 的值将从 DHCP 服务器获取。(大多数情况下,该值会被设置为 1500。)RouteMetric=100 定义了将给予此网络链路的优先级。(当然,这里只有一个网络链路,所以这对我们来说并没有什么作用。)

为了展示静态 IP 地址配置的样子,我创建了一台新的 Ubuntu Server 机器,并告诉安装程序创建一个静态配置。那台机器上的 Netplan .yaml 文件是这样的:

donnie@ubuntu2004-staticip:/etc/netplan$ cat 00-installer-config.yaml 
# This is the network config written by 'subiquity'
network:
  ethernets:
    enp0s3:
      addresses:
      - 192.168.0.49/24
      gateway4: 192.168.0.1
      nameservers:
        addresses:
        - 192.168.0.1
        - 8.8.8.8
  version: 2
donnie@ubuntu2004-staticip:/etc/netplan$

生成的临时 .network 文件看起来是这样的:

donnie@ubuntu2004-staticip:/run/systemd/network$ cat 10-netplan-enp0s3.network 
[Match]
Name=enp0s3
[Network]
LinkLocalAddressing=ipv6
Address=192.168.0.49/24
Gateway=192.168.0.1
DNS=192.168.0.1
DNS=8.8.8.8
donnie@ubuntu2004-staticip:/run/systemd/network$

这次,我们只有 [Match][Network] 两个部分。由于我们在这台机器上没有使用 DHCP,所以没有 [DHCP] 部分。

现在,请记住,你不必使用在安装操作系统时创建的默认 Netplan 配置。你可以根据需要编辑或替换默认的 .yaml 配置文件。接下来我们来看看这个。

创建 Netplan 配置

现在,假设我们想将第一台 Ubuntu Server 机器从 DHCP 地址转换为静态地址配置。我做的第一件事是重命名当前的 .yaml 文件,保留它作为备份,以防将来想恢复:

donnie@ubuntu2004:/etc/netplan$ sudo mv 00-installer-config.yaml 00-installer-config.yaml.bak
[sudo] password for donnie: 
donnie@ubuntu2004:/etc/netplan$

接下来,我将创建新的 00-static-config.yaml 文件,如下所示:

donnie@ubuntu2004:/etc/netplan$ sudo vim 00-static-config.yaml
donnie@ubuntu2004:/etc/netplan$

让我们将这个新文件调整为如下所示:

network:
  ethernets:
    enp0s3:
      addresses:
      - 192.168.0.50/24
      gateway4: 192.168.0.1
      nameservers:
        addresses:
        - 192.168.0.1
        - 8.8.8.8
        - 208.67.222.222
        - 208.67.220.220
  version: 2

好吧,我得承认,我稍微作弊了一下,将这个内容从另一个虚拟机上复制并粘贴过来。然后我修改了 IP 地址,并添加了另外两个 DNS 服务器的地址。顺便提一下,我在这里使用的 DNS 服务器地址是:

  • 192.168.0.1:这是我的互联网网关路由器的地址。该路由器已配置为使用由我的 ISP(TDS Telecom)运行的 DNS 服务器。因此,192.168.0.1 地址并不是真正的 DNS 服务器。

  • 8.8.8.8:这是 Google 的一个 DNS 服务器地址。

  • 208.67.222.222208.67.220.220:这些地址是由 OpenDNS 组织维护的 DNS 服务器地址。

所以,关于 nameservers: 地址,越多越好。如果一个 DNS 服务器宕机,我们只需要使用另一个,从而消除了单一网络故障点。

保存新文件后,你需要 应用 它,像这样:

donnie@ubuntu2004:/etc/netplan$ sudo netplan apply

请注意,如果你从远程终端执行此操作,可能永远看不到命令提示符返回,这会让你以为系统卡住了。其实并不是这样,而是因为如果你分配了与机器原始 IP 地址不同的地址,你会断开 SSH 连接。(事实上,这就是我刚才遇到的情况。)所以,实际上,你可能想从服务器的本地终端而不是远程执行这个操作。

当你使用 apply 操作时,你是在生成新的 networkd 配置并重新启动 networkd 服务。生成的 networkd 配置现在应该类似于这样:

donnie@ubuntu2004:/run/systemd/network$ cat 10-netplan-enp0s3.network 
[Match]
Name=enp0s3
[Network]
LinkLocalAddressing=ipv6
Address=192.168.0.50/24
Gateway=192.168.0.1
DNS=192.168.0.1
DNS=8.8.8.8
DNS=208.67.222.222
DNS=208.67.220.220
donnie@ubuntu2004:/run/systemd/network$

现在,让我们尝试一些不同的操作。假设我们的虚拟机有多个网络接口,我们希望确保这个网络配置始终应用于正确的接口。我们可以通过将这个配置分配给目标接口的 MAC 地址来实现这一点。首先,我们使用 ip a 来获取 MAC 地址,像这样:

donnie@ubuntu2004:~$ ip a
. . .
. . .
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:f2:c1:7a brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.50/24 brd 192.168.0.255 scope global enp0s3
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fef2:c17a/64 scope link 
       valid_lft forever preferred_lft forever
. . .
. . .

如你所知,网络接口的硬件地址有很多种称呼。我们大多数人都叫它 MAC 地址。它也被称为 物理地址硬件地址,或者像我们这里看到的,link/ether 地址。无论如何,我们复制那个地址,然后粘贴到 .yaml 文件中。在我们刚创建的配置文件的 enp0s3: 节点下,我们插入一个新的 match: 节点,里面包含所需网络接口的 macaddress: 属性。修改后的文件现在应该是这样的:

network:
  ethernets:
    enp0s3:
      match:
        macaddress: 08:00:27:f2:c1:7a
      addresses:
      - 192.168.0.50/24
      gateway4: 192.168.0.1
      nameservers:
        addresses:
        - 192.168.0.1
        - 8.8.8.8
        - 208.67.222.222
        - 208.67.220.220
  version: 2

这次,我们不使用 apply 操作,而是尝试使用 try 操作,像这样:

donnie@ubuntu2004:/etc/netplan$ sudo netplan try
Warning: Stopping systemd-networkd.service, but it can still be activated by:
  systemd-networkd.socket
Do you want to keep these settings?
Press ENTER before the timeout to accept the new configuration
Changes will revert in 110 seconds
Configuration accepted.
donnie@ubuntu2004:/etc/netplan$

netplan try 命令与 netplan apply 做的事情相同,不同之处在于它让你有机会在新配置不起作用时,恢复到旧的配置。

好吧,我已经向你展示了如何使用 Netplan 和 networkd 组合来设置网络的几个例子。当然,这里还有很多内容。如果你想查看一些更复杂的网络设置,最好的办法是查阅 netplan 的手册页。在手册页的底部,你会看到一些非常好的示例。

好的,我们继续前进,学习如何在 Red Hat 环境中使用 networkd。

在 RHEL 类型机器上理解 networkd 和 resolved

我们已经确定,所有 RHEL 类型的机器(例如我们的 AlmaLinux 机器)默认使用 NetworkManager。现在,假设我们有一台 AlmaLinux 服务器,并且需要 networkd 的附加功能。systemd-networkd包默认没有安装,并且不在普通的 Alma 软件仓库中。但是,它在第三方 EPEL 仓库中,因此我们可以通过以下命令来安装它:

[donnie@localhost ~]$ sudo dnf install epel-release

现在,我们可以安装systemd-networkd包,其中包括 networkd 和 resolved:

[donnie@localhost ~]$ sudo dnf install systemd-networkd

接下来,禁用 NetworkManager 并启用 networkd 和 resolved。请注意,我暂时不停止 NetworkManager,也不启动 networkd 和 resolved(因为我通过远程登录,不想断开网络连接。再者,我还没有创建 networkd 配置):

[donnie@localhost ~]$ sudo systemctl disable NetworkManager
. . .
[donnie@localhost ~]$ sudo systemctl enable systemd-networkd systemd-resolved

在配置 networkd 时,我们不能使用systemctl edit命令,因为它会将.network文件创建在错误的位置。相反,我们将进入/etc/systemd/network/目录,并使用普通的文本编辑器。我们将此文件命名为99-networkconfig.network,并添加以下内容:

[Match]
Name=enp0s3
[Network]
DHCP=yes
IPv6AcceptRA=yes

networkd 的.network文件与其他 systemd 单元文件的设置方式相同。你不需要像处理 Netplan 的.yaml文件那样担心正确的缩进,只需将所有参数放入正确的部分即可。在[Match]部分,我们指定了网络适配器的名称。在[Network]部分,我们表示希望通过 DHCP 获取 IP 地址,并接受 IPv6 路由器广告。

下一步是摆脱静态的/etc/resolv.conf文件,并创建一个符号链接指向 resolved 生成的文件。我们可以通过这两个命令来实现:

[donnie@localhost ~]$ cd /etc
[donnie@localhost etc]$ sudo rm resolv.conf 
[sudo] password for donnie: 
[donnie@localhost etc]$ sudo ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf
[donnie@localhost etc]$

如果此时你执行ls -l /etc/resolv.conf命令,你会发现符号链接已损坏。原因是我们还没有启动systemd-resolved.service。因此,resolved 还没有在/run/systemd/resolve/目录中生成动态的resolv.conf文件。

最后的步骤是重启机器,然后测试网络是否正常运行。

现在,假设我们想将这台机器转换为静态配置,并且还想添加一些功能。让我们编辑/etc/systemd/network/99-networkconfig.network文件,并使其如下所示:

[Match]
Name=enp0s3
MACAddress=08:00:27:d2:fb:23
[Network]
Address=192.168.0.51/24
DNSSEC=yes
DNSOverTLS=opportunistic
Gateway=192.168.0.1
DNS=192.168.0.1
DNS=8.8.8.8
DNS=208.67.222.222
DNS=208.67.220.220
IPv6AcceptRA=yes

[Match]部分,我添加了MACAddress=等号行,以确保此配置始终适用于此特定网络适配器。就像我之前做的那样,我通过执行ip a命令来获取 MAC 地址。

[Network]部分,我为其分配了 IP 地址及其子网掩码、默认网关地址和四个 DNS 服务器地址。我还强制这台机器使用DNSSEC,并且在可用时使用DNSOverTLS。保存此文件后,重启机器或执行sudo networkctl reload命令。然后,验证网络是否正常工作。(请注意,在配置 networkd 时无需执行daemon-reload。)

在我们继续之前,我想谈谈你在这里看到的两个奇怪的 DNS 选项。yes,一定要彻底测试,以确保你能访问到所有需要访问的内容。到目前为止,这里运行得很好。让我感到惊讶的是,因为大多数网站仍然没有设置 DNSSEC 加密密钥。然而,公共 DNS 根服务器和顶级域名服务器已配置了 DNSSEC 密钥,因此我们至少可以验证它们的响应。

为了更好地了解 DNSSEC 在公共互联网中的使用范围,访问dnssec-analyzer.verisignlabs.com/,并输入一个网站的域名。这将显示一个简单的图表,展示哪些使用了 DNSSEC,哪些没有。

opportunistic,这允许机器在可用时使用它。(你的 IT 团队需要与公司管理层合作,确定DNSOverTLS的优点是否超过其缺点。)

当你在没有 Netplan 的情况下使用 networkd 时, /run/systemd/network/目录下不会有动态的.network文件,正如你在 Ubuntu 机器上看到的那样。然而,resolved 会在/run/systemd/resolve/目录中创建一个动态的resolv.conf文件。我们不需要去查看它,因为我们已经在/etc/目录中为它创建了一个符号链接。让我们来看看里面有什么内容:

[donnie@localhost etc]$ cat resolv.conf 
. . .
. . .
nameserver 192.168.0.1
nameserver 8.8.8.8
nameserver 208.67.222.222
# Too many DNS servers configured, the following entries may be ignored.
nameserver 208.67.220.220
[donnie@localhost etc]$

哎呀,看来 resolved 只想在其配置中看到最多三个 DNS 服务器。目前,这样就可以了。这将帮助我们稍后看到一些其他需要关注的内容。

好的,我已经给你展示了几个使用 networkd 和 resolved 的简单示例。如果你想真正震惊一下,可以打开 systemd.network 的手册页,看看里面的内容。(是的,就是 systemd.network,而不是 network 后面加上 d。)你可以创建一些非常复杂的设置,并且可以在手册页的底部查看一些示例。我特别感兴趣的是,使用 networkd 你可以做一些过去必须通过 iptablesnftables 完成的事情。在我看来,使用 networkd 做这些事会更简单一些。你还会看到,通过添加 [DHCPSERVER] 部分,networkd 可以作为一个简单的 DHCP 服务器工作。添加 [CAN] 部分允许你控制 [BRIDGE] 配置,从而为容器分配普通 IP 地址,这样外部世界就能访问它们,而不需要使用端口转发。嗯,关于使用 networkd 可以做的事情的列表相当长,我在这里无法涵盖所有内容。

最后,我想让你打开 Ubuntu 服务器机器上的 netplan 手册页。尽管 Netplan 应该是 networkd 的前端,但你会发现通过 Netplan 你能做的事情只是直接使用 networkd 时能够做的一部分。

现在我们已经看过了配置 networkd 和 resolved 的基础知识,让我们来看一对网络诊断工具。

使用 networkctl 和 resolvectl

在 Ubuntu 和 Alma 机器上,你可以尝试两个很酷的工具,它们可以帮助你查看网络配置的状态。要查看网络链接及其状态,可以使用 networkctlnetworkctl list。(list 选项是默认的,因此你不必输入它。) 在 Alma 机器上,你应该能看到类似这样的内容:

[donnie@localhost ~]$ networkctl
IDX LINK     TYPE          OPERATIONAL   SETUP     
  1    lo           loopback      carrier                     unmanaged
  2    enp0s3   ether            routable                   configured
2 links listed.
[donnie@localhost ~]$

在这台机器上,我们只有两个链接。回环链接的 OPERATIONAL 状态显示为 carrier,这意味着该链接是可操作的,但不可路由。SETUP 显示回环链接是 unmanaged,这意味着我们无法重新配置它。enp0s3 链接是我们的正常以太网链接,显示为 routable 且已 configured

在 Ubuntu 服务器机器上,情况有点更有趣。如图所示,这里有更多的链接:

donnie@ubuntu2004:~$ networkctl
IDX LINK                   TYPE       OPERATIONAL    SETUP     
  1    lo                         loopback   carrier                      unmanaged 
  2    enp0s3                 ether         routable                   configured
  3    docker0               bridge       no-carrier                 unmanaged 
  4    cali659bb8bc7b1 ether        degraded                   unmanaged 
  7    vxlan.calico          vxlan       routable                    unmanaged 
5 links listed.
donnie@ubuntu2004:~$

除了两个正常的链接外,还有三个是在安装 docker 软件包时创建的链接。docker0 链接是一个 unmanaged 桥接,目前处于 no-carrier OPERATIONAL 状态。我没有运行任何容器,所以没有人使用它。底部的两个链接是 Kubernetes 的链接,Kubernetes 是 Docker 容器的编排管理器。(calico 是来自 Project Calico 的引用,Project Calico 是维护该 Kubernetes 网络代码的团队。)cali659bb8bc7b1 链接被标记为 degraded,这意味着它处于在线状态,且有 carrier,但仅对链路本地地址有效。

status 选项显示与其关联的 IP 地址、默认网关地址以及我们希望使用的 DNS 服务器的地址。以下是在 Ubuntu 服务器机器上显示的内容:

donnie@ubuntu2004:~$ networkctl status
●   State: routable                                          
  Address: 192.168.0.50 on enp0s3                            
           172.17.0.1 on docker0                             
           10.1.89.0 on vxlan.calico                         
           fe80::a00:27ff:fef2:c17a on enp0s3                
           fe80::ecee:eeff:feee:eeee on cali659bb8bc7b1      
           fe80::648c:87ff:fe5f:94d5 on vxlan.calico         
  Gateway: 192.168.0.1 (Actiontec Electronics, Inc) on enp0s3
      DNS: 192.168.0.1                                       
           8.8.8.8                                           
           208.67.222.222                                    
           208.67.220.220
. . .
. . .

当然,我们已经了解了如何重新加载修改后的网络配置。在 Ubuntu 使用 Netplan 时,你可以执行 sudo netplan applysudo netplan try。在 Alma 机器上,你可以执行 sudo networkctl reload

要查看 DNS 服务器信息,我们可以使用 resolvectl。这里需要注意的主要内容是输出被分成了几个部分。首先是 Global 部分,显示的是 /etc/systemd/resolved.conf 文件中的设置。在 Alma 机器上,这些设置如下所示:

[donnie@localhost ~]$ resolvectl
Global
       LLMNR setting: yes
MulticastDNS setting: yes
  DNSOverTLS setting: no
      DNSSEC setting: allow-downgrade
    DNSSEC supported: yes
          DNSSEC NTA: 10.in-addr.arpa
                      16.172.in-addr.arpa
. . .
. . .

Global 部分之后,每个链接都有自己的设置,这些设置可以覆盖 Global 设置。以下是 Alma 机器上 enp0se 链接的设置:

. . .
. . .
Link 2 (enp0s3)
      Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6
       LLMNR setting: yes
MulticastDNS setting: no
  DNSOverTLS setting: opportunistic
      DNSSEC setting: yes
    DNSSEC supported: yes
  Current DNS Server: 192.168.0.1
         DNS Servers: 192.168.0.1
                      8.8.8.8
                      208.67.222.222
                      208.67.220.220

仔细查看,你会看到一些 Link 设置覆盖了 Global 设置。之前我们看到,当前机器的 resolv.conf 文件有一个警告,提示我们列出了太多的 nameserver,resolved 可能会忽略第四个。但在这里我们看到所有四个 nameserver,因此一切正常。

networkctl 和 resolvectl 都有很多更多的选项。我会让你在 networkctlresolvectl 的手册页中阅读它们。

接下来,我们简要了解一下 networkd 和 resolved 单元文件。

查看 networkd 和 resolved 单元文件

在我们离开之前,我希望你快速查看一下 networkd 和 resolved 的单元文件。以下是它们的列表:

[donnie@localhost system]$ pwd
/lib/systemd/system
[donnie@localhost system]$ ls -l *networkd*
-rw-r--r--. 1 root root 2023 Jun  6 22:26 systemd-networkd.service
-rw-r--r--. 1 root root  640 May 15 12:33 systemd-networkd.socket
-rw-r--r--. 1 root root  752 Jun  6 22:26 systemd-networkd-wait-online.service
[donnie@localhost system]$ ls -l *resolved*
-rw-r--r--. 1 root root 1668 Aug 10 17:19 systemd-resolved.service
[donnie@localhost system]$

我不会花时间为你一一追踪它们,因为现在你应该能够自己完成这项工作了。我的意思是,主要还是要查阅 systemd.directives 手册页,就像我们之前做过的那样。做完这些后,我们就可以结束这部分内容了。

总结

和往常一样,在本章中我们涵盖了很多内容。我们首先对比了 NetworkManager 与 systemd-networkd 和 systemd-resolved。接下来,我们看了如何处理 Ubuntu 上的 Netplan 工具。RHEL 类型的发行版默认使用 NetworkManager,而不使用 Netplan,因此我们查看了如何将 Alma 机器转换为使用 networkd 和 resolved。然后,我们使用了几个诊断工具,最后简要地查看了 networkd 和 resolved 的单元文件。

现在,是时候谈谈 时间 了,我们将在下一章讨论。到时见!

问题

回答以下问题,测试你对本章内容的理解:

  1. 以下哪项说法是正确的?

    A. NetworkManager 更适合服务器,因为它可以快速在网络间切换。

    B. NetworkManager 更适合服务器,因为它更具多功能性。

    C. NetworkManager 更适合桌面和笔记本电脑,因为它可以快速在网络间切换。

    D. networkd 更适合桌面和笔记本电脑,因为它可以快速在网络间切换。

  2. 判断正误:如果 Netplan .yaml 文件中没有renderer:行,系统将默认使用 NetworkManager。

  3. 在一台 Ubuntu 服务器机器上,编辑完网络配置文件后,以下哪项操作你会执行?

    A. sudo netplan reload

    B. sudo networkctl reload

    C. sudo netplan restart

    D. sudo networkctl restart

    E. sudo netplan apply

  4. networkctl命令显示一个链接为degraded时,这意味着什么?

    A. 链接处于离线状态。

    B. 链接在线,但未以全速运行。

    C. 链接在线,但仅对链路本地地址有效。

    D. 链接不可靠。

答案

  1. C

  2. 错误

  3. E

  4. C

深入阅读

在 Ubuntu 上使用 Netplan 配置网络:

第十六章:理解使用 systemd 的时间同步

在现代计算机系统中,保持准确的时间至关重要。为了做到这一点,我们的计算机通过使用某种实现的 网络时间协议 (NTP) 从时间服务器获取当前时间。在本章中,我们将研究这些不同的实现,并讨论每个实现的优缺点。

本章我们将讨论以下主题:

  • 理解准确时间的重要性

  • 比较 NTP 实现

  • 理解 chrony

  • 理解 systemd-timesyncd

  • 理解 精确时间协议 (PTP)

好的,开始吧!

技术要求

在 Ubuntu 和 RHEL 世界中,时间同步的方式有所不同。因此,我们将同时使用 Ubuntu 服务器和 两个 AlmaLinux 虚拟机来观察这两者。

查看以下链接,观看《代码实战》视频:bit.ly/3Dh4byf

理解准确时间的重要性

计算机上的准确时间同步曾经并不是那么重要。我的第一份计算机工作是处理一对晶体管计算机,它们每台都和冰箱差不多大,处理能力比现代智能手机低了几个数量级。没有硬件时钟,也没有 NTP。每次我们重启这些庞然大物时,我们就看着那不怎么准的墙上时钟,手动输入从上面得到的时间。早期个人计算机的情况没有太大变化。就是说,你仍然需要手动设置时间,但它们最终配备了电池供电的硬件时钟,即使关机时钟也能继续保持。

如今,计算机保持准确的时间至关重要。科学计算、日志记录、数据库更新和金融交易都需要准确的时间。某些安全协议,如 Kerberos、DNSSEC 和 传输层安全 (TLS) 也需要准确时间。现代股票交易所使用的自动化交易机器人也需要准确的时间。因此,出于这些和其他原因,人类发明了 NTP。

NTP 的基本概念很容易理解。每个现代操作系统都包括一个 NTP 客户端。每次启动计算机时,NTP 客户端都会从互联网上某个高精度的 NTP 服务器获取正确的时间。为了确保更高的时间准确性,一些组织可能会使用本地时间源,这可以是本地服务器,也可以是类似 GPS 时钟的设备。

有几种软件实现的 NTP。我们来快速比较一下它们。

比较 NTP 实现

ntpd。它创建于 1980 年代,并且长时间为我们提供了良好的服务。你可以在客户端机器上使用它来保持时间同步,或者可以将其设置为时间服务器。然而,它确实有几个缺点,包括在 2017 年代码审计中发现的许多安全问题。

chrony 实现可以作为客户端或服务器使用,它是从零开始创建的,旨在修复 ntpd 的不足。与 ntpd 不同,chrony 具有以下特点:

  • 它在网络连接不稳定或长时间关闭的计算机上运行良好。

  • 它在虚拟机中表现更好。

  • 当硬件时钟振荡器的速度因温度变化而波动时,它能更好地调整自己。

  • 它可以通过使用硬件时间戳和硬件参考时钟实现亚微秒级精度。

RHEL 7 及其克隆版本是首个将 chrony 作为默认时间同步工具而不是 ntpd 的 Linux 发行版。RHEL 8 和 SUSE 发行版也默认使用 chrony

另一个替代方案是 systemd-timesyncd,它是 systemd 生态系统的一部分。与 ntpdchrony 不同,systemd-timesyncd 是一个轻量级的实现,缺少 NTP 所具备的一些功能。例如,你不能用它来设置时间服务器,也不能与硬件时间戳或硬件参考时钟一起使用。所以,你可以忘记用 systemd-timesyncd 获取那种精确到亚微秒的准确度。另一方面,SNTP 和 systemd-timesyncd 可能在大多数情况下已经足够用了。Ubuntu 默认使用 systemd-timesyncd,大多数时候它能正常工作。如果不能,你可以轻松将你的机器切换到 chrony

精确时间协议PTP)不是 NTP 的实现。实际上,它是一个完全不同的协议,旨在实现极其—我指的是真正极其—精确的时间同步。要使用它,你必须在本地网络中拥有一个精确的时间源,并且必须有能够与之协作的交换机和路由器。它通过硬件时间戳和硬件参考时钟来实现皮秒级精度。

好的,这就是我们的概述。现在,让我们稍微谈一下 chrony。我们将在 AlmaLinux 机器上查看它,因为 Alma 默认使用它。

了解 AlmaLinux 机器上的 chrony。

chrony 系统中有两个组件。我们有作为守护进程的 chronyd 和作为用户界面的 chronycchronyd 组件可以在客户端或服务器模式下运行。首先,让我们看一下 chronyd 的单元文件。

chronyd.service 文件

/lib/systemd/system/chronyd.service 文件中有一些有趣的内容。在 [Unit] 部分,我们有:

[Unit]
Description=NTP client/server
Documentation=man:chronyd(8) man:chrony.conf(5)
After=ntpdate.service sntp.service ntpd.service
Conflicts=ntpd.service systemd-timesyncd.service
ConditionCapability=CAP_SYS_TIME

Conflicts=行表示我们不能在同一台机器上运行多个 NTP 实现。如果 systemd 检测到ntpdsystemd-timesyncd正在运行,那么chronyd将无法启动。ConditionCapability=行表示这个服务是在一个非特权账户下运行的,尽管在这个单元文件或/etc/chrony.conf文件中并没有配置非特权用户账户。相反,chronyd被硬编码为在非特权的chrony账户下运行。我们可以通过简单的ps aux命令来确认这一点,如下所示:

[donnie@localhost ~]$ ps aux | grep chrony
chrony       727  0.0  0.1 128912  3588 ?        S    15:23   0:00 /usr/sbin/chronyd
donnie      1901  0.0  0.0  12112  1092 pts/0    R+   16:44   0:00 grep --color=auto chrony
[donnie@localhost ~]$

由于chronyd是以非特权用户账户运行的,我们需要为该非特权用户账户设置CAP_SYS_TIME能力,以便它能够设置系统时间。

接下来,我们来看一下chronyd.service文件的[Service]部分:

[Service]
Type=forking
PIDFile=/run/chrony/chronyd.pid
EnvironmentFile=-/etc/sysconfig/chronyd
ExecStart=/usr/sbin/chronyd $OPTIONS
ExecStartPost=/usr/libexec/chrony-helper update-daemon
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=full

ExecStart=行使用从EnvironmentFile=行引用的文件中获取的选项来启动chronyd。如果我们查看该文件,会发现没有配置任何选项:

[donnie@localhost system]$ cd /etc/sysconfig/
[donnie@localhost sysconfig]$ cat chronyd 
# Command-line options for chronyd
OPTIONS=""
[donnie@localhost sysconfig]$

ExecStartPost=行中引用的chrony-helper程序是一个 shell 脚本,用于从 DHCP 或 DNS 服务器获取 NTP 服务器的地址。目前,这一行对我们没有任何作用。因为chronyd当前配置为连接一个列在/etc/chrony.conf文件中的 NTP 服务器池,如下所示:

[donnie@localhost sysconfig]$ cd /etc/
[donnie@localhost etc]$ cat chrony.conf 
# Use public servers from the pool.ntp.org project.
# Please consider joining the pool (http://www.pool.ntp.org/join.html).
pool 2.cloudlinux.pool.ntp.org iburst
. . .
. . .

[Service]部分的底部,我们可以看到PrivateTmp=yesProtectHome=yesProtectSystem=full行,这些配置增加了安全性。

最后,这是chronyd.service文件的[Install]部分:

[Install]
WantedBy=multi-user.target

好的,这里没有什么特别的内容。它只是标准的WantedBy=行,用来使这个服务在多用户模式下运行。

接下来,我们来看一下chrony.conf文件。

chrony.conf文件

大多数chronyd配置都在/etc/chrony.conf文件中进行。(唯一的例外是在一些少见的情况下,你可能需要在/etc/sysconfig/chronyd文件中配置某些选项。)我不会覆盖文件中的每个选项,因为你可以通过查看chrony.conf的手册页来了解它们。然而,我会指出一些你可能需要重新配置的地方。

默认情况下,chrony.conf配置为从一组位于互联网的时间服务器获取当前时间,如下所示:

pool 2.cloudlinux.pool.ntp.org iburst

iburst选项在末尾允许chronyd在首次启动机器时稍微加快时间同步的速度。大型组织可能会有本地时间服务器,以防止其网络上的所有机器都连接到互联网获取时间。在这种情况下,你需要用本地时间服务器的 IP 地址配置这一行。(当我们设置时间服务器时稍后会看到这一点。)

为了提高时间同步的准确性,你可以通过去掉以下行前面的#符号来启用硬件时间戳:

#hwtimestamp *

唯一的限制是,计算机中的网络接口适配器必须支持硬件时间戳。要验证这一点,可以使用ethtool -T命令,后跟网络接口适配器的名称。以下是在我的一台 2009 年款惠普机器上的示例:

donnie@localhost:~> sudo ethtool -T eth1
Time stamping parameters for eth1:
Capabilities:
 software-transmit     (SOF_TIMESTAMPING_TX_SOFTWARE)
 software-receive      (SOF_TIMESTAMPING_RX_SOFTWARE)
 software-system-clock (SOF_TIMESTAMPING_SOFTWARE)
PTP Hardware Clock: none
Hardware Transmit Timestamp Modes: none
Hardware Receive Filter Modes: none
donnie@localhost:~>

哦,这可不行。没有 PTP 硬件时钟,也没有硬件时间戳。我们来看看我的戴尔 Precision 工作站,它是几年前更新的:

图 16.1 - 我的戴尔 Precision T3610 工作站上的硬件时间戳

是的,这看起来确实好多了。我们看到了 PTP 硬件时钟和硬件时间戳。不好的一点是,目前我无法利用这个功能,因为这台机器运行的是 Lubuntu Linux。Lubuntu 就像 Ubuntu 一样,运行systemd-timesyncd,它无法利用硬件时间戳。但目前没关系。如果我真的需要,我可以很容易地将这台机器切换到chrony。(我稍后会展示如何做。)

现在,让我们跳到chrony.conf文件的底部,在那里我们看到这些行:

# Specify directory for log files.
logdir /var/log/chrony
# Select which information is logged.
#log measurements statistics tracking

在这里,我们可以看到它被配置为将chronyd日志存储在/var/log/chrony/目录中。但是,如果我们现在去那里,我们会看到一个空目录。这是因为底部的那一行,告诉chronyd记录哪些信息,已经被注释掉了。要改变这一点,只需删除行首的#符号,让它看起来像这样:

log measurements statistics tracking

然后,重新启动chronyd

[donnie@localhost ~]$ sudo systemctl restart chronyd
[donnie@localhost ~]$

你现在应该能在/var/log/chrony/目录中看到日志文件:

[donnie@localhost ~]$ cd /var/log/chrony/
[donnie@localhost chrony]$ ls -l
total 12
-rw-r--r--. 1 chrony chrony 2603 Aug 24 14:29 measurements.log
-rw-r--r--. 1 chrony chrony 1287 Aug 24 14:29 statistics.log
-rw-r--r--. 1 chrony chrony  792 Aug 24 14:29 tracking.log
[donnie@localhost chrony]$

这基本上涵盖了基础内容。让我们通过设置一个chronyd时间服务器来让它更高级一些。

设置一个 chronyd 时间服务器

对于这个演示,你需要两台 Alma 虚拟机。我们将设置一台作为时间服务器,另一台使用时间服务器。(理想情况下,我们希望时间服务器有一个静态 IP 地址,但我们现在不考虑这一点。)

在时间服务器机器上,编辑/etc/chrony.conf文件。这里是你将要修改的那一行:

#allow 192.168.0.0/16

删除行首的#并更改网络地址,使其与你自己的地址匹配。对我来说,网络地址是正确的,但子网掩码是错误的。所以,我会将这一行改成这样:

allow 192.168.0.0/24

接下来,重新启动chronyd

[donnie@localhost ~]$ sudo systemctl restart chronyd
[donnie@localhost ~]$

设置时间服务器的最后一步是打开适当的防火墙端口:

[donnie@localhost ~]$ sudo firewall-cmd --permanent --add-service=ntp
success
[donnie@localhost ~]$ sudo firewall-cmd --reload
success
[donnie@localhost ~]$

现在,切换到另一个 Alma 虚拟机并编辑其/etc/chrony.conf文件。注释掉pool行,并添加一行,指向时间服务器虚拟机的 IP 地址。现在这两行应该看起来像这样:

#pool 2.cloudlinux.pool.ntp.org iburst
server 192.168.0.14 iburst

保存文件并重新启动chronyd服务。当你查看chronyd的状态时,你应该能看到这台机器现在从你的时间服务器获取时间。它应该看起来像这样:

[donnie@logserver ~]$ systemctl status chronyd
● chronyd.service - NTP client/server
   Loaded: loaded (/usr/lib/systemd/system/chronyd.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2021-08-24 14:59:43 EDT; 55s ago
 . . .
. . .
Aug 24 14:59:48 logserver chronyd[15558]: Selected source 192.168.0.14
Aug 24 14:59:48 logserver chronyd[15558]: System clock TAI offset set to 37 seconds
[donnie@logserver ~]$

注意

有时需要在这个命令前加上 sudo 才能查看网络时间源的信息。

就是这些。现在让我们换个话题,来看一下 chronyc 客户端工具。

使用 chronyc

你可以使用 chronyc 工具查看关于 chronyd 服务的信息,或者动态配置 chronyd 服务的某些方面。让我们从查看时间服务器的跟踪信息开始:

[donnie@localhost ~]$ chronyc tracking
Reference ID    : 32CDF46C (50-205-244-108-static.hfc.comcastbusiness.net)
Stratum         : 3
Ref time (UTC)  : Tue Aug 24 19:16:00 2021
System time     : 0.000093940 seconds fast of NTP time
Last offset     : -0.000033931 seconds
RMS offset      : 0.000185221 seconds
Frequency       : 10909.050 ppm fast
Residual freq   : +0.002 ppm
Skew            : 0.344 ppm
Root delay      : 0.016927114 seconds
Root dispersion : 0.018588312 seconds
Update interval : 128.6 seconds
Leap status     : Normal
[donnie@localhost ~]$

我不打算在这里讲解所有内容,而是让你通过查看 chronyc 手册页来阅读相关内容。然而,我确实想谈谈顶部的 Reference ID 行。

Reference ID 行只是告诉我们本地时间服务器所同步的远程时间服务器的主机名或 IP 地址。我们看到这个本地时间服务器同步到了一个远程时间服务器,该服务器由 Comcast 或者使用 Comcast 托管的某个组织运营。请注意,这个远程时间服务器是 chrony.conf 文件中配置的池的一部分。

现在,让我们来看一下我们设置的作为本地时间服务器客户端的 Alma 机器:

[donnie@logserver ~]$ chronyc tracking
Reference ID    : C0A8000E (192.168.0.14)
. . .
. . .
[donnie@logserver ~]$

正如预期的那样,我们看到了本地时间服务器的 IP 地址。

sources 命令将显示我们机器可以访问的所有时间服务器。以下是 Alma 机器默认池中的时间服务器:

[donnie@localhost ~]$ chronyc sources
210 Number of sources = 4
MS Name/IP address                 Stratum Poll Reach LastRx Last sample               
===============================================================================
^* 50-205-244-108-static.hf>    2   9   377   349   -551us[ -384us] +/-   43ms
^+ clock.nyc.he.net                    2   8   377    13  +1084us[+1084us] +/-   51ms
^+ t2.time.gq1.yahoo.com         2   9   377    92   +576us[ +576us] +/-   49ms
^+ linode.appus.org                   2   8   377    23   +895us[ +895us] +/-   70ms
[donnie@localhost ~]$

如前所述,我会让你查看 chronyc 手册页,了解所有字段的含义。

到目前为止,我们可以使用普通用户权限查看所有内容。查看其他类型的信息可能需要 sudo 权限,正如我们在时间服务器上看到的那样:

[donnie@localhost ~]$ sudo chronyc clients
[sudo] password for donnie: 
Hostname                      NTP   Drop Int IntL Last     Cmd   Drop Int  Last
===============================================================================
192.168.0.7                    29      0   8   -   129        0      0   -     -
localhost                       0      0   -   -     -       8      0   8   287
[donnie@localhost ~]$

非常酷。我们可以看到我们设置的作为本地时间服务器客户端的虚拟机的 IP 地址。

只是为了好玩,我们来看一下本地时间服务器做了多少工作:

[donnie@localhost ~]$ sudo chronyc serverstats
NTP packets received       : 84
NTP packets dropped        : 0
Command packets received   : 20
Command packets dropped    : 0
Client log records dropped : 0
[donnie@localhost ~]$

这显示了从客户端接收到的 NTP 包和命令包的数量。

这个命令远比我在这里展示的要复杂得多。你最好的选择是阅读 chronyc 手册页来了解它。

这就是 chronydchronyc 的内容了。那么,让我们切换到 Ubuntu 机器,看看 systemd-timesyncd

理解 systemd-timesyncd

Ubuntu 默认使用 systemd-timesyncd。它是一个简单、轻量级的系统,易于配置。在我们深入之前,让我们快速查看一下 systemd-timesyncd.service 文件。

systemd-timesyncd.service 文件

/lib/systemd/system/systemd-timesyncd.service 文件的 [Unit] 部分如下所示:

[Unit]
Description=Network Time Synchronization
Documentation=man:systemd-timesyncd.service(8)
ConditionCapability=CAP_SYS_TIME
ConditionVirtualization=!container
DefaultDependencies=no
After=systemd-sysusers.service
Before=time-set.target sysinit.target shutdown.target
Conflicts=shutdown.target
Wants=time-set.target time-sync.target

注意 ConditionVirtualization=!container 行。ConditionVirtualization= 部分检查操作系统是否在虚拟化环境中运行。在这种情况下,它检查是否在容器中运行。! 放在 container 前面表示否定。换句话说,如果 systemd 检测到该操作系统在容器中运行,那么 systemd-timesyncd 服务就不会启动。

[Service]部分,你会看到比 Alma 机器上的chronyd.service文件中更多与安全相关的参数。由于数量众多,我只能在这里展示其中的一些:

[Service]
AmbientCapabilities=CAP_SYS_TIME
CapabilityBoundingSet=CAP_SYS_TIME
ExecStart=!!/lib/systemd/systemd-timesyncd
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
. . .
. . .
SystemCallFilter=@system-service @clock
Type=notify
User=systemd-timesync
WatchdogSec=3min

这很有道理,因为 Ubuntu 使用 AppArmor 作为其强制访问控制系统,而不是像 Alma 机器那样使用 SELinux。AppArmor 的默认配置提供的保护远不如 SELinux 的默认配置,因此在这个服务文件中加入更多的安全指令是合理的。此外,请注意User=systemd-timesync这一行,它配置了该服务的非特权用户账户。

[Install]部分与我们通常看到的略有不同:

[Install]
WantedBy=sysinit.target
Alias=dbus-org.freedesktop.timesync1.service

与作为multi-user.target的一部分启动不同,systemd-timesyncd作为sysinit.target的一部分启动。因此,它在启动过程中更早启动。

接下来,让我们简要地看一下如何配置systemd-timesyncd

timesyncd.conf 文件

当我说我们会简要讲解时,我确实是指简要。因为配置的内容不多。以下是/etc/systemd/timesyncd.conf文件的全部内容:

[Time]
#NTP=
#FallbackNTP=ntp.ubuntu.com
#RootDistanceMaxSec=5
#PollIntervalMinSec=32
#PollIntervalMaxSec=2048

一切都被注释掉了,这意味着所有设置都使用默认值。首先需要注意的是,NTP=这一行没有设置,FallbackNTP=这一行指向了一个时间服务器池ntp.ubuntu.com。因此,这台机器只会从该池中的一个时间服务器获取时间。剩下的三个参数设置了合理的默认值,通常你不需要更改它们。(我会让你去阅读timesyncd.conf手册页来了解它们。)

目前关于这个文件的内容已经足够了。现在,让我们看一下几个timedatectl选项。

使用 timedatectl

有两个timedatectl查看选项是特定于systemd-timesyncd的。timesync-status选项看起来像这样:

donnie@ubuntu2004-staticip:/etc/systemd$ timedatectl timesync-status
       Server: 91.189.94.4 (ntp.ubuntu.com)
Poll interval: 32s (min: 32s; max 34min 8s)
         Leap: normal                      
      Version: 4                           
      Stratum: 2                           
    Reference: 8CCBCC4D                    
    Precision: 1us (-23)                   
Root distance: 45.074ms (max: 5s)          
       Offset: -336.094ms                  
        Delay: 101.668ms                   
       Jitter: 1.560ms                     
 Packet count: 214                         
    Frequency: -500.000ppm                 
donnie@ubuntu2004-staticip:/etc/systemd$

在文件顶部,我们看到了这台机器访问的远程时间服务器,并且它是ntp.ubunutu.com池的成员。进一步来看,我们看到从时间服务器获取的Rootdistance为 45.07 毫秒,远低于timesyncd.conf文件中设置的五秒。

另一个timedatectl选项是show-timesync,其输出类似于以下内容:

donnie@ubuntu2004-staticip:~$ timedatectl show-timesync
FallbackNTPServers=ntp.ubuntu.com
ServerName=ntp.ubuntu.com
ServerAddress=91.189.89.198
RootDistanceMaxUSec=5s
PollIntervalMinUSec=32s
PollIntervalMaxUSec=34min 8s
PollIntervalUSec=32s
NTPMessage={ Leap=0, Version=4, Mode=4, Stratum=2, Precision=-23, RootDelay=1.129ms, RootDispersion=30.349ms, Reference=11FD227B, OriginateTimestamp=Tue 2021-08-24 17:16:48 EDT, ReceiveTimestamp=Tue 2021-08-24 17:16:48 EDT, TransmitTimestamp=Tue 2021-08-24 17:16:48 EDT, DestinationTimestamp=Tue 2021-08-24 17:16:48 EDT, Ignored=no PacketCount=1, Jitter=0 }
Frequency=-32768000
donnie@ubuntu2004-staticip:~$

这显示了与timesync-status选项相同的信息,只不过现在是机器可读的格式。

接下来,让我们编辑/etc/systemd/timesyncd.conf文件,使这台机器从我们的本地 AlmaLinux 时间服务器获取时间。我们只需取消注释#NTP=这一行,并添加 Alma 机器的 IP 地址。现在它应该看起来像这样:

NTP=192.168.0.14

在重启systemd-timesyncd服务后,我们应该看到这台机器现在从我们的本地时间服务器获取时间,正如我们在这里看到的:

donnie@ubuntu2004-staticip:~$ timedatectl timesync-status
       Server: 192.168.0.14 (192.168.0.14) 
Poll interval: 32s (min: 32s; max 34min 8s)
         Leap: normal                      
      Version: 4                           
      Stratum: 3                           
    Reference: 32CDF46C                    
    Precision: 1us (-25)                   
Root distance: 27.884ms (max: 5s)          
       Offset: -279.517ms                  
        Delay: 470us                       
       Jitter: 0                           
 Packet count: 1                           
    Frequency: -500.000ppm                 
donnie@ubuntu2004-staticip:~$

很有可能,systemd-timedatectl已经满足了你的所有需求。但如果你确实需要chrony带来的额外功能和精度呢?那么,让我们看看是否可以将我们的 Ubuntu 机器切换到chrony

配置 Ubuntu 使用 chrony

第一步是停止并禁用systemd-timesyncd,如下面所示:

donnie@ubuntu2004-staticip:~$ sudo systemctl disable --now systemd-timesyncd
Removed /etc/systemd/system/dbus-org.freedesktop.timesync1.service.
Removed /etc/systemd/system/sysinit.target.wants/systemd-timesyncd.service.
donnie@ubuntu2004-staticip:~$

现在,安装chrony软件包,如下所示:

donnie@ubuntu2004-staticip:~$ sudo apt install chrony

由于这是 Ubuntu,chronyd服务将在安装完成后自动启用并启动。与在 Alma 机器上看到的唯一不同之处在于,Ubuntu 上的chrony.conf文件位于/etc/chrony/目录。

有时候,你只需要精确一些。那么,我们来聊一聊 PTP。

理解精确时间协议

对于许多金融、科学和企业应用,你必须获得最精确的时间。在这些情况下,从互联网上的远程时间服务器获取时间是不足够的。所以,你需要更好的解决方案。通过适当的硬件,PTP可以将你的网络时间同步到皮秒级精度。PTP 的整个解释相当复杂,因此让我简化一下。

PTP 概述

与 NTP 不同,PTP 不能从互联网上的远程时间服务器获取时间。相反,PTP 只能在同步消息发送到网络的范围内使用。客户端设备将通过发送延迟请求消息进行响应,主时钟将通过延迟响应消息进行回应。携带这些消息的网络数据包都具有时间戳,这些时间戳将用于计算如何调整网络设备上的时间。为了使这一切正常工作,你的网络必须配置有能够传输这些消息的交换机和路由器。

除了主时钟之外,PTP 网络上还可以找到其他三种类型的时钟:

  • 普通时钟:这些时钟位于终端用户设备上,如服务器、桌面客户端、物联网设备等。

  • 透明时钟:这些是传递消息的网络交换机,用于在主时钟和普通时钟之间传输信息。透明时钟不能发送超出其 VLAN 边界的消息。

  • 延迟请求消息。

可以将 Linux 服务器设置为边界时钟,但你可能不会这样做。很可能,你的组织将从其首选的网络设备供应商(如 Cisco 或 Juniper)处获得透明时钟和边界时钟。那么,你如何在 Linux 上使用 PTP 呢?通常,你只需要在服务器、桌面机器和物联网设备上设置 PTP,使它们从 PTP 服务器获取时间,而不是从 NTP 服务器获取时间。让我们来看看吧。

安装 PTP

要将 Linux 服务器、Linux 桌面或 Linux 物联网设备设置为从 PTP 源获取时间,你需要安装linuxptp软件包。在 Alma 机器上,你可以执行:

[donnie@logserver ~]$ sudo dnf install linuxptp

在 Ubuntu 机器上,你可以执行:

donnie@ubuntu2004:~$ sudo apt install linuxptp

接下来,停止并禁用你机器上正在运行的任何时间同步服务。如果你的机器正在运行chronyd,命令如下:

[donnie@logserver ~]$ sudo systemctl disable --now chronyd

如果你的机器正在运行systemd-timesyncd,命令如下:

donnie@ubuntu2004:~$ sudo systemctl disable --now systemd-timesyncd

安装linuxptp软件包会安装两个不同的服务,即ptp4l服务和phc2sys服务。在启用或启动 PTP 服务之前,我们需要先配置它们。让我们看看如何在 Alma 系统上配置。

在 AlmaLinux 上配置 PTP,使用软件时间戳

第一步是编辑/etc/sysconfig/ptp4l文件。第一次打开文件时,你会看到如下内容:

OPTIONS="-f /etc/ptp4l.conf -i eth0"

这个默认配置是为主服务器设置的,但它有错误的网络适配器名称。我们将添加-s选项使其以客户端模式运行,并更改网络适配器的名称。即使主机计算机的网络适配器支持硬件时间戳,你的虚拟机也无法使用硬件时间戳。为了解决这个问题,我们还将添加-S选项,使用软件时间戳。编辑后的行应该如下所示:

OPTIONS="-f /etc/ptp4l.conf -S -s -i enp0s3"

(当然,请使用你自己网络适配器的名称代替我的名称。)

现在,启用并启动ptp4l服务:

[donnie@logserver ~]$ sudo systemctl enable --now ptp4l

即使我的网络中没有 PTP 时间源,服务仍然在运行。不管怎样,systemctl status输出的最后一行显示ptp4l服务选择了最好的主时钟。我不知道那个时钟在哪里,但这并不重要。在实际场景中,你会知道,因为你会在处理一个真实的时钟:

[donnie@logserver ~]$ systemctl status ptp4l
● ptp4l.service - Precision Time Protocol (PTP) service
   Loaded: loaded (/usr/lib/systemd/system/ptp4l.service; enabled; vendor preset: disabled)
   Active: active (running) since Wed 2021-08-25 18:16:26 EDT; 8s ago
 Main PID: 1841 (ptp4l)
    Tasks: 1 (limit: 4938)
   Memory: 276.0K
   CGroup: /system.slice/ptp4l.service
           └─1841 /usr/sbin/ptp4l -f /etc/ptp4l.conf -S -s -i enp0s3
. . .
. . .
Aug 25 18:16:33 logserver ptp4l[1841]: [5697.998] selected local clock 080027.fffe.94a66f as best master
[donnie@logserver ~]$

好的,软件时间戳的部分没问题。现在,让我们来看一下硬件时间戳部分。

在 AlmaLinux 上配置 PTP,使用硬件时间戳

使用硬件时间戳能够提供最精确的时间同步。唯一的问题是,你的计算机上的网络接口适配器必须能够支持硬件时间戳。幸运的是,对于较新的计算机来说,这通常不是问题。(在理解 chrony部分,我向你展示了如何验证你的网络适配器是否支持硬件时间戳。)

第一步是编辑/etc/sysconfig/ptp4l文件,就像之前做的那样。这次,省略-S选项,使编辑后的行如下所示:

OPTIONS="-f /etc/ptp4l.conf -s -i enp0s3"

接下来,你需要配置并启用phc2sys服务,以便计算机时钟可以与网络适配器中的 PTP 硬件时钟同步。第一步是配置/etc/sysconfig/phc2sys文件。默认情况下,文件内容如下:

OPTIONS="-a -r"

将该行更改为如下所示:

OPTIONS="-c CLOCK_REALTIME -s enp0s3 -w"

以下是解释:

  • -c CLOCK_REALTIME-c选项指定要同步的时钟。CLOCK_REALTIME是普通计算机时钟。

  • -s enp0s3:在这个文件中,-s指定了用于同步的设备。在本例中,我们使用的是位于enp0s3网络适配器中的 PTP 硬件时钟来同步普通系统时钟。

  • -w:这告诉phc2sys服务在尝试同步系统时钟之前,等待ptp4l服务进入同步状态。

最后的步骤是重启ptp4l服务,并启用并启动phc2sys服务。请注意,在你的虚拟机上这一步会失败,因为 VirtualBox 网络适配器没有 PTP 硬件时钟。当你看到你需要看到的内容时,禁用ptp4lphc2sys服务,并重新启用chronyd服务。

接下来,我们看看如何在 Ubuntu 上执行这些操作。

在 Ubuntu 上配置带有软件时间戳的 PTP

在 Ubuntu 上没有补充的 PTP 配置文件,因此你需要编辑ptp4l.service文件。从以下操作开始:

donnie@ubuntu2004:~$ sudo systemctl edit --full ptp4l

[Service]部分,你需要更改ExecStart行,格式如下:

ExecStart=/usr/sbin/ptp4l -f /etc/linuxptp/ptp4l.conf -i eth0

将其更改为类似这样的形式:

ExecStart=/usr/sbin/ptp4l -f /etc/linuxptp/ptp4l.conf -S -s -i enp0s3

最后,启用并启动ptp4l服务,就像你之前在 Alma 机器上所做的那样。

现在,让我们通过在 Ubuntu 上配置硬件时间戳来结束这部分内容。

在 Ubuntu 上配置带有硬件时间戳的 PTP

再次,从编辑ptp4l.service文件开始。这次,启用硬件时间戳,通过去掉-S选项,使得ExecStart行如下所示:

ExecStart=/usr/sbin/ptp4l -f /etc/linuxptp/ptp4l.conf -s -i enp0s3

接下来,通过以下操作编辑phc2sys.service文件:

donnie@ubuntu2004:~$ sudo systemctl edit --full phc2sys

[Service]部分,将ExecStart行设置为如下所示:

ExecStart=/usr/sbin/phc2sys -c CLOCK_REALTIME -s enp0s3 -w

最后的步骤是重启ptp4l服务,并启用并启动phc2sys服务。遗憾的是,由于 VirtualBox 网络适配器中没有 PTP 硬件时钟,这一步也会失败。当你看到你想要看到的内容时,可以将虚拟机恢复到你之前使用的任何时间同步服务。

好了,时间同步部分就到这里。我认为是时候结束这一章了。

总结

正如往常一样,我们讨论了很多内容,并且过程中也很有趣。我们首先讨论了为什么准确的时间同步如此重要,然后简要概述了各种时间同步软件的实现。接着,我们详细了解了chronysystemd-timesyncd。最后,我们快速了解了 PTP。

在下一章,我们将讨论 systemd 与引导管理器和引导加载程序的关系。我们在那里见。

问题

回答以下问题,测试你对本章内容的理解:

  1. chrony.conf文件中,以下哪一行将允许chronyd作为时间服务器运行?

    A. network 192.168.0.0/24

    B. allow 192.168.0.0/24

    C. permit 192.168.0.0/24

    D. listen 192.168.0.0/24

  2. 你将如何设置systemd-timesyncd作为时间服务器?(我们假设我们在192.168.0.0/24网络上。)

    A. 在timesyncd.conf文件中添加一行network 192.168.0.0/24

    B. 在timesyncd.conf文件中添加一行permit 192.168.0.0/24

    C. 在timesyncd.conf文件中添加一行allow 192.168.0.0/24

    D. 你不能。

  3. 在处理 PTP 时,以下哪种时钟类型允许消息在 PTP 主时钟和同一 VLAN 上的客户端机器之间流动?

    A. 边界时钟

    B. 大师时钟

    C. 路由器时钟

    D. 透明时钟

  4. 在处理 PTP 时,哪项服务会使机器的系统时钟与网络适配器中的 PTP 硬件时钟同步?

    A. phc2sys

    B. ptp4l

    C. ptp

    D. 时钟

答案

  1. B

  2. D

  3. D

  4. A

进一步阅读

要了解本章所涵盖的更多内容,请查看以下资源:

第十七章:理解 systemd 和引导加载程序

引导加载程序对任何操作系统都是必需的,包括 Linux。在本章中,我们将讨论 GRUB2 和 systemd-boot 引导加载程序,并讨论它们之间的区别。熟悉本章内容可以帮助你选择最适合自己需求的引导加载程序,并解决可能出现的问题。

本章我们将涵盖以下主题:

  • 理解基本的计算机架构

  • 理解 GRUB2

  • 理解 systemd-boot

  • 理解安全启动

请注意,目前使用的引导加载程序有多个,其中一些是特定于嵌入式和物联网设备的。本章我们只会集中讨论 GRUB2 和 systemd-boot,它们用于服务器和普通工作站。

现在,让我们开始吧!

技术要求

我们将从一直使用的 Ubuntu Server 和 Alma 虚拟机开始。我们将用它们来看一个基于 BIOS 的普通 GRUB2 配置。

要查看 GRUB2 引导加载程序如何在基于 EFI 的机器上工作,你需要创建一对启用了 EFI 功能的 Alma 和 Ubuntu Server 虚拟机。为此,像平常一样创建 Alma 和 Ubuntu 虚拟机的初始 VirtualBox 设置。然后,在启动机器并安装操作系统之前,打开 设置 对话框。在 系统 菜单下,勾选 启用 EFI 复选框,如下所示:

图 17.1 – 勾选启用 EFI 框

然后,像平常一样安装操作系统。

要查看 systemd-boot 环境,你需要创建一个安装了 Pop!_OS Linux 的虚拟机。像为 Alma 和 Ubuntu 虚拟机一样启用 EFI 功能,并像平常一样安装操作系统。

注意

Pop!_OS Linux 是由计算机厂商 System76 基于 Ubuntu 源代码构建的。Pop!_OS 是我知道的唯一一款默认使用 systemd-boot 的 Linux 发行版。你可以使用 GRUB2 或 systemd-boot 安装 Clear Linux 和 Arch Linux,但安装它们比我们现在要处理的要复杂得多。

你可以从这里下载 Pop!_OS:

pop.system76.com/

现在你已经有了虚拟机,让我们简要定义一下我们需要了解的几个术语。

查看以下链接,观看《代码实战》视频:bit.ly/3pkVA8D

理解基本的计算机架构

在我们讨论引导加载程序之前,我们需要定义一些描述基本计算机架构的术语:

  • init 系统。

  • 引导管理器:当你第一次开启计算机时,引导管理器会显示一个引导菜单。如果你安装了多个操作系统,引导管理器会让你选择启动哪个系统。如果一个 Linux 发行版安装了多个内核,引导管理器也会让你选择启动哪个内核。

  • BIOS基本输入输出系统BIOS)是位于计算机主板上的固件。它包含了启动计算机的基本指令。计算机启动后,BIOS 将执行上电自检POST),以验证硬件是否正常工作。然后,BIOS 会启动启动加载器。它在当时表现良好,但现在已经过时。一个问题是它无法处理大于两 TB 的硬盘。如果你在一台 BIOS 基础的机器上安装了三 TB 的硬盘,虽然可以使用这个硬盘,但其中一个 TB 的空间会浪费掉。BIOS 也无法处理安全启动功能。

  • EFI/UEFI:最初称为可扩展固件接口EFI),但在版本 2 变体中名称被更改为统一可扩展固件接口UEFI)。它已经取代了较新的计算机上的 BIOS。与 BIOS 不同,EFI/UEFI 在处理非常大的硬盘时表现良好。它还与安全启动功能兼容。

  • MBR:分区类型大致分为两类。主引导记录MBR)类型是较旧的类型。它的主要缺陷是无法处理大于两 TB 的分区。即使你有一台基于 EFI/UEFI 的机器,可以支持大硬盘,MBR 仍然将你限制在较小的分区中。有点令人困惑的是,MBR 这个术语也指代硬盘的第一个 512 字节扇区,这里是 BIOS 基础机器上安装启动加载器的地方。

  • GPTGUID 分区表GPT)类型的分区已取代旧的 MBR 类型。它在处理大于两 TB 的分区时表现良好。(确切的最大分区大小取决于你用于格式化分区的文件系统。)在 EFI/UEFI 机器上,你需要将启动加载器安装在 GPT 分区中,而不是 MBR 中。(稍后我会解释为什么我使用启动加载器而不是启动加载程序。)

  • GRUB2GRUB2Grand Unified Bootloader Version 2)目前是笔记本、台式机和服务器上最流行的启动加载器。它在安装了多个操作系统的机器上表现良好。它不是 systemd 生态系统的一部分,但可以在 systemd 机器上使用。

  • systemd-boot:这个启动加载器是 systemd 生态系统的一部分。它目前还不被广泛使用,但未来有可能会变得流行。它比 GRUB2 更轻量级,配置更简单,也非常适合安装了多个操作系统的机器。

好的,现在我们已经弄清楚了术语,接下来看看 GRUB2。

理解 GRUB2

原始的 GRUB,现在被称为 GRUB Legacy,首次出现在 1995 年,作为替代旧的 LILO 启动加载程序。它易于使用,因为它配置简单,并且在所有使用它的 Linux 发行版中实现方式一致。与 LILO 不同,它可以启动非 Linux 操作系统。所以,你可以在同一台计算机上安装 Windows 和 Linux,GRUB 让你选择启动哪个系统。GRUB Legacy 在旧的基于 BIOS 的计算机上运行良好,但在新的 EFI/UEFI 计算机上无法使用。(实际上,Fedora 团队确实创建了一个可以与 EFI/UEFI 一起使用的 GRUB Legacy 分支版本,但他们在 2013 年放弃了这个版本,转而使用 GRUB2。)

GRUB2 不是 GRUB Legacy 的更新版本。相反,它是一个全新的启动加载程序,从头开始创建。现在,我得告诉你,它有优点也有缺点。优点是它可以与新的 EFI/UEFI 计算机一起工作。缺点是它的配置要复杂得多,而且不同的 Linux 发行版实现方式不同。因此,当你需要处理多个发行版时,可能会感到有些困惑。

几乎所有 Linux 发行版,包括我们一直在使用的 Ubuntu 和 Alma 发行版,都使用 GRUB2。在基于 BIOS 的机器上,GRUB2 被安装到主驱动器的 MBR 中,即驱动器的前 512 字节扇区。在 EFI/UEFI 机器上,GRUB2 被安装到一个特殊的 EFI 分区中,该分区必须是 GPT 类型的分区。(这个特殊的分区被称为 EFI 系统分区ESP。)

现在,事情变得有些复杂。正如我所说的,与 GRUB Legacy 不同,GRUB2 在不同的 Linux 发行版中并不是以相同的方式实现的,稍后我们会看到。为了了解这一切是如何工作的,我们先从比较在基于 BIOS 和 EFI/UEFI 的 AlmaLinux 虚拟机上的 GRUB2 设置开始。

比较基于 BIOS 和 EFI/UEFI 系统的 GRUB2

在基于 BIOS 和 EFI/UEFI 的机器上,Linux 内核和 initramfs 文件被安装到 /boot 分区。但这里是相似之处的终点。我们来看一下在 BIOS 上是如何做的。

基于 BIOS 的 Alma 8 机器上的 GRUB2

在基于 BIOS 的机器上,/boot 分区通常是 /dev/sda1,如我们所见:

[donnie@alma-bios ~]$ mount | grep 'boot'
/dev/sda1 on /boot type xfs (rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota)
[donnie@alma-bios ~]$

我们还看到 /boot 分区仅使用普通的 Linux 文件系统格式化。在 RHEL 类型的机器上,这通常是 xfs。在 Ubuntu 机器上,这通常是 ext4

主引导记录(MBR),即启动加载程序安装的位置,并不是一个分区。相反,MBR 只是驱动器的前 512 字节。GRUB2 配置文件(grub2.cfg)位于 /boot/grub2/ 目录下。在 RHEL 类型的机器上,/etc/grub.cfg 的符号链接指向实际的配置文件,如我们所见:

[donnie@alma-bios etc]$ sudo ls -l grub2.cfg 
lrwxrwxrwx. 1 root root 22 Mar 15 14:28 grub2.cfg -> ../boot/grub2/grub.cfg
[donnie@alma-bios etc]$

但请理解,如果你需要重新配置 GRUB2,你永远不会编辑这个 grub.cfg 文件。相反,你需要编辑 /etc/default/grub 文件。然后,你可以通过以下命令重新构建 grub.cfg 文件:

[donnie@alma-bios ~]$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg

/boot/ 目录下有一个 efi/ 目录,但它并未被使用。它包含一些子目录,但没有文件,如下所示:

[donnie@alma-bios ~]$ sudo ls -l /boot/efi/EFI/almalinux
total 0
[donnie@alma-bios ~]$

每当我们启动机器时,都会看到一个包含不同引导选项的引导菜单:

图 17.2 – AlmaLinux 上的 GRUB2 引导菜单

这些菜单选项的配置文件位于 /boot/loader/entries/ 目录下。该目录需要 root 权限才能进入。因此,为了简化操作,我们先进入 root shell:

[donnie@alma-bios ~]$ sudo su -
[sudo] password for donnie: 
Last login: Sat Aug 28 18:09:25 EDT 2021 on pts/0
[root@alma-bios ~]# cd /boot/loader/entries/
[root@alma-bios entries]#

现在,让我们看看我们有什么:

[root@alma-bios entries]# ls -l
total 16
-rw-r--r--. 1 root root 388 Apr  5 12:09 3a17f34dc2694acda37caa478a339408-0-rescue.conf
-rw-r--r--. 1 root root 368 Apr  5 13:20 3a17f34dc2694acda37caa478a339408-4.18.0-240.15.1.el8_3.x86_64.conf
-rw-r--r--. 1 root root 368 Apr 11 18:23 3a17f34dc2694acda37caa478a339408-4.18.0-240.22.1.el8_3.x86_64.conf
-rw-r--r--. 1 root root 316 Apr  5 12:09 3a17f34dc2694acda37caa478a339408-4.18.0-240.el8.x86_64.conf
[root@alma-bios entries]#

这些配置文件被称为 BootLoaderSpec (BLS) 文件。每当你启动机器时,GRUB2 会从这些 BLS 文件中获取信息,并用它来填充引导菜单。每次安装新的 Linux 内核时,都会自动生成一个新的 BLS 文件,即使是你自己编译的内核也是如此。如果你进行系统更新并且 dnf 删除了任何较旧的内核,那么那些较旧内核的 BLS 文件也会被删除。让我们看一下其中一个文件,看看里面有什么内容:

[root@alma-bios entries]# cat 3a17f34dc2694acda37caa478a339408-4.18.0-240.22.1.el8_3.x86_64.conf 
title AlmaLinux (4.18.0-240.22.1.el8_3.x86_64) 8.3 (Purple Manul)
version 4.18.0-240.22.1.el8_3.x86_64
linux /vmlinuz-4.18.0-240.22.1.el8_3.x86_64
initrd /initramfs-4.18.0-240.22.1.el8_3.x86_64.img $tuned_initrd
options $kernelopts $tuned_params
id almalinux-20210409120623-4.18.0-240.22.1.el8_3.x86_64
grub_users $grub_users
grub_arg --unrestricted
grub_class kernel
[root@alma-bios entries]#

正如我们所见,这个文件定义了要加载的内核和 initramfs 镜像,以及各种内核选项。但我们没有看到具体的内核选项。相反,我们看到的是以 $ 为前缀的变量名。这意味着内核选项信息将从 /boot/grub2/grub.cfg 文件和 /boot/grub2/grubenv 文件中获取。

如果你在已经安装了其他操作系统的计算机上安装 Linux,那么该操作系统的引导菜单项也应该会自动创建。(即使另一个操作系统是 Windows,这也有效。)/etc/grub.d/30_os-prober 脚本是用来为你找到其他操作系统的。

注意

这与你可能习惯的情况不同。旧版 Linux 发行版,如 RHEL 7 系列的发行版,不使用 BLS 文件。相反,所有的引导菜单信息都列在 grub.cfg 文件中。Red Hat 在 Fedora 30 中首次引入了 BLS 文件,现在所有 RHEL 8 系列的发行版都使用它们。(正如我们稍后将看到的,即使是最新的 Ubuntu 发行版仍然没有使用它们。)

现在,让我们来看一下 EFI/UEFI 机器。

在基于 EFI/UEFI 的 Alma 机器上的 GRUB2

在我们的 EFI/UEFI 机器上,/boot/ 分区挂载在 /dev/sda2,而 /boot/efi/ 分区挂载在 /dev/sda1,如图所示:

[donnie@alma-efi ~]$ mount | grep 'boot'
/dev/sda2 on /boot type xfs (rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota)
/dev/sda1 on /boot/efi type vfat (rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=ascii,shortname=winnt,errors=remount-ro)
[donnie@alma-efi ~]$

/boot/efi/ 分区是引导加载程序所在的位置。我们还可以看到,正常的引导分区使用的是常规的 xfs Linux 文件系统格式化,但 /boot/efi/ 分区是使用 vfat 文件系统格式化的。efi 分区必须始终使用 vfat 格式化,因为其他格式无法正常工作。

接下来,我们看到 /etc/ 目录中的符号链接有一个不同的名称,并且指向一个不同位置的 grub.cfg 文件:

[donnie@alma-efi etc]$ sudo ls -l grub2-efi.cfg 
lrwxrwxrwx. 1 root root 34 Mar 15 14:28 grub2-efi.cfg -> ../boot/efi/EFI/almalinux/grub.cfg
[donnie@alma-efi etc]$

正如之前所说,我们想要查看的目录需要 root 权限才能进入。为了简化操作,我们先进入 root shell:

[donnie@alma-efi ~]$ sudo su -
Last login: Sat Aug 28 17:52:08 EDT 2021 on pts/0
[root@alma-efi ~]#

仍然存在一个/boot/grub2/目录,但它唯一包含的内容是指向 GRUB 环境设置文件的符号链接:

[root@alma-efi ~]# cd /boot/grub2/
[root@alma-efi grub2]# ls -l
total 0
lrwxrwxrwx. 1 root root 28 Mar 15 14:28 grubenv -> ../efi/EFI/almalinux/grubenv
[root@alma-efi grub2]#

几乎所有其他重要的内容都在/boot/efi/目录中:

[root@alma-efi ~]# cd /boot/efi
[root@alma-efi efi]# ls
EFI
[root@alma-efi efi]# cd EFI/
[root@alma-efi EFI]# ls
almalinux  BOOT
[root@alma-efi EFI]#

在这个嵌套结构的底部,我们看到/boot/efi/EFI/almalinux//boot/efi/EFI/BOOT/目录。我们来看看BOOT/目录:

[root@alma-efi EFI]# ls -l BOOT/
total 1568
-rwx------. 1 root root 1237503 Mar 15 14:44 BOOTX64.EFI
-rwx------. 1 root root  362968 Mar 15 14:44 fbx64.efi
[root@alma-efi EFI]# 

BOOTX64.EFI文件是shim系统的一部分,允许 Linux 在启用了安全启动功能的机器上启动。(我们将在本章末讨论安全启动。)fbx64.efi文件是回退启动加载器。它的作用是重新创建内置于固件中的启动管理器选项,以防它们被意外删除。它通过扫描BOOTX64.CSV文件来完成这项工作,这些文件位于子目录中,包含任何已安装操作系统的条目。

现在,以下是我们在almalinux/目录中看到的内容:

[root@alma-efi EFI]# ls -l almalinux/
total 5444
-rwx------. 1 root root     122 Mar 15 14:44 BOOTX64.CSV
drwx------. 2 root root    4096 Mar 15 14:28 fonts
-rwx------. 1 root root    6572 Aug 26 18:13 grub.cfg
-rwx------. 1 root root    1024 Aug 28 17:51 grubenv
-rwx------. 1 root root 1900112 Mar 15 14:28 grubx64.efi
-rwx------. 1 root root 1171320 Mar 15 14:44 mmx64.efi
-rwx------. 1 root root 1240144 Mar 15 14:44 shimx64-almalinux.efi
-rwx------. 1 root root 1237503 Mar 15 14:44 shimx64.efi
[root@alma-efi EFI]#

除了我们在 BIOS 机器上看到的正常GRUB2文件外,我们还看到了几个特定于 EFI/UEFI 机器的文件:

  • grubx64.efi:这使得 GRUB2 能够在 EFI/UEFI 机器上运行。

  • shim64-almalinux.efishimx64.efi:这些文件与BOOTX64.EFI文件一起工作,使 Alma 能够在启用了安全启动的机器上运行。

  • mmx64.efi:这是机器所有者密钥系统的一部分,它也有助于安全启动。

  • BOOTX64.CSV:这个文件与回退启动加载器一起工作,并包含此 Alma 安装的启动菜单条目。(如果安装了多个操作系统,它们将有各自的BOOTX64.CSV文件。)如果你查看这个文件的内容,你会看到:

    [root@alma-efi almalinux]# cat BOOTX64.CSV 
    ´´shimx64.efi,AlmaLinux,,This is the boot entry for AlmaLinux
    [root@alma-efi almalinux]#
    

    注意

    需要记住的是,BOOTX64.CSV文件不是像大多数 Linux 配置文件那样的 ASCII 文本文件。(这可以解释你在代码中看到的那两个奇怪的问号。)相反,它是一个 UTF-16 Unicode 文件,正如我们在这里看到的:

    BOOTX64.CSV文件,你需要将其转换为 UTF-16 格式。假设你已经在主目录中创建了一个boot.csv文件。你可以使用iconv工具进行转换,如下所示:

    [donnie@alma-efi ~]$ iconv -t UTF-16 < ~/boot.csv > BOOTX64.CSV

    [donnie@alma-efi ~]$

    现在,你准备好将文件复制到正确的位置了。

接下来,我们有与 BIOS 机器上的启动菜单选项相同的BLS文件:

[donnie@alma-efi ~]$ sudo ls -l /boot/loader/entries/
[sudo] password for donnie: 
total 8
-rw-r--r--. 1 root root 388 Aug 26 18:10 5a1e1f5e83004e9eb0f6e2e0a346d4e7-0-rescue.conf
-rw-r--r--. 1 root root 316 Aug 26 18:10 5a1e1f5e83004e9eb0f6e2e0a346d4e7-4.18.0-240.el8.x86_64.conf
[donnie@alma-efi ~]$

当我们启动一个 EFI/UEFI 机器时,我们会看到启动菜单与在 BIOS 机器上的显示有所不同:

图 17.3 – EFI/UEFI 机器上的启动菜单

我们现在看到一个系统设置选项,它将我们带入到我们在这里看到的 EFI 管理工具:

图 17.4 – EFI 管理工具

这可以为我们做几件事情。如果我们选择启动维护管理器,我们会看到启动选项选项。如果选择它,我们会看到可以添加或删除启动选项,或更改默认启动顺序:

图 17.5 – 启动选项屏幕

如果我们需要从 DVD 或 USB 设备引导,而不是从默认设备引导,这可能会派上用场。

引导管理器选项下,我们可以看到EFI 内部 Shell选项:

图 17.6 – 引导管理器屏幕

这个内部 Shell 可以帮助你排查启动问题,界面如下所示:

图 17.7 – EFI 内部 Shell

要查看你可以在这个 shell 中运行的命令,只需输入 help。我不会详细介绍这个 shell,因为你不会经常使用它。不过,我在进一步阅读部分链接了一个很好的教程。

好的,这就涵盖了 AlmaLinux 机器上的 GRUB2 部分。现在,让我们来看看 Ubuntu 机器上情况是如何有所不同的。

BIOS 和 EFI/UEFI 系统上的 GRUB2(以 Ubuntu 为例)

最大的不同之处在于,Ubuntu 并没有像 RHEL 8 类发行版那样使用 BootLoaderSpec 文件。相反,所有菜单项都定义在 /boot/grub/grub.cfg 文件中。要查看这些菜单项,打开该文件并搜索以 menuentry 开头的段落。以下是其中一个菜单项的片段:

menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-34969a2a-6e3f-4806-8260-e62b948678e3' {
        recordfail
        load_video
        gfxmode $linux_gfx_mode
        insmod gzio
. . .
. . .
        linux   /boot/vmlinuz-5.4.0-81-generic root=UUID=34969a2a-6e3f-4806-8260-e62b948678e3 ro  
        initrd  /boot/initrd.img-5.4.0-81-generic
}

唯一的另一个实际区别是,在 /etc/ 目录下没有指向 grub.cfg 文件的符号链接。

接下来,让我们重启系统,看看好玩的内容。让我们来看一下systemd-boot

理解 systemd-boot

这里需要注意的第一件事是名称,systemd-boot。这真让人震惊,我知道。我们有一个 systemd 组件,它的名称居然没有以字母 d 结尾。但说真的,systemd-boot 是 systemd 的一个组件,具有一些很酷的功能。与 GRUB2 相比,它更轻量,配置更简单,启动更快,并且能很好地与现代的安全启动(Secure Boot)实现兼容。与广泛的看法相反,systemd-boot 是一个引导管理器,而不是引导加载器。它可以自动探测机器上的其他操作系统并将其添加到引导菜单中。(GRUB2 只有在首次安装操作系统时才会这样做,而 systemd-boot 每次启动机器时都会这样做。)一旦你启动了机器并选择了所需的引导选项,systemd-boot 会将引导操作交给一个真正的引导加载器。

那么,为什么它没有被更广泛使用呢?其实是因为 systemd-boot在 EFI/UEFI 系统上工作。现在仍有很多旧的基于 BIOS 的计算机在使用,如果所有操作系统都切换到仅支持 EFI/UEFI 的引导加载器,那么这些旧机器就无法使用了。

你可以在 systemd-boot 手册页中阅读有关 systemd-boot 的各种功能。

对于我们的演示,我们将使用Pop!_OS Linux,这是 System76 公司的一款产品。System76 是一家计算机供应商,所以他们生产只能运行在新型机器上的操作系统是合情合理的。它基于 Ubuntu,因此你可以使用与你习惯使用的 Ubuntu 命令相同的命令,除了涉及引导加载器的命令。(在撰写本文时,它基于 Ubuntu 21.04。)

当我创建 Pop!_OS 虚拟机时,我只是接受了安装程序的默认分区设置。它长这样:

donnie@pop-os:~$ mount | grep 'sda'
/dev/sda3 on / type ext4 (rw,noatime,errors=remount-ro)
/dev/sda2 on /recovery type vfat (rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro)
/dev/sda1 on /boot/efi type vfat (rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro)
donnie@pop-os:~$

这里我们看到/dev/sda1被挂载为/boot/efi/,这是 EFI 系统分区,启动加载程序就存放在这里。我们还看到一个/recovery/分区,这是我在任何 Linux 机器上都没见过的东西。这个/recovery/分区包含了一个 Pop!_OS 的实时版本。如果你需要修复或重新安装操作系统而不丢失用户文件,就从这个分区启动。

与 GRUB2 不同,systemd-boot 在/etc/目录下没有任何配置文件。相反,它们都在/boot/efi/目录下。像之前一样,我们通过进入root shell 来简化操作:

donnie@pop-os:~$ sudo su -
[sudo] password for donnie: 
root@pop-os:~#

这是/boot/efi/目录下的内容:

root@pop-os:~# cd /boot/efi
root@pop-os:/boot/efi# ls -l
total 8
drwx------ 7 root root 4096 Aug 27 14:15 EFI
drwx------ 3 root root 4096 Sep  1 17:11 loader
root@pop-os:/boot/efi#

让我们先看一下loader/子目录:

root@pop-os:/boot/efi/loader# ls -l
total 12
drwx------ 2 root root 4096 Sep  1 15:45 entries
-rwx------ 1 root root   23 Sep  1 17:11 loader.conf
-rwx------ 1 root root  512 Aug 27 14:15 random-seed
root@pop-os:/boot/efi/loader#

稍后我们会回到这两个文件。首先,让我们看看entries/子目录:

root@pop-os:/boot/efi/loader/entries# ls -l
total 12
-rwx------ 1 root root 256 Sep  1 15:48 Pop_OS-current.conf
-rwx------ 1 root root 274 Sep  1 15:48 Pop_OS-oldkern.conf
-rwx------ 1 root root 299 Aug 27 10:13 Recovery-9C63-930A.conf
root@pop-os:/boot/efi/loader/entries#

这三个BootLoaderSpec文件代表了在你启动机器时,启动菜单上出现的三个选项。(我知道你还没见过启动菜单,不过没关系,我们很快就能解决这个问题。)开个玩笑,让我们先看看Pop_OS-current.conf文件:

root@pop-os:/boot/efi/loader/entries# cat Pop_OS-current.conf 
title Pop!_OS
linux /EFI/Pop_OS-bc156c8a-fcb8-4a74-b491-089c77362828/vmlinuz.efi
initrd /EFI/Pop_OS-bc156c8a-fcb8-4a74-b491-089c77362828/initrd.img
options root=UUID=bc156c8a-fcb8-4a74-b491-089c77362828 ro quiet loglevel=0 systemd.show_status=false splash
root@pop-os:/boot/efi/loader/entries#

与 GRUB2 不同,这里没有其他文件来存储内核选项。所以,所有选项都必须存储在这里。你可能觉得有点不寻常的是,这个BLS文件所调用的内核文件具有.efi的文件扩展名。稍后我会解释为什么会这样。

/boot/efi/loader/random-seed文件存储了一个随机种子值。(我敢打赌你绝对猜不到这个。)这允许机器以完全初始化的熵池启动,从而使/dev/urandom设备能够生成更好的随机数。通过允许系统生成更难破解的安全密钥,这增强了安全性。(不过请注意,这个功能在虚拟机上不起作用。)

接下来,让我们看看/boot/efi/loader/loader.conf文件:

root@pop-os:/boot/efi/loader# cat loader.conf 
default Pop_OS-current
root@pop-os:/boot/efi/loader#

等一下,这就这么简单?嗯,是的。(我不是告诉过你 systemd-boot 比 GRUB2 更容易配置吗?)不过,我确实发现了一个小问题。那就是,启动菜单不会显示,除非你在开机后迅速按下正确的按键。让我们编辑这个文件,让启动菜单显示五秒钟。编辑后的文件应该是这样:

default Pop_OS-current
timeout 5

好吧,那有点难。我希望你能处理好。不过,说正经的,还有一些选项你可以在loader.conf的手册页中阅读,配置起来都很简单。(顺便提一下,别急着重启机器。稍后我们还会做一次更改,然后你就可以重启了。)

/boot/efi/EFI/目录下,我们看到了这些子目录:

root@pop-os:/boot/efi/EFI# ls -l
total 20
drwx------ 2 root root 4096 Aug 27 14:15 BOOT
drwx------ 2 root root 4096 Aug 27 14:15 Linux
drwx------ 2 root root 4096 Sep  1 15:45 Pop_OS-bc156c8a-fcb8-4a74-b491-089c77362828
drwx------ 2 root root 4096 Aug 27 14:13 Recovery-9C63-930A
drwx------ 2 root root 4096 Aug 27 14:15 systemd
root@pop-os:/boot/efi/EFI#

Linux/子目录是空的,所以我们不需要看它。在BOOT/子目录中,我们只看到一个文件:

root@pop-os:/boot/efi/EFI/BOOT# ls -l
total 92
-rwx------ 1 root root 94079 Jul 20 14:47 BOOTX64.EFI
root@pop-os:/boot/efi/EFI/BOOT#

正如我们在 Alma 和 Ubuntu 机器上看到的那样,我们有 BOOTX64.EFI 文件,这使得这台计算机能与安全启动一起工作。然而,我们没有备用引导程序文件。

systemd/ 子目录中,我们看到使 systemd-boot 工作的可执行文件:

root@pop-os:/boot/efi/EFI/systemd# ls -l
total 92
-rwx------ 1 root root 94079 Jul 20 14:47 systemd-bootx64.efi
root@pop-os:/boot/efi/EFI/systemd#

最后,我们来看看 Pop_OS-bc156c8a-fcb8-4a74-b491-089c77362828/ 子目录中的内容:

root@pop-os:/boot/efi/EFI/Pop_OS-bc156c8a-fcb8-4a74-b491-089c77362828# ls -l
total 240488
-rwx------ 1 root root       167 Sep  1 15:48 cmdline
-rwx------ 1 root root 108913836 Sep  1 15:48 initrd.img
-rwx------ 1 root root 107842809 Sep  1 15:48 initrd.img-previous
-rwx------ 1 root root  14750528 Sep  1 15:48 vmlinuz.efi
-rwx------ 1 root root  14739488 Sep  1 15:48 vmlinuz-previous.efi
root@pop-os:/boot/efi/EFI/Pop_OS-bc156c8a-fcb8-4a74-b491-089c77362828#

这与你在 GRUB2 机器上看到的完全不同。在这里,我们使用的是内建于 Linux 内核中的 EFI Stub Loader 功能。vmlinuz.efi 文件只是 /boot/vmlinuz-5.11.0-7633-generic 文件的副本,它是最新安装的 Linux 内核。通过将这个内核文件重命名为 .efi 文件扩展名,systemd-boot 实际上将这个内核文件变成了它自己的引导加载程序。(相当巧妙,不是吗?)vmlinuz-previous.efi 文件是 /boot/vmlinuz-5.11.0-7620-generic 文件的副本,它是第二旧的已安装内核。每次我们在这个 systemd-boot 机器上安装一个新内核时,原始副本将放入顶层的 /boot/ 目录,而带 .efi 文件扩展名的副本将放入这个目录。

另一个要注意的地方是,这里没有我们在 Alma 和 Ubuntu 机器上看到的 shimx64*.efi 文件。这是因为 systemd-boot 不需要 shim 系统来与安全启动一起工作。(我将在 理解安全启动 部分更详细地解释这一点。)

好的,我们不再需要根 shell 了,所以输入 exit 回到你的普通用户 shell。

我想向你展示的最后一个 systemd-boot 组件是bootctl工具。要查看 systemd-boot 的状态,可以运行不带任何选项的命令:

donnie@pop-os:~$ sudo bootctl
System:
     Firmware: UEFI 2.70 (EDK II 1.00)
  Secure Boot: disabled
   Setup Mode: user
 Boot into FW: supported
Current Boot Loader:
      Product: systemd-boot 247.3-3ubuntu3.4pop0~1626806865~21.04~19f7a6d
     Features: P Boot counting
               P Menu timeout control
. . .
. . .

使用 list 选项来查看所有的启动菜单条目:

donnie@pop-os:~$ sudo bootctl list
Boot Loader Entries:
        title: Pop!_OS (Pop_OS-current.conf) (default)
           id: Pop_OS-current.conf
       source: /boot/efi/loader/entries/Pop_OS-current.conf
        linux: /EFI/Pop_OS-bc156c8a-fcb8-4a74-b491-089c77362828/vmlinuz.efi
       initrd: /EFI/Pop_OS-bc156c8a-fcb8-4a74-b491-089c77362828/initrd.img
      options: root=UUID=bc156c8a-fcb8-4a74-b491-089c77362828 ro quiet loglevel=0 systemd.show_status=false splash
. . .
. . .        
title: Reboot Into Firmware Interface
           id: auto-reboot-to-firmware-setup
       source: /sys/firmware/efi/efivars/LoaderEntries-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f

使用set-default选项可以永久更改默认启动选项,或使用set-oneshot选项只为下一次启动设置默认启动选项。从输出的启动选项列表中获取你想使用的启动选项 ID,并通过任一选项指定,像这样:

donnie@pop-os:~$ sudo bootctl set-oneshot Pop_OS-oldkern.conf
donnie@pop-os:~$

现在,继续重新启动计算机。这次,你会看到启动菜单出现。五秒钟的菜单计时器到期后,你将看到计算机启动到你通过 bootctl set-oneshot 命令选择的备用内核。

你可以用 bootctl 做更多的事情,但我让你在 bootctl 手册页中自行阅读。

让我们通过简要地了解一下安全启动功能来结束本章。

理解安全启动

安全启动是一个 EFI/UEFI 功能,它防止计算机加载任何未由授权安全密钥签名的操作系统、操作系统模块或设备驱动程序。它有助于防止计算机加载各种类型的恶意软件,如 rootkit。要启用或禁用安全启动,请将计算机启动到设置屏幕,如我在我的最新款 Acer 电脑上所示:

图 17.8 – 启用安全启动的 UEFI 设置屏幕

出于某种奇怪的原因,即使这台机器使用的是 UEFI,它仍然被称为 BIOS 设置实用程序。我希望你注意到的是 TPM 支持 选项,它显示为 [已启用]TPM 代表 受信平台模块,它是主板上的固件芯片,包含 Secure Boot 所需的签名密钥。

在 Windows 机器上,Secure Boot 是一个重要的功能,因为 Windows 传统上非常容易受到恶意软件的感染。Linux 则不太容易受到感染,尽管 Secure Boot 对其也可能有用。自从微软推出 Windows 8 以来,所有预装 Windows 的新计算机默认启用了 Secure Boot。目前,如果需要的话,仍然可以在 Windows 机器上禁用 Secure Boot。但在 Windows 11 中,这将不再是一个选项。

当 Secure Boot 最初面市时,它引起了 Linux 用户的极大不满和强烈反应。这是因为 Secure Boot 是通过查看机器引导程序的加密签名,然后将其与计算机 TPM 中的签名列表进行比较来工作的。好吧,听起来似乎没那么糟糕。糟糕的是,必要的签名和签名密钥在计算机制造时就已加载到 TPM 中,并且微软几乎控制了哪些签名和密钥会被加载。因此,一开始,Windows 是唯一一个在启用 Secure Boot 的机器上能保证启动的操作系统。Linux 开发者不得不想出一种方法,让 Linux 能与 Secure Boot 一起工作。当 Linux 用户得知这意味着必须让微软为 Linux 引导程序发布签名密钥时,他们更是痛苦万分。(没错,就是那个曾经有 CEO 说 Linux 是 癌症 的微软。)

在本章中,尽管你可能还没有意识到,我们已经看到 GRUB2 和 systemd-boot 如何以不同的方式处理 Secure Boot 功能。GRUB2 与 shim 系统配合使用,该系统使用预签名的 shim 文件。在 Alma 机器上,这些文件是 /boot/efi/EFI/almalinux/ 目录下的 shimx64.efishimx64-almalinux.efi 文件。在 Ubuntu Server 机器上,我们只有 /boot/efi/EFI/ubuntu/ 目录中的 shimx64.efi 文件。那么,为什么我们需要这个 shim 系统,而不是直接对 GRUB2 引导程序文件进行签名呢?有两个原因。一个是 GRUB2 本身已经相当臃肿,如果在其上增加 Secure Boot 代码,会使其更加臃肿。另一个原因是 GRUB2 的代码是按照 GPL3 开源许可证发布的。由于某些原因,我并不清楚,微软拒绝为任何 GPL3 许可下的内容提供签名密钥。因此,systemd-boot 是根据 GPL2 许可证发布的,而微软似乎更喜欢这个许可证。

当我们查看 Pop!_OS Linux 时,我指出它没有任何 shimx64*.efi 文件。由于 systemd-boot 是根据 GPL2 发布的,微软会为其文件签名,这使得 shim 系统变得不必要。

好的,我又在读你的心思了,希望这次是最后一次。你在想,但,Donnie。如果我创建了一个内核模块,我需要它在安全启动的机器上加载怎么办?如果我在一个启用了安全启动的机器上安装了 Linux,然后决定不信任任何由微软颁发的签名密钥呢?我该怎么办?

好吧,在这两种情况下,您都可以自己创建签名密钥并将其加载到 TPM 中。这是一个复杂的过程,我无法在这里详细讲解,所以我会参考《Linux EFI 引导程序管理》网站,该网站链接在“进一步阅读”部分。在它的目录中,您会看到指向“安全启动”页面的链接,您将在那里找到具体的操作流程。

好了,大家,这就是引导程序章节的全部内容。让我们总结一下,然后把这部分内容收尾整理好。

总结

正如往常一样,我们在这一章中涵盖了很多内容。我们首先概述了计算机架构,然后讨论了 GRUB2 引导程序在基于 BIOS 和 EFI/UEFI 计算机上的工作原理。接着,我们介绍了在 Pop!_OS Linux 机器上的 systemd-boot,并以对“安全启动”的讨论做了总结。

在下一章,也是我们最后一章中,我们将讨论 systemd-logind。到时候见。

问题

为了测试你对本章内容的掌握,请回答以下问题:

  1. 以下哪个陈述是正确的?

    A. 只有 GPT 分区可以用于基于 BIOS 的计算机。

    B. 只有 GPT 分区才能用于安装 GRUB2。

    C. 只有 GPT 分区才能用于安装 systemd-boot。

    D. 只有 MBR 分区才能用于安装 systemd-boot。

  2. GRUB2 如何与安全启动兼容?

    A. 它使用了 shim 系统。

    B. 它的文件是由微软直接签名的。

    C. GRUB2 不能与安全启动兼容。

  3. systemd-boot 是如何工作的?

    A. 它使用grubx64.efi文件来激活引导程序。

    B. 它将内核文件复制到具有.efi文件扩展名的文件中,以便内核可以充当其自己的引导程序。

    C. 它直接从/boot/目录调用 Linux 内核。

    D. 它根本不起作用。

  4. 要使安全启动工作,必须具备什么条件?

    A. 没有任何作用。它在所有计算机上都能工作。

    B. 机器具有 BIOS 芯片,并且启用了 TPM。

    C. 机器有 EFI/UEFI,并且启用了 TPM。

    D. 没有任何作用。它从未工作过。

答案

  1. C

  2. A

  3. B

  4. C

进一步阅读

若要了解更多关于本章所涵盖的主题,请查看以下资源:

第十八章:理解 systemd-logind

是的,的确如此——systemd 中甚至有一种新的管理用户登录和用户会话的方式。在本章中,我们将深入探讨 systemd-logind,并向你展示一些相当巧妙的用户管理技巧。掌握这些技巧无疑能帮助你在商业环境中发挥作用。本章的具体主题包括:

  • 理解需要一个新的登录服务

  • 理解 systemd-logind.service

  • 理解 logind.conf

  • 理解 loginctl

  • 理解 polkit

好的,让我们开始吧。

技术要求

对于本章,我们不需要任何复杂的配置。只需要使用你常规的 logind.conf 演示,因为在图形模式的机器上重启 systemd-logind 服务是有问题的(稍后我会详细解释)。在本章结束时,会有几个演示需要桌面界面,因此你需要一台带有 Gnome 3 桌面的 Alma 机器来进行这些演示。

好的,让我们开始查看 systemd-logind.service 文件。

查看以下链接,观看《代码演示》视频:bit.ly/3EiIHSD

理解需要一个新的登录服务

我知道,你在想,为什么我们需要一个新的登录服务? 其中一个原因是因为 systemdcgroups 的紧密集成。systemd-logind 服务为我们做了很多事情,但它的主要工作是为每个登录系统的用户创建 cgroup 切片和范围。随着本章的展开,我们还会看到 systemd-logind 为我们做的一些其他事情。(想要了解 systemd-logind 所做所有事情的简短描述,请参阅 systemd-logind 的手册页。)

理解 systemd-logind.service

在 RHEL 类型系统和 Ubuntu 中,单元文件的设置有显著差异。我们首先来看 Alma 机器上 RHEL 类型的配置。

Alma Linux 的 systemd-logind.service 文件

在 Alma 机器上,/lib/systemd/system/systemd-logind.service 文件的 [Unit] 部分如下所示:

[Unit]
Description=Login Service
Documentation=man:systemd-logind.service(8) man:logind.conf(5)
Documentation=https://www.freedesktop.org/wiki/Software/systemd/logind
Documentation=https://www.freedesktop.org/wiki/Software/systemd/multiseat
Wants=user.slice
After=nss-user-lookup.target user.slice
# Ask for the dbus socket.
Wants=dbus.socket
After=dbus.socket

以下是详细分解:

  • Wants=user.slice:这完全有道理。由于 systemd 与 cgroups 的紧密集成,必须为每个登录的用户创建一个用户切片。

  • After=nss-user-lookup.target:这是 /etc/nsswitch.conf 文件,我们接下来会讨论。

  • Wants=dbus.socketAfter=dbus.socket:这个服务文件没有 [Install] 部分,因此当我们进入多用户或图形目标时,服务不会自动启动。相反,当第一个用户首次登录时,会生成一个 dbus 消息,从而自动启动该服务。

好的,让我们查看 /etc/nsswitch.conf 文件中的相关行。打开文件并查找这四行:

passwd:      sss files systemd
shadow:     files sss
group:       sss files systemd
. . .
. . .
gshadow:    files

passwd:shadow:group:行中,sss表示用户和组信息将从sssd中提取,这使得你可以使用/etc/passwd/etc/group/etc/shadow/etc/gshadow文件。如果系统在sssfiles中找不到登录用户的信息,它会转向systemdsystemd设置允许系统验证动态用户,这些用户可能在服务单元文件中配置,并且在/etc/passwd/etc/shadow文件中没有条目。

注意

动态用户,如前文所提到的,并不是用于普通的登录计算机的人的用户。它们是系统账户,用于以减少的权限运行服务。每当使用动态用户的服务启动时,动态用户就会动态创建,并在服务停止时被销毁。你永远不会在/etc/passwd/etc/group/etc/gshadow/etc/shadow文件中看到动态用户的条目。

现在,让我们回到systemd-logind.service文件,看看[Service]部分。我不能一次性展示给你所有内容,所以这里是顶部部分:

[Service]
ExecStart=/usr/lib/systemd/systemd-logind
Restart=always
RestartSec=0
BusName=org.freedesktop.login1
WatchdogSec=3min
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_MAC_ADMIN CAP_AUDIT_CONTROL CAP_CHOWN CAP_KILL CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_FOWNER CAP_SYS_TTY_CONFIG
. . .
. . .

这主要是我们之前讲过的标准内容,所以你应该已经掌握了。我要你注意的重点是CapabilityBoundingSet=行,它为该服务授予了许多 root 级别的能力。[Service]部分的第二部分包含了很多安全性和资源控制指令:

. . .
MemoryDenyWriteExecute=yes
RestrictRealtime=yes
RestrictNamespaces=yes
RestrictAddressFamilies=AF_UNIX AF_NETLINK
RestrictSUIDSGID=yes
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
SystemCallArchitectures=native
LockPersonality=yes
FileDescriptorStoreMax=512
# Increase the default a bit in order to allow many simultaneous logins since we keep one fd open per session.
LimitNOFILE=16384

和往常一样,我会留给你自己查阅systemd.directives手册页面中的这些指令。

好的,这就是 Alma 机器上的systemd-logind.service文件。让我们看看 Ubuntu 机器上的文件。

Ubuntu Server 的systemd-logind.service文件

Ubuntu 机器上的systemd-logind.service文件与 Alma 机器上的有很大不同。我们先来看一下[Unit]部分:

[Unit]
Description=Login Service
Documentation=man:systemd-logind.service(8) man:logind.conf(5)
Documentation=https://www.freedesktop.org/wiki/Software/systemd/logind
Documentation=https://www.freedesktop.org/wiki/Software/systemd/multiseat
Wants=user.slice modprobe@drm.service
After=nss-user-lookup.target user.slice modprobe@drm.service
ConditionPathExists=/lib/systemd/system/dbus.service
# Ask for the dbus socket.
Wants=dbus.socket
After=dbus.socket
. . .

我们看到的第一个区别是,Wants=行调用了modprobe@.service来加载drm内核模块。我不确定为什么这样,因为这看起来应该是在启动机器时加载的。实际上,这在 Alma 机器上似乎是这样,如我们所见:

[donnie@localhost ~]$ lsmod | grep drm
drm_kms_helper        233472  1 vmwgfx
syscopyarea            16384  1 drm_kms_helper
sysfillrect            16384  1 drm_kms_helper
sysimgblt              16384  1 drm_kms_helper
fb_sys_fops            16384  1 drm_kms_helper
drm                   569344  4 vmwgfx,drm_kms_helper,ttm
[donnie@localhost ~]$

出于我不明白的原因,Ubuntu 的开发人员决定在systemd-logind服务启动时加载drm模块,而不是在启动时加载。

Ubuntu 机器上的[Service]部分要大得多,因为它包含了比 Alma 机器上更多的安全指令。为什么?请记住,Alma 机器正在运行的systemd-logind.service文件为我们提供了一些 AppArmor 无法提供的良好的强制访问控制保护。以下是[Service]部分的一个片段,显示了 Ubuntu 的一些额外指令:

[Service]
BusName=org.freedesktop.login1
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_MAC_ADMIN CAP_AUDIT_CONTROL CAP_CHOWN CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_FOWNER CAP_SYS_TTY_CONFIG CAP_LINUX_IMMUTABLE
DeviceAllow=block-* r
DeviceAllow=char-/dev/console rw
DeviceAllow=char-drm rw
DeviceAllow=char-input rw
…
…
# Increase the default a bit in order to allow many simultaneous logins since
# we keep one fd open per session.
LimitNOFILE=524288

和往常一样,我让你在手册页面中阅读这些安全指令。

接下来,我们来看一下systemd-logind.service的配置文件。

理解 logind.conf

/etc/systemd/logind.conf 文件是 systemd-logind 服务的配置文件。在我们深入之前,我建议你使用文本模式虚拟机进行本节操作。演示将要求你对 logind.conf 文件做几个修改,并且每次修改后都需要重启 systemd-logind 服务。如果你在图形模式的机器上进行操作,你会被登出桌面,然后需要重新登录。桌面不一定能正确恢复,你可能会需要重启机器。而在文本模式机器上就没有这个问题。因此,由于 Ubuntu Server 机器已经处于文本模式,我们将用它进行演示。

好消息是,logind.conf 文件在 Ubuntu 机器和 Alma 机器上是完全相同的。它看起来是这样的:

图 18.1 – logind.conf 文件

/etc/systemd/ 下的所有配置文件一样,所有指令都被注释掉了。显示的值是编译进 systemd-logind 可执行文件中的默认值。你可能通过查看这些指令的名称就能猜出它们的作用,如果不太明白的部分,你可以查阅 logind.conf 的手册页。所以,与其详细介绍每个指令,我更想讲解几个较为有趣的指令。我们先从 虚拟终端 设置开始。

虚拟终端

在顶部,我们看到 #NAutoVTs=6 这一行。这设置了可用的 虚拟终端 数量。虚拟终端对桌面机器来说没什么用,因为你可以直接从 F4 终端通过 ssh 打开多个终端模拟器来连接到其他 GPU 挖矿设备。在图形模式的桌面机器上,一个虚拟终端通常保留给桌面。(它通常是 F1 终端,但不同的发行版可能会有所不同。)你可以在虚拟机上尝试这个,但是有一个小技巧。如果你的主机是运行 Windows,就像在裸机 Linux 上那样直接按 Ctrl-Alt-Function 键序列。但如果你的主机是运行 Linux,你需要打开 VirtualBox输入/键盘 菜单,并启动 软键盘

图 18.2 – 虚拟机的软键盘

然后,通过点击软键盘来执行你的 Ctrl-Alt-Function 键序列。如果你试图用普通键盘来做,主机机器会拦截这个键序列。

你可能永远不会编辑这一行,因为六个虚拟终端对于大多数人来说已经足够了。但如果你需要更多虚拟终端,你可以在这里添加。例如,假设你需要八个虚拟终端而不仅仅是六个,只需将 #NAutoVTs=6 改为 #NAutoVTs=8。然后,重启 systemd-logind 服务:

donnie@ubuntu20-04:/etc/systemd$ sudo systemctl restart systemd-logind
donnie@ubuntu20-04:/etc/systemd$

你可以通过按 Ctrl + Alt + F7Ctrl + Alt + F8 来查看额外的两个虚拟终端。

接下来,让我们看看用户如何在退出后保持进程继续运行。

保持用户进程在退出后继续运行

以下三行是一起工作的:

#KillUserProcesses=no
#KillOnlyUsers=
#KillExcludeUsers=root

如果你像我一样是老一辈人,你可能还记得在旧的 SysV 时代它是如何工作的。你会登录到 Linux 服务器,从命令行启动一个进程,然后在进程仍在运行时退出。问题是,一旦你退出,进程就会停止。如果你远程登录并启动了一个进程,那么如果不小心关闭了本地机器的远程终端,或者本地机器重新启动,进程也会停止。为了确保进程在远程机器上继续运行,防止出现上述情况,你需要使用screennohup启动进程。现在,只要这三行保持不变,你就不必担心这个问题。为了演示,如果你还没有为 Frank 创建账户,可以在文本模式的 Ubuntu 机器上为他创建一个账户:

donnie@ubuntu20-04:~$ sudo adduser frank

然后,让他远程登录。让他在自己的主目录中创建loop.sh脚本,像这样:

#!/bin/bash
i=0
for i in {0..100000}
do
        echo $i >> number.txt
        sleep 5
done
exit

这是一个小小的循环,什么也不做,只是每隔五秒钟在文本文件中创建一个条目。没关系,因为它达到了我们的目的。(另外,请注意,我并没有上传这个脚本到loop.sh文件中:)

frank@ubuntu20-04:~$ chmod u+x loop.sh
frank@ubuntu20-04:~$ ls -l
total 4
-rwxr--r-- 1 frank 83 Sep  9 16:29 loop.sh
frank@ubuntu20-04:~$

现在,让 Frank 将脚本作为后台进程启动:

frank@ubuntu20-04:~$ ./loop.sh &
[1] 2446
frank@ubuntu20-04:~$

通过执行tail -f number.txt来验证脚本是否正在运行:

frank@ubuntu20-04:~$ tail -f number.txt
10
11
12
13
14
15
16
17
18

执行Ctrl + C来停止tail -f进程。然后,让 Frank 通过输入exit退出。

接下来,让 Frank 重新登录,然后再次让他执行tail -f number.txt。你应该能看到数字列表不断递增,这意味着即使 Frank 退出,进程仍然在继续。要停止该进程,使用ps aux获取PID号,然后在kill命令中使用该 PID 号:

frank@ubuntu20-04:~$ ps aux | grep loop.sh
frank       2446  0.1  1.5  32012 31120 ?        S    16:35   0:00 /bin/bash ./loop.sh
frank       2598  0.0  0.0   3304   736 pts/2    S+   16:46   0:00 grep --color=auto loop.sh
frank@ubuntu20-04:~$ kill 2446
frank@ubuntu20-04:~$

然后,让 Frank 通过输入exit退出。

现在,假设我们不希望 Frank 在退出后继续保持其进程运行。在你自己的终端中,打开/etc/systemd/logind.conf文件。将#KillOnlyUsers=这一行修改成如下所示:

KillOnlyUsers=frank

保存文件并重启systemd-logind服务:

donnie@ubuntu20-04:/etc/systemd$ sudo systemctl restart systemd-logind
donnie@ubuntu20-04:/etc/systemd$

请注意,这个服务没有reload选项。

回到 Frank 的终端,让他重新登录。让他像之前一样在后台启动loop.sh脚本。当你这次执行tail -f number.txt命令时,你应该能看到数字列表不再递增。

好的,现在我们暂时不需要 Frank 了,让他退出吧。

注意

几天前,就是我开始写这一章的那天,猫 Frank 决定帮忙。他按了一些键并删除了整段文字,用一串破折号替换了它。(幸好有撤销功能。)

接下来,让我们看一下几个电源管理指令

电源管理指令

/etc/systemd/logind.conf 文件的更下方,您会看到 HandlePowerKey=, HandleSuspendKey=, HandleHibernateKey=, HandleLidSwitch=, HandleLidSwitchExternalPower=HandleLidSwitchDocked= 等电源管理指令。您大概可以从这些指令的名称推测它们的作用,您也可以在 logind.conf 文件中看到默认设置。要查看可以为这些指令使用的其他设置,只需查看 logind.conf 的手册页。那是一个很好的写法,我在这里就不重复了。不过,我会给出一个例子。

假设您有一台笔记本电脑,并且您希望它在合上盖子时继续运行。只需查找这行:

#HandleLidSwitch=suspend

将其更改为如下所示:

HandleLidSwitch=ignore

我假设您在图形模式下运行笔记本电脑。(难道不是每个人都这样吗?)由于在图形模式下重启 systemd-logind.service 效果不佳,最佳选择是直接重启机器,以便新设置生效。现在,当您合上笔记本电脑的盖子时,它将继续像打开盖子时一样运行。(如果您真的想试试,可以在虚拟机上做这个。但由于您的虚拟机没有盖子,您不会看到任何变化。)

在我们的最后一个例子中,来做一些 空闲操作

空闲操作指令

IdleAction,嗯?如果这不是个矛盾修饰法,那我就不知道什么才是了。但说正经的,您可以配置接下来的两个指令来控制当计算机在指定时间段内处于空闲状态时会发生什么:

#IdleAction=ignore
#IdleActionSec=30min

默认情况下,机器会一直运行,直到您关闭它。为了好玩,把这两行改成如下所示:

IdleAction=poweroff
IdleActionSec=3min

重启 systemd-logind.service,然后等待,不要触碰虚拟机。大约三分钟后,您应该会看到虚拟机自动关闭。当然,您不希望将虚拟机保持这种配置,因此重新启动虚拟机并将这些设置恢复为默认值。然后,再次重启 systemd-logind.service

仍然有一些指令我没有覆盖,但您可以在 logind.conf 的手册页中阅读到。接下来,我们将继续介绍 loginctl 管理工具。

理解 loginctl

另一个好消息是,loginctl 在 Ubuntu 和 Alma 上的表现完全相同。您可以使用它来监视其他用户的活动,修改某个用户的登录环境设置,甚至作为安全工具清除恶意用户。

注意

对于这一部分,我们将继续使用 Ubuntu Server 机器。如果尚未创建 Pogo、Vicky 和 Frank 的用户帐户,请创建。通过本地终端登录,并再次通过远程终端登录。按 Ctrl-Alt-F2 在虚拟机上进入第二个虚拟终端,让 Vicky 在那里登录。然后,让 Pogo 和 Frank 从他们各自的远程终端登录。

在进入正题之前,我们需要定义几个术语:

  • session:每当用户登录系统时,都会创建一个会话。每个会话都会被分配一个十进制数字作为其 ID。

  • seat0是你永远会看到的唯一座位。创建新的座位涉及配置udev规则,而这超出了本书的范围。

执行loginctl没有任何选项或loginctl list-sessions会显示谁已登录以及他们是从哪里登录的:

donnie@ubuntu20-04:~$ loginctl
SESSION  UID USER   SEAT  TTY  
     10         1001 frank                 pts/1
     14         1003 vicky    seat0    tty2 
     16         1004 pogo                 pts/2
      3          1000 donnie  seat0    tty1 
      6          1000 donnie              pts/0
5 sessions listed.
donnie@ubuntu20-04:~$

你会看到,只有 Vicky 和我有指定的座位,而 Frank 和 Pogo 则必须站着。(是的,我知道,这是个糟糕的笑话。)但说正经的,Vicky 和我被分配到seat0,因为我们都从本地终端登录。我从tty1登录,它是默认的虚拟终端。然后,我按下Ctrl-Alt-F2切换到第二个虚拟终端(tty2),并让 Vicky 在那边登录。可能并不常见两个人登录到同一台本地机器的两个不同虚拟终端,但这种情况也是可能发生的。我现在这么做是为了展示,可以将多个用户分配到同一个座位。你还会看到,我有两个会话在进行,因为我既从本地终端登录,也通过远程ssh会话登录到了pts/0终端。Frank 和 Pogo 仅通过远程登录,这就是他们没有座位的原因。另请注意,每个会话在第一列都有自己分配的 ID 号。

注意

我刚刚向你展示了list-sessions选项在 Ubuntu 上的工作方式。在 RHEL 8 类型的发行版中,例如 Alma,任何远程登录的用户,SEATTTY列都会为空。(我也不知道为什么。)不过,当你使用user-statussession-status选项时,你会看到用户的pts信息,我接下来会解释。

使用user-status选项查看用户的详细信息。如果你没有指定用户名,系统会显示你自己账户的信息。现在,让我们看看我们无畏的负鼠 Pogo 到底在搞什么小动作:

donnie@ubuntu20-04:~$ loginctl user-status pogo
pogo (1004)
           Since: Sat 2021-09-11 16:50:45 EDT; 24min ago
           State: active
        Sessions: *16
          Linger: no
            Unit: user-1004.slice
                  ├─session-16.scope
                  │ ├─2211 sshd: pogo [priv]
                  │ ├─2302 sshd: pogo@pts/2
                  │ └─2303 -bash
                  └─user@1004.service
. . .
Sep 11 16:50:45 ubuntu20-04 systemd[2226]: Startup finished in 125ms.
donnie@ubuntu20-04:~$

为了查看稍少一些的信息,我们来看一下 Pogo 的session-status。我们看到他处于会话号16,所以命令和输出看起来会是这样:

donnie@ubuntu20-04:~$ loginctl session-status 16
16 - pogo (1004)
           Since: Sat 2021-09-11 16:50:45 EDT; 39min ago
          Leader: 2211 (sshd)
             TTY: pts/2
          Remote: 192.168.0.51
         Service: sshd; type tty; class user
           State: active
            Unit: session-16.scope
                  ├─2211 sshd: pogo [priv]
                  ├─2302 sshd: pogo@pts/2
                  └─2303 -bash
Sep 11 16:50:45 ubuntu20-04 systemd[1]: Started Session 16 of user pogo.
donnie@ubuntu20-04:~$

我们已经看过如何获取有关用户和会话的信息。接下来,让我们看看如何获取关于座位的信息。list-seat命令会显示所有可用的座位:

donnie@ubuntu20-04:~$ loginctl list-seats
SEAT 
seat0
1 seats listed.
donnie@ubuntu20-04:~$

除非你配置了一个或多个udev规则,否则seat0是你永远看到的唯一座位。现在,使用seat-status选项查看此座位包含的硬件:

donnie@ubuntu20-04:~$ loginctl seat-status seat0
seat0
        Sessions: *14 3
         Devices:
                  ├─/sys/devices/LNXSYSTM:00/LNXPWRBN:00/input/input0
                  │ input:input0 "Power Button"
                  ├─/sys/devices/LNXSYSTM:00/LNXSLPBN:00/input/input1
                  │ input:input1 "Sleep Button"
                  ├─/sys/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/LNXVIDEO:00/input/input4
                  │ input:input4 "Video Bus"
                  ├─/sys/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0/block/sr0
                  │ block:sr0
. . .

获取有关用户、会话和座位的信息还有其他一些选项,但你已经大致了解了。况且,你可以从loginctl的手册页中获取更多信息。

接下来,假设出于某种原因,你想把 Frank 踢出他的会话。只需使用terminate-session选项,后面跟上 Frank 的会话 ID 号,像这样:

donnie@ubuntu20-04:~$ sudo loginctl terminate-session 10
[sudo] password for donnie: 
donnie@ubuntu20-04:~$

在这里,你可以看到 Frank 的会话确实已经被终止:

frank@ubuntu20-04:~$ Connection to 192.168.0.49 closed by remote host.
Connection to 192.168.0.49 closed.
donnie@siftworkstation: ~
$ 

如果一个用户登录了多个会话,并且你希望关闭所有这些会话,可以使用 terminate-user 选项,像这样:

donnie@ubuntu20-04:~$ sudo loginctl terminate-user pogo
donnie@ubuntu20-04:~$

还有一些其他的管理命令,可能对你有帮助。它们很容易理解,并且在 loginctl 的手册页中有详细的说明。

接下来,我们介绍一个在 某些 场合可以替代 sudo 的酷工具。

理解 polkit

systemd-logind 确实提供对 polkit 功能的访问。PolicyKit 是 Red Hat 的一项创新,出现在多年前,并且可以在各种类似 Unix 的操作系统上使用。2012 年,发布了一个新版本,并改名为 polkit。开发者更改名称是为了提醒大家,这是一套全新的代码库,与旧版本不兼容。

polkit 服务与 sudo 类似,它允许通常没有特权的用户执行某些特权任务。不过,二者之间有一个重要的区别。

sudo 工具非常容易配置,你可以轻松地将几乎任何管理员权限授予任何用户。当你安装操作系统时,你会为自己配置完整的 sudo 权限,其他用户则没有权限。另一方面,polkit 默认配置了一些可以授予 root 权限的管理任务。你可以添加更多任务,有时你可能会需要这样做。但请记住,编写 polkit 的规则和操作比编写 sudo 的规则要复杂。因此,在尝试自己编写之前,你需要研究系统中已经存在的示例并阅读文档。我们在查看这些规则和操作之前,先来看一下 polkit 如何授予 root 权限。

我们将从 Alma Linux 机器开始。为了查看一些 polkit 目录的内容,我们需要 root 权限,所以让我们切换到 root shell:

[donnie@localhost ~]$ sudo su -
[sudo] password for donnie: 
[root@localhost ~]#

现在,查看 /etc/polkit-1/rules.d/ 目录:

[root@localhost ~]# cd /etc/polkit-1/rules.d/
[root@localhost rules.d]# ls
49-polkit-pkla-compat.rules  50-default.rules
[root@localhost rules.d]#

我们需要的文件是 50-default.rules 文件,其内容如下所示:

[root@localhost rules.d]# cat 50-default.rules 
/* -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- */
// DO NOT EDIT THIS FILE, it will be overwritten on update
//
// Default rules for polkit
//
// See the polkit(8) man page for more information
// about configuring polkit.
polkit.addAdminRule(function(action, subject) {
    return ["unix-group:wheel"];
});
[root@localhost rules.d]#

请注意最后一行:

polkit.addAdminRule(function(action, subject) {
return ["unix-group:wheel"];

这意味着,如果 polkit 检测到某人试图执行管理员任务而没有使用 sudo,它会检查 wheel 组中是否有成员。如果 wheel 组中有成员,它将提示用户输入该成员的密码。如果 wheel 组没有成员,则表示 root 用户已设置密码。如果是这种情况,polkit 会提示输入 root 用户的密码。

在 Ubuntu 机器上,我们需要查看的文件位于 /etc/polkit-1/localauthority.conf.d/ 目录下,并且我们不需要 root 权限即可访问:

donnie@ubuntu20-04:~$ cd /etc/polkit-1/localauthority.conf.d/
donnie@ubuntu20-04:/etc/polkit-1/localauthority.conf.d$ ls -l
total 8
-rw-r--r-- 1 root 267 Aug 16  2019 50-localauthority.conf
-rw-r--r-- 1 root root  65 Aug 16  2019 51-ubuntu-admin.conf
donnie@ubuntu20-04:/etc/polkit-1/localauthority.conf.d$

50-localauthority.conf 文件如下所示:

donnie@ubuntu20-04:/etc/polkit-1/localauthority.conf.d$ cat 50-localauthority.conf 
# Configuration file for the PolicyKit Local Authority.
#
# DO NOT EDIT THIS FILE, it will be overwritten on update.
#
# See the pklocalauthority(8) man page for more information
# about configuring the Local Authority.
#
[Configuration]
AdminIdentities=unix-user:0
donnie@ubuntu20-04:/etc/polkit-1/localauthority.conf.d$

这里只需要一行重要的内容,它会查找 root 用户(即 UID 为 0unix-user)。另一个文件则查找 sudoadmin 组的成员:

donnie@ubuntu20-04:/etc/polkit-1/localauthority.conf.d$ cat 51-ubuntu-admin.conf 
[Configuration]
AdminIdentities=unix-group:sudo;unix-group:admin
donnie@ubuntu20-04:/etc/polkit-1/localauthority.conf.d$

目前,Red Hat 和 Ubuntu 系统之间最大的区别是,在 Red Hat 类型的系统中,wheel 组的成员具有完全的 sudo 权限。而在 Ubuntu 系统中,sudo 组或 admin 组的成员拥有完全的 sudo 权限。现在,让我们看看它是如何工作的。

在 Ubuntu 机器上,尝试不使用 sudo 重新加载ssh服务:

donnie@ubuntu20-04:~$ systemctl reload ssh
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to reload 'ssh.service'.
Authenticating as: Donald A. Tevault (donnie)
Password: 
==== AUTHENTICATION COMPLETE ===
donnie@ubuntu20-04:~$

如你所见,polkit 要求输入我的密码,因为我是唯一一个属于 sudo 组的成员。现在,让我们尝试使用 polkit 查看防火墙配置:

donnie@ubuntu20-04:~$ iptables -L
Fatal: can't open lock file /run/xtables.lock: Permission denied
donnie@ubuntu20-04:~$

它失败了,因为 polkit 没有配置为与iptables命令一起使用。

接下来,让我们看看如果 Pogo 尝试使用 polkit 会发生什么。然而,为了使其正常工作,他的密码需要与您的密码不同。如果是相同的,请将其更改为其他密码:

donnie@ubuntu20-04:~$ sudo passwd pogo
New password: Retype new password: 
passwd: password updated successfully
donnie@ubuntu20-04:~$

现在,让我们让 Pogo 尝试重新加载ssh

pogo@ubuntu20-04:~$ systemctl reload ssh
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to reload 'ssh.service'.
Authenticating as: Donald A. Tevault (donnie)
Password: 
polkit-agent-helper-1: pam_authenticate failed: Authentication failure
==== AUTHENTICATION FAILED ===
Failed to reload ssh.service: Access denied
See system logs and 'systemctl status ssh.service' for details.
pogo@ubuntu20-04:~$

如前所述,polkit 要求输入我的密码,因为我是 sudo 组的成员,而 Pogo 不是。Pogo 不知道我的密码,所以他无法执行此命令。

在我们仍然使用 Ubuntu 机器时,让我们看看这些规则中的一些内容。我们将cd进入/usr/share/polkit-1/rules.d/目录,并查看systemd-networkd.rules文件:

// Allow systemd-networkd to set timezone, get product UUID,
// and transient hostname
polkit.addRule(function(action, subject) {
    if ((action.id == "org.freedesktop.hostname1.set-hostname" ||
         action.id == "org.freedesktop.hostname1.get-product-uuid" ||
         action.id == "org.freedesktop.timedate1.set-timezone") &&
        subject.user == "systemd-network") {
        return polkit.Result.YES;
    }
});

在这里,我们将 root 权限分配给systemd-networkd系统用户帐户,以便它可以在不提示密码的情况下执行这三项任务。(return polkit.Result.YES;这一行是防止它要求输入密码的原因。)

对于更复杂的情况,让我们cd进入/usr/share/polkit-1/actions/目录,并查看其中一个文件。我们将选择com.ubuntu.languageselector.policy文件,因为它是最短的。我们只需要查看action id=部分,它看起来是这样的:

. . .
 <action id="com.ubuntu.languageselector.setsystemdefaultlanguage">
    <description gettext-domain="language-selector">Set system default language</description>
    <message gettext-domain="language-selector">System policy prevented setting default language</message>
    <defaults>
      <allow_any>auth_admin</allow_any>
      <allow_inactive>no</allow_inactive>
      <allow_active>auth_admin_keep</allow_active>
    </defaults>
  </action>
. . .

底部的<default>段落是我们定义谁可以执行此操作的地方。下面是详细说明:

  • <allow_any>:此标签设置对任何客户端机器的授权。auth_admin设置要求用户输入管理员密码,然后才能执行该操作。

  • <allow_inactive>:此标签设置在本地控制台上处于非活动会话中的客户端的授权。此处设置为no,这会阻止这些客户端获得任何授权。

  • <allow_active>:这是为处于本地控制台活动会话中的客户端设置的。auth_admin_keep值要求用户输入管理员密码。它还允许用户在短时间内保持授权。

其他操作文件以类似的方式设置,我将留给你自己查看它们。有关规则和操作的更多详细信息,请参阅polkit的手册页。

每当有人尝试执行在polkit中配置的管理操作时,polkit服务都会通过dbus消息被激活,正如我们在其单元文件中的Type=dbus行所看到的那样:

donnie@ubuntu20-04:~$ cd /lib/systemd/system
donnie@ubuntu20-04:/lib/systemd/system$ cat polkit.service 
[Unit]
Description=Authorization Manager
Documentation=man:polkit(8)
[Service]
Type=dbus
BusName=org.freedesktop.PolicyKit1
ExecStart=/usr/lib/policykit-1/polkitd --no-debug
donnie@ubuntu20-04:/lib/systemd/system$

好的,Ubuntu 机器的部分就到这里。Alma 机器基本相同,不过你需要 root 权限才能cd进入rules.d/目录,如下所示:

[donnie@localhost system]$ cd /usr/share/polkit-1/
[donnie@localhost polkit-1]$ ls -l
total 8
drwxr-xr-x. 2 root    4096 Jul 23 15:51 actions
drwx------. 2 polkitd root  287 Jul 12 17:51 rules.d
[donnie@localhost polkit-1]$

现在,让我们转到 Alma 虚拟机的本地图形终端。如果你还在 root shell 中,输入exit以返回到自己的 shell。现在,尝试重新加载sshd,你会看到一个对话框弹出来,要求输入管理员密码:

图 18.3 – 图形 polkit 密码对话框

好的,我觉得关于 polkit 的内容差不多了。让我们总结一下学到的内容,完成这部分。

总结

和往常一样,本章我们学习了一些很酷的东西。我们从systemd-logind.service文件的讨论开始,了解了它在 Ubuntu 和 Alma 机器上的不同配置。接着我们研究了logind.conf文件,并尝试了一些配置选项。之后,我们玩了下loginctl,最后讨论了 polkit。

各位,今天的内容不仅仅是本章的总结,也是整本书的总结。希望你们和我一样,喜欢这趟穿越理想化的systemd 世界的旅程。保重,期待很快再见。

问题

  1. systemd-logind服务是如何激活的?

    A. 作为多用户目标的一部分

    B. 作为图形目标的一部分

    C. 当它收到dbus消息时

    D. 作为sysinit目标的一部分

  2. 当两个不同的用户远程登录到 Linux 服务器时,会发生什么?

    A. 它们都分配给seat0

    B. 一个分配给seat0,另一个分配给seat1

    C. 它们都分配给seat1

    D. 它们都没有分配座位。

  3. 以下哪一文件中systemd-logind会查看如何进行用户认证?

    A. /etc/nsswitch.conf

    B. /etc/default/nsswitch.conf

    C. /etc/sysconfig/nsswitch.conf

    D. /etc/authenticate.conf

  4. 关于 polkit,以下哪个说法是正确的?

    A. 在默认配置下,它只与预定义的一组管理命令一起工作。

    B. 在默认配置下,它与所有管理命令一起工作,就像 sudo 一样。

    C. 仅适用于 root 用户密码。

    D. 只能在文本模式机器上使用。

答案

  1. C

  2. D

  3. A

  4. A

进一步阅读

Packt.com

订阅我们的在线数字图书馆,完全访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助您规划个人发展并推动职业进步。更多信息,请访问我们的网站。

第十九章:为什么订阅?

  • 通过来自超过 4,000 名行业专业人士的实用电子书和视频,花费更少的时间学习,更多时间编码

  • 使用专为您制定的技能计划来提升学习效果

  • 每月获取一本免费的电子书或视频

  • 全文可搜索,便于查找重要信息

  • 复制粘贴,打印和书签内容

您知道 Packt 提供每本书的电子书版本吗?PDF 和 ePub 文件均可供下载。您可以升级到电子书版本,享受打印书客户的折扣。请通过packt.com与我们联系,了解更多详情。

www.packt.com,您还可以阅读一系列免费的技术文章,注册多个免费的新闻订阅,并获得 Packt 图书和电子书的独家折扣和优惠。

您可能喜欢的其他书籍

如果您喜欢本书,您可能对 Packt 的其他书籍感兴趣:

精通 Adobe Photoshop Elements

](https://github.com/OpenDocCN/freelearn-linux-pt3-zh/raw/master/docs/linux-svc-mgt-mdez-sysd/img/9781800569829_Cover.png)](https://www.packtpub.com/product/red-hat-enterprise-linux-8-administration/9781800569829)

Red Hat Enterprise Linux 8 管理

Miguel Pérez Colino,Pablo Iranzo Gómez,Scott McCarty

ISBN: 978-1-80056-982-9

  • 在裸机、虚拟化和云等不同环境中部署 RHEL 8

  • 在本地和远程系统上规模化管理用户和软件

  • 发现如何通过 SELinux、OpenSCAP 和 firewalld 保护系统

  • 获得对 LVM、Stratis 和 VDO 存储组件的概述

  • 使用无密码 SSH 和隧道进行主机远程管理

  • 监控系统的资源使用情况,并采取措施解决问题

  • 理解启动过程、性能优化和容器

精通 Adobe Captivate 2019 - 第五版

Linux 内核编程

Kaiwan N Billimoria

ISBN: 978-1-78995-343-5

  • 为 5.x 内核编写高质量的模块化内核代码(LKM 框架)

  • 配置并从源代码构建内核

  • 探索 Linux 内核架构

  • 掌握有关内核内存管理的关键内部知识

  • 理解和使用各种动态内核内存分配/释放 API

  • 发现有关内核 CPU 调度的关键内部方面

  • 理解内核并发问题

  • 查找如何使用关键内核同步原语

Packt 正在寻找像您这样的作者

如果你有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并立即申请。我们已经与成千上万的开发者和技术专家合作,帮助他们将自己的见解分享给全球的技术社区。你可以提交一般申请,申请我们正在招聘作者的特定热门话题,或者提交你自己的想法。

分享你的想法

现在你已经完成了《通过 systemd 轻松管理 Linux 服务》,我们很想听听你的想法!如果你是从 Amazon 购买的这本书,请点击这里直接访问该书的 Amazon 评论页面,分享你的反馈或在购买页面留下评论。

你的评价对我们和技术社区非常重要,将帮助我们确保提供优质的内容。

posted @ 2025-07-04 15:40  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报