• 深入理解Systemd的运行原理
  • 发布于 2个月前
  • 245 热度
    0 评论
在我决定写这文章之前,我已知道 Systemd 是 Linux 中的初始化(init)进程,并且是所有其他进程运行的进程。我执行过一些 Systemd 命令,但坦白说,我并未真正深入了解过它。我从未思考过 Systemd 如何知道运行哪些进程或者它还能做些什么。我只知道当一个进程启动,Systemd 会以某种方式接管它。

有许多出色的资源详细介绍了 Systemd 的运行原理,但真正帮助我开始理解是自己设置它来管理一个服务。在这个教程中,我们将设定一个简单的 Golang 程序,并让 Systemd 在后台运行它。我们将确保它在被终止后会重启,并且我们还会确保 Systemd 在系统启动时启动该进程。这样,我们可以深入地了解 Systemd 的运行原理和它的其他功能。

一.什么是初始化进程?
在 Linux 进程世界中,init 进程是至高无上的。它是在内核加载后第一个启动的进程,所有其他进程都在其之下运行。init 进程首先通过初始化必要的进程将机器转变为一个可以正常运行的状态。然后,它负责在系统运行期间启动和停止进程,以及在系统关闭时安全地终止进程。

在旧版本的 Linux 中,init 进程是一个或一系列的 shell 脚本。但是,大多数现代 Linux 发行版已将 shell 脚本替换为一个名为 systemd 1 的二进制程序。Systemd 是一个较新的 init 系统,设计用来提高性能和简化服务管理。它使用称为单元的配置文件来定义和管理服务,并提供了如并行服务启动、套接字激活和按需服务启动等高级功能。它还管理日志。对于一个程序来说,要做的事情太多了,这就是为什么 Systemd 不仅仅是一个程序,它实际上是一套协同工作的工具集。

我应该指出,并非所有的 Linux 发行版都使用 Systemd,但许多已经切换过来。你应该检查你特定的 Linux 版本使用哪种 init 进程。
以下是一些使用 Systemd 的流行 Linux 发行版:
Ubuntu 15.04 及后续版本
Debian 8(Jessie)及后续版本
CentOS 7 及后续版本
RHEL 7 及后续版本
Fedora
Arch Linux

二.快速了解父进程和子进程
在我们开始设定我们的 Golang 应用被 Systemd 管理之前,让我们快速浏览一下 Linux 进程。我有一个运行 Ubuntu 22.04.2 LTS 的 Amazon EC2 实例。我们可以使用 htop 来获取所有正在运行的进程列表。一旦进入 htop,按下 t 键就可以得到一个树形视图,这样我们就可以看到哪些进程正在其他进程之下运行。

请看最顶端的进程,PID 为 1 的 /sbin/init。它的 PID 为 1,因为它是在我们启动这个 Linux 实例时启动的第一个进程。你也会注意到,在我们的树视图中,所有其他的进程都存在于 /sbin/init 的分支下。

但是等等,我认为 Systemd 是 init 进程呢?让我们到 /sbin 目录下看看。
ubuntu@ip-xxxx:/sbin$ ls -lah | grep init
lrwxrwxrwx  1 root root     20 Mar 20 14:32 init -> /lib/systemd/systemd
如果我们更近一步研究 init,我们可以看到它其实只是一个指向 /lib/systemd/systemd 的符号链接。所以实际上,所有的进程都是由 Systemd 运行的。

我们的进程
为了详细介绍如何设定一个进程由 Systemd 管理,我们将使用一个简单的 Go 程序作为例子,该程序每秒打印一次 “Hello World”。
package main
import (
    "fmt"
    "time"
)

func main() {
  for {
      fmt.Println("Hello World")
        time.Sleep(1 * time.Second)
  }

}
代码和可执行文件位于我的主目录中。
ubuntu@ip-xxxx:~/hello-world$ ls
go.mod  hello-world  main.go
让我们运行一下吧。
ubuntu@ip-xxxx:~/hello-world$ ./hello-world
Hello World
Hello World
Hello World
...
当它运行时,我们可以在单独的选项卡中打开 htop。

这张图片被放大了,因此我们看不到最顶端的 init 进程,但是请相信,它仍然在那里,观察着一切。我们可以沿着树状结构向下追踪到 SSH(这是我连接到这个实例的方式)。SSH 下有它自己运行的子进程。在这些子进程中,有我们的 shell,即 bash,这是我们运行 hello-world 的环境。最后,在 bash 下面,我们有我们的 hello-world 进程。(在运行一个简单的 Go 程序时出现三个子进程,可能是因为 Go 运行时和操作系统处理进程创建和管理的方式。)

三.什么是 Systemd 单元?
当我们谈论 Linux 中长期运行的进程时,我们通常将它们称为服务或守护进程。Systemd 称它们为单元。Systemd 单元可以是服务,比如我们的 hello world 程序,但它也可以是其他 Systemd 可以管理的东西,如套接字、挂载点或目标(稍后会更详细地讨论)。单元在单元文件中定义。Systemd 单元文件是一种配置文件,定义了各种系统组件。每个单元文件包含了关于单元行为、依赖关系、启动条件以及 Systemd 管理和监控相应系统资源所需的其他设置的信息。

这些单元文件存在于几个不同的地方。了解其中的一些是非常有帮助的。
自定义系统单元文件 ——/etc/systemd/system/:管理员可以在这里创建自定义的系统单元文件来定义新的服务或修改现有的服务。这个目录中的文件优先于系统目录中的文件,使你可以覆盖或扩展默认设置。我们将在这里为我们的 hello-world 服务创建自己的单元文件。

标准单元文件 ——/lib/systemd/system/:用于系统范围内的单元的单元文件。这些通常与预安装的应用程序有关,由发行版的维护者管理。
已安装包的单元文件 ——/usr/lib/systemd/system:一般来说,你的包管理器会把已安装包的单元文件放在这里。这是安装过程的一部分,会自动进行。例如,Nginx 在安装后会在这里放置几个单元文件。

如何创建你自己的 Systemd 单元
现在我们可以创建自己的 Systemd 单元文件来管理我们的 hello-world 进程。创建一个名为 /etc/systemd/system/hello-world.service 的文件。
[Unit]
Description=Hello World Service

[Service]
ExecStart=/home/ubuntu/hello-world/hello-world

[Install]
WantedBy=multi-user.target
这里最引人注目的是 ExecStart,它告诉 Systemd 如何启动我们的服务。WantedBy 使我们可以设置程序运行所需的依赖关系。我们可以用它来声明另一个服务,但声明一个目标也很常见。

目标是单元的组,经常被用来指定你可能希望你的系统处于的某个状态,类似于 Unix 中的运行级别。在这种情况下,设置 multi-user.target 确保系统处于可以进行多用户交互的状态。在这里声明它就是告诉 Systemd,当它达到多用户目标状态时,应启动我们的 hello-world 服务。


如果你需要知道某个目标包含哪些其他单元,检查起来很容易。目标是 Systemd 可以管理的另一种形式的单元,并且它们的配置方式与我们的服务相同;都在单元文件中配置。我们可以在 /lib/systemd/system/multi-user.target 找到多用户目标的单元文件,但这并不能显示所有的依赖关系,因为它们并不全都在一个文件中声明。要查看多用户目标(或任何单元)的所有依赖关系,我们可以运行:
ubuntu@ip-xxxx:~$ sudo systemctl list-dependencies multi-user.target
multi-user.target
● ├─apport.service
● ├─chrony.service
● ├─console-setup.service
● ├─cron.service
● ├─dbus.service
○ ├─dmesg.service
○ ├─e2scrub_reap.service
○ ├─ec2-instance-connect.service
○ ├─grub-common.service
○ ├─grub-initrd-fallback.service
○ ├─hibinit-agent.service
○ ├─irqbalance.service
○ ├─lxd-agent.service
● ├─networkd-dispatcher.service
○ ├─open-vm-tools.service
● ├─plymouth-quit-wait.service
● ├─plymouth-quit.service
....
为了节省空间,这里的输出被截断了,但你应该明白这是什么意思。注意,我们还没看到我们的 hello-world 程序列出来。我们马上就会看到的。
仅仅创建单元文件并不能让 Systemd 开始管理我们的程序。我们需要进行两个步骤。首先,我们需要让 Systemd 读取我们新创建的。service 单元文件。我们可以使用以下命令来实现:
systemctl daemon-reload
这会导致 Systemd 重新读取所有单元文件。现在 Systemd 了解了我们的单元,我们可以检查服务的状态。
# 堆代码 duidaima.com
ubuntu@ip-xxxx:/etc/systemd/system$ sudo systemctl status hello-world
○ hello-world.service - Hello World Service
     Loaded: loaded (/etc/systemd/system/hello-world.service; enabled; vendor preset: enabled)
     Active: inactive (dead)
目前,我们的服务处于非活动状态。让我们启动它,然后再次检查状态。
ubuntu@ip-xxxx:/etc/systemd/system$ sudo systemctl status hello-world
● hello-world.service - Hello World Service
     Loaded: loaded (/etc/systemd/system/hello-world.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2023-07-20 20:51:37 UTC; 14s ago
   Main PID: 179038 (hello-world)
      Tasks: 3 (limit: 1141)
     Memory: 564.0K
        CPU: 3ms
     CGroup: /system.slice/hello-world.service
             └─179038 /home/ubuntu/hello-world/hello-world


Jul 20 20:51:42 ip-xxxx hello-world[179038]: Hello World
Jul 20 20:51:43 ip-xxxx hello-world[179038]: Hello World
Jul 20 20:51:44 ip-xxxx hello-world[179038]: Hello World
Jul 20 20:51:45 ip-xxxx hello-world[179038]: Hello World
Jul 20 20:51:46 ip-xxxx hello-world[179038]: Hello World
Jul 20 20:51:47 ip-xxxx hello-world[179038]: Hello World
Jul 20 20:51:48 ip-xxxx hello-world[179038]: Hello World
Jul 20 20:51:49 ip-xxxx hello-world[179038]: Hello World
Jul 20 20:51:50 ip-xxxx hello-world[179038]: Hello World
Jul 20 20:51:51 ip-xxxx hello-world[179038]: Hello World
注意状态是如何从非激活(死亡)转变为激活(运行)。同时,也要注意,我们在状态信息下方获取到了日志的最后十行,这非常酷。我们再次查看 htop,并查看我们的 hello-world 进程在树视图中的显示位置。

我们可以看到,与我们之前从 bash 中运行的进程不同,它现在显示为 /sbin/init(即 Systemd)的直接子进程。

如果我们再次检查 multi-user target 的依赖性,我们也可以看到:
ubuntu@ip-xxxx:~$ sudo systemctl list-dependencies multi-user.target
multi-user.target
● ├─apport.service
● ├─chrony.service
● ├─console-setup.service
● ├─cron.service
● ├─dbus.service
○ ├─dmesg.service
○ ├─e2scrub_reap.service
○ ├─ec2-instance-connect.service
○ ├─grub-common.service
○ ├─grub-initrd-fallback.service
● ├─hello-world.service
○ ├─hibinit-agent.service
○ ├─irqbalance.service
嘿,看看这,列表中的第 11 个就是我们的 hello-world.service!太好了。现在我们的服务已经在后台运行了。如果我们把它关掉会发生什么呢?
ubuntu@ip-xxxx:~$ sudo kill 179038
正如我们所期望的,我们的单位变得不活跃。我们还可以在日志中看到它被停用的位置。
○ hello-world.service - Hello World Service
     Loaded: loaded (/etc/systemd/system/hello-world.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Mon 2023-07-24 19:18:25 UTC; 1min 41s ago
    Process: 179038 ExecStart=/home/ubuntu/hello-world/hello-world (code=killed, signal=TERM)
    Main PID: 179038 (code=killed, signal=TERM)
        CPU: 39.210s

Jul 24 19:18:18 ip-xxxx hello-world[179038]: Hello World
Jul 24 19:18:19 ip-xxxx hello-world[179038]: Hello World
Jul 24 19:18:20 ip-xxxx hello-world[179038]: Hello World
Jul 24 19:18:21 ip-xxxx hello-world[179038]: Hello World
Jul 24 19:18:22 ip-xxxx hello-world[179038]: Hello World
Jul 24 19:18:23 ip-xxxx hello-world[179038]: Hello World
Jul 24 19:18:24 ip-xxxx hello-world[179038]: Hello World
Jul 24 19:18:25 ip-xxxx hello-world[179038]: Hello World
Jul 24 19:18:25 ip-xxxx systemd[1]: hello-world.service: Deactivated successfully.
Jul 24 19:18:25 ip-xxxx systemd[1]: hello-world.service: Consumed 39.210s CPU time.
但如果我们希望服务一直在运行呢?在这种情况下,我们可以告诉 Systemd,当服务停止时,应该重新启动它。我们需要做的就是更新我们的单元文件。

我们有两种方式来告诉 Systemd 何时重新启动我们的服务。设置 Restart=always 将确保无论何时,只要服务停止,Systemd 就会重新启动它,即使我们手动停止了它。而设置 Restart=on-failure 则会告诉 Systemd,只有在服务因错误(返回非零状态)退出时,才需要重新启动服务;基本上就是当它崩溃时。
[Unit]
Description=Hello World Service

[Service]
ExecStart=/home/ubuntu/hello-world/hello-world
Restart=always

[Install]
WantedBy=multi-user.target
现在让我们重新启动我们的服务。
_ubuntu@ip-xxxx:~$ sudo systemctl start hello-world
Warning: The unit file, source configuration file or drop-ins of hello-world.service changed on disk. Run 'systemctl daemon-reload' to reload units.
似乎 Systemd 检测到我们的文件已更改。这很酷。我们只需按照错误中的说明让 Systemd 重新读取单元文件即可。

我不完全确定这在所有情况下都是必要的,但我也将重新启动该服务。
_ubuntu@ip-xxxx:~$ sudo systemctl restart hello-world
现在,如果你再次检查状态,它应该处于活动状态。那么我们就杀掉它吧。
ubuntu@ip-xxxx:~$ sudo kill 190646
然后再次检查状态。虽然服务仍然处于活动状态,但这是因为 Systemd 在我们检查状态和发现它已停用之前,就已经将其快速重新启动了。但无需担忧,因为我们可以直接从日志中看到单元何时被停止以及何时重新启动。
ubuntu@ip-xxxx:~$ sudo systemctl status hello-world
● hello-world.service - Hello World Service
     Loaded: loaded (/etc/systemd/system/hello-world.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2023-07-24 19:43:23 UTC; 2s ago
   Main PID: 190658 (hello-world)
      Tasks: 3 (limit: 1141)
     Memory: 572.0K
        CPU: 1ms
     CGroup: /system.slice/hello-world.service
             └─190658 /home/ubuntu/hello-world/hello-world

Jul 24 19:43:23 ip-xxxx systemd[1]: hello-world.service: Scheduled restart job, restart counter is at 1.
Jul 24 19:43:23 ip-xxxx systemd[1]: Stopped Hello World Service.
Jul 24 19:43:23 ip-xxxx hello-world[190658]: Hello World
Jul 24 19:43:23 ip-xxxx systemd[1]: Started Hello World Service.
Jul 24 19:43:24 ip-xxxx hello-world[190658]: Hello World
Jul 24 19:43:25 ip-xxxx hello-world[190658]: Hello World
这只是我们开始利用 Systemd 在单元文件中配置的一小部分。我们还可以为我们的服务设定资源限制,配置套接字,设定文件权限等等。

四.关于 journalctl
尽管我们可以另写一篇完整的博文来介绍 journalctl,但我还是想在这里简要地提一下,因为它可以是使用 Systemd 的一大优点。当 Systemd 管理服务单元时,它会自动捕获其标准输出和标准错误流,并将它们存储在日志中。这意味着任何由服务产生的日志消息或输出都会被记录并可以通过 journalctl 命令获取。

我们可以使用 journalctl -u hello-world 来检查 hello-world 的日志。这会给出所有的日志,但这可能会让你觉得浏览起来有些吃力。更常见的是,你可能在调试时需要从特定的时间范围获取日志。为此,你可以使用 --since 和 --until 选项。
journalctl -u hello-world --since "2023-07-25 12:00:00" --until "2023-07-25 18:00:00"

结论
确定这篇文章要涵盖的内容真的很难。Systemd 比我预期的更大也更复杂。我试图提供一种实践的方法来介绍一些 Systemd 的内部运作,并且希望重现那些真正帮助我开始理解 Systemd 的课程和知识。

如果你希望更深入地理解,我建议你接下来研究以下主题:
1.查看更多关于 Systemd 如何使用。wants 目录处理依赖的信息。
2.更深入地阅读Target将帮助你理解单元分组的概念以及它们如何用来达到特定的系统状态或目标。Target是在启动期间或在切换不同系统模式时管理系统行为的强大工具。
3.花些时间看看 SSH 或 Nginx 等服务的现有单元文件。研究这些文件会让你了解经验丰富的管理员如何使用 Systemd 来配置和管理这些关键服务。


如果你杀死 Systemd 会发生什么?
最后,我知道你在想如果你执行 sudo kill -9 1 会发生什么,我可以告诉你,在 Ubuntu 的 EC2 机器上,这并不会触发内核恐慌或在亚马逊引发爆炸,这有点令人失望。

要理解为什么,我们需要回顾一下 kill 命令的几个要点。默认情况下,kill 命令会向进程发送一个 SIGTERM 信号。SIGTERM 信号可以被进程捕获,并用于优雅地退出程序。

但我们也可以通过添加 - 9,即 sudo kill -9 <PID>,向进程发送 SIGKILL 信号。SIGKILL 信号无法被进程捕获,这意味着在你的程序中,你无法忽略或优雅地处理 SIGKILL。你的进程被终止了,这对你来说是个坏消息。但这对所有进程都是如此,除了 init。init 是 Linux 内核允许捕获并忽略 SIGKILL 的唯一进程。

因此,实际上,sudo kill -9 1 什么也不做。SIGKILL 就这样消失了。你甚至在日志中都看不到任何东西,除了你执行了这个命令。这是个好消息,因为杀死 Systemd 肯定会引发内核恐慌,这对于管理员来说是非常糟糕的事情。但对于我们这些有破坏欲望的人来说,这是非常悲伤的事情。

那么如果我们不能发送 SIGKILL,那么可以发送 SIGTERM,SIGKILL 的可捕获的兄弟信号吗?其实,我们可以做到这一点。结果可能并不会引发爆炸,但它们会更有趣一些。

当我执行 sudo kill 1 时,起初好像什么也没发生。系统还在运行,我甚至还保持着 SSH 连接。我唯一注意到的变化是 PID 1 从 /sbin/init 变为了 /lib/systemd/systemd --system --deserialize 12。

所以我推测存在一段代码,一旦 Systemd 收到 SIGTERM 信号,它会立即重启 Systemd。当它重启时,它会直接调用 Systemd,而不是通过符号链接来访问它。(或者类似的操作。)

我进行了一些深入的研究,我在一些论坛上找到了这样的信息:
--deserialize 用于恢复一个先前的 Systemd 实例通过 exec() 函数调用时,已经写入到文件中的内部状态。它的选项参数是该进程的一个开放的文件描述符。

所以无论何种防止故障的机制,似乎都会重新启动 Systemd 并恢复到之前保存的状态,这就是为什么我并没有真正注意到任何的干扰。这个功能非常棒。我想我需要找其他的东西来打破。
用户评论