理解 Systemd 目标:核心概念详解

理解 systemd 目标、默认启动目标、运行级别映射、隔离、自定义目标及故障排查命令。

理解 Systemd 目标:核心概念详解

如果你不再把 systemd 目标看作服务,就更容易理解它们。服务启动一个进程。目标将单元分组到一个命名的系统状态中。当机器启动到 multi-user.target 时,systemd 并不是在启动一个名为“multi-user”的程序。它是在尝试达到一个状态,即该目标所想要的单元已经启动,或者至少已经尝试过启动它们。

这种区别在你调试启动问题时很有帮助。如果 graphical.target 启动缓慢,目标本身很少是问题所在。被拉入该目标的显示、登录、网络、挂载或应用程序单元中的一个或多个运行缓慢或失败。目标为你提供了地图。

什么是 Systemd 目标?

在 systemd 生态系统中,目标是一种特殊类型的单元文件(如 .service.socket 文件),它起着关键的组织作用。与服务单元定义如何启动或停止特定进程不同,目标单元定义了一个系统状态或一个应同时激活的单元集合。它们充当其他 systemd 单元的逻辑分组点和同步点。

将目标视为系统运行过程中的里程碑。当 systemd 启动时,它不会随意启动一系列服务;而是朝着实现特定目标而努力。这个目标反过来会拉入达到该状态所需的所有必要服务、套接字、挂载点和其他目标。这种依赖驱动的方法确保了可预测且高效的启动过程。

对于熟悉 SysVinit 等旧版 Linux 初始化系统的人来说,systemd 目标是运行级别的现代等价物。虽然 SysVinit 有一组固定的运行级别(例如,运行级别 3 用于多用户文本模式,运行级别 5 用于多用户图形模式),但 systemd 目标更加灵活。它们是有名称的,而不是编号的,并且你可以定义自定义目标,从而提供更大的粒度和可扩展性。

目标如何工作:分组和依赖关系

目标通过其单元文件中定义的显式依赖关系来实现分组和状态定义功能。用于此的主要指令是 Wants=Requires=After=Before=

  • Wants=:指定“弱”依赖关系。如果 目标 A Wants= 单元 B,则当 目标 A 被激活时,systemd 将尝试启动 单元 B。但是,即使 单元 B 启动失败,目标 A 仍会启动。这通常用于分组相关但并非严格必需的服务。
  • Requires=:指定“强”依赖关系。如果 目标 A Requires= 单元 B,则必须成功启动 单元 B目标 A 才能激活。如果 单元 B 失败,目标 A 也会失败或无法启动。这用于关键依赖关系。
  • After=:定义顺序依赖关系。如果 目标 AAfter= 单元 B,则 目标 A 仅在 单元 B 启动之后才会启动。这并不暗示对成功启动的依赖,仅表示顺序。
  • Before=After= 的反向。如果 目标 ABefore= 单元 B,则 单元 B 仅在 目标 A 启动之后才会启动。
  • Conflicts=:确保某些单元不会同时激活。如果 目标 A Conflicts= 单元 B,则激活 目标 A 将停止正在运行的 单元 B,反之亦然。

这些指令使目标能够充当强大的编排器,根据需要拉入服务和其他目标,并定义它们应启动的顺序。例如,multi-user.target 通常 Wants= network.target 和各种其他服务,确保它们在系统达到多用户状态时处于活动状态。

你可以检查目标单元文件的内容以查看其依赖关系:

systemctl cat multi-user.target

此命令将输出 multi-user.target 单元文件的内容,显示其 DescriptionDocumentation,以及关键的 Wants=Requires=After= 和其他定义多用户状态构成的指令。

常见 Systemd 目标详解

Systemd 提供了各种预定义目标,每个目标对应特定的系统状态或功能。理解这些对于系统管理至关重要:

  • default.target:这是最重要的目标,因为它定义了系统启动到的默认状态。它通常是指向 graphical.target(用于桌面)或 multi-user.target(用于服务器)的符号链接。
  • graphical.target:此目标通常用于具有图形桌面环境的系统。它会拉入 multi-user.target,然后添加图形登录管理器和显示服务器(例如 GDM、LightDM、Xorg、Wayland)所需的服务。
  • multi-user.target:这是没有图形界面的多用户系统的标准状态。它常见于服务器,并提供命令行访问、网络和大多数守护进程操作所需的所有必要服务。
  • basic.target:一个最小状态,包含基本操作所需的基本系统服务,但在 multi-user.target 之前。它通常拉入 sysinit.target 和其他基本服务。
  • sysinit.target:此目标在启动过程早期达到。它负责核心系统初始化任务,例如挂载 /etc/fstab 文件系统(不包括远程文件系统)、设置交换分区以及其他与硬件相关的初始化。
  • local-fs.target:确保 /etc/fstab 中指定的所有本地文件系统都已挂载。
  • remote-fs.target:确保 /etc/fstab 中指定的所有远程文件系统(例如 NFS、CIFS)都已挂载。
  • network.target:表示基本网络连接可用(例如,网络接口已启动)。它不保证完整的互联网连接或 IP 地址分配。
  • network-online.target:一个同步点,供希望等待网络管理器认为网络在线的服务使用。它不证明互联网、DNS 或远程 API 可访问,并且仅在相关的 wait-online 服务启用时才能按预期工作。
  • rescue.target:提供一个单用户 shell,运行最少服务并挂载本地文件系统。适用于系统恢复和故障排除。
  • emergency.target:比 rescue.target 更小的环境。它在根文件系统上提供一个 shell,该文件系统通常以只读方式挂载。不启动其他服务。适用于关键紧急情况。
  • poweroff.targetreboot.targethalt.target:这些目标分别用于关闭、重启或停止系统。激活时,它们会停止大多数服务,并为所需的电源状态准备系统。

管理 Systemd 目标

与 systemd 目标交互主要涉及 systemctl 命令行工具。

查看活动目标和默认目标

查看系统当前运行在哪个目标中:

systemctl get-default

列出所有当前加载的目标单元:

systemctl list-units --type=target

此命令显示活动、已加载和静态目标及其描述。

更改默认启动目标

你可以更改系统默认启动到的目标。例如,将 multi-user.target 设置为默认目标:

sudo systemctl set-default multi-user.target

要恢复为 graphical.target

sudo systemctl set-default graphical.target

此命令会创建一个从 /etc/systemd/system/default.target 到所需目标文件的符号链接。

临时启动到不同目标

有时你只需要临时启动到特定目标(例如,用于故障排除)。你可以通过在启动时附加内核参数来实现。当 GRUB 启动菜单出现时,编辑启动项(通常按 e),并在内核命令行中添加 systemd.unit=target_name.target

例如,要启动到救援模式:

systemd.unit=rescue.target

运行时切换目标

你可以使用 systemctl isolate 命令在系统运行时切换到不同的目标。此命令将停止新目标不需要的所有服务,并启动新目标所需的所有服务。

警告:使用 systemctl isolate 可能会中断系统操作,尤其是在桌面机器上从 graphical.target 切换到 multi-user.target 等较低级别的目标时。请谨慎使用。

要从 graphical.target 切换到 multi-user.target

sudo systemctl isolate multi-user.target

要返回 graphical.target(假设它是之前的状态):

sudo systemctl isolate graphical.target

创建自定义目标

虽然 systemd 提供了许多有用的目标,但你可能会发现创建自定义目标是有益的。这对于复杂的应用程序部署尤其如此,你需要将几个应始终同时启动和停止的服务分组,或者为你的应用程序定义特定环境。

要创建自定义目标:

  1. 创建 .target 文件:将其放置在 /etc/systemd/system/ 中。例如,my-application.target
    # /etc/systemd/system/my-application.target
    [Unit]
    Description=我的自定义应用程序目标
    Wants=my-database.service my-webserver.service
    After=my-database.service my-webserver.service
    
    • Description:人类可读的描述。
    • Wants=:列出此目标应拉入的服务或其他目标。
    • After=:定义顺序。目标将在这些单元之后启动。
  2. 创建服务:确保 my-database.servicemy-webserver.service(或你列出的任何服务)存在并已正确配置。
  3. 重新加载 systemd:通知 systemd 新的单元文件。
    
    

sudo systemctl daemon-reload 4. **启用和启动**:你现在可以启用和启动自定义目标,这将依次启动其想要的服务。 bash sudo systemctl enable my-application.target sudo systemctl start my-application.target ```

这允许你将一组相关服务作为单个逻辑单元进行管理,从而简化复杂的应用程序部署。

运行级别和目标(不含模糊概念)

如果你来自 SysVinit,粗略的映射如下:

旧运行级别概念 常见的 systemd 目标
单用户修复模式 rescue.target
多用户文本模式 multi-user.target
多用户图形模式 graphical.target
重启 reboot.target
关机 poweroff.target

将其视为翻译辅助工具,而不是完美模型。SysV 运行级别是一小组固定的编号状态。Systemd 目标是有依赖关系的命名单元,并且可以有很多。软件包可以安装自己的目标。你可以为部署工作流创建一个。有些目标旨在被隔离;其他目标只是在启动过程中使用的分组点。

你可以使用以下命令查看哪些目标允许隔离:

systemctl show multi-user.target -p AllowIsolate
systemctl show basic.target -p AllowIsolate

这很重要,因为 systemctl isolate 不是一个无害的“切换视图”命令。它会停止不属于新目标事务的单元。在桌面上,隔离 multi-user.target 通常会停止图形会话。在远程服务器上,隔离错误的目标可能会停止网络或登录服务,并将你锁定在外。

服务如何成为目标的一部分

大多数日常目标成员资格来自服务文件的 [Install] 部分:

[Install]
WantedBy=multi-user.target

当你运行:

sudo systemctl enable myapp.service

systemd 会在如下目录下创建一个符号链接:

/etc/systemd/system/multi-user.target.wants/myapp.service

该符号链接使得 multi-user.target 在启动期间想要该服务。服务文件可以存在并且完全有效,但未启用。在这种情况下,启动 multi-user.target 不会自动拉入它。

这就是为什么 systemctl start myapp.servicesystemctl enable myapp.service 解决不同的问题。start 立即运行它。enable 将其连接到未来的启动目标。enable --now 两者都做。

要检查服务是否为目标启用:

systemctl is-enabled myapp.service
systemctl list-dependencies multi-user.target | grep myapp

如果服务可以手动启动但无法在启动时启动,这是首先要检查的事项之一。

一个实际有用的小型自定义目标

自定义目标在它们为操作员提供单个命令来管理一组相关单元时最为有用。想象一个简单的应用程序栈:

app-api.service
app-worker.service
app-scheduler.service

你可以创建:

# /etc/systemd/system/app-stack.target
[Unit]
Description=应用程序栈
Wants=app-api.service app-worker.service app-scheduler.service
After=network-online.target
Wants=network-online.target
AllowIsolate=no

然后将每个服务添加到目标:

[Install]
WantedBy=app-stack.target

daemon-reload 之后,根据你想要的行为启用服务或目标:

sudo systemctl daemon-reload
sudo systemctl enable app-api.service app-worker.service app-scheduler.service
sudo systemctl start app-stack.target

这为你提供了一个可读的分组,而无需假装目标是进程管理器。如果 app-worker.service 失败,请检查该服务。目标只是分组点。

如果你希望停止目标也停止所有栈服务,请将 PartOf=app-stack.target 添加到每个服务:

[Unit]
PartOf=app-stack.target

现在 systemctl stop app-stack.target 会传播到成员服务。这通常是自定义目标示例中缺失的部分。

使用目标进行故障排除

目标对于排查启动问题或服务故障也非常有价值:

  • 识别依赖关系:如果服务启动失败,检查它所属的目标可以揭示缺失或失败的依赖关系。使用 systemctl status <service_name>systemctl list-dependencies <target_name>
  • 启动到最小目标:如果你的系统无法启动到 graphical.targetmulti-user.target,请尝试使用内核参数方法启动到 rescue.targetemergency.target。这提供了一个最小环境,你可以在其中诊断问题,而无需处理许多正在运行的服务的复杂性。
  • 检查日志:在尝试启动目标或服务后,始终检查 journalctl 日志中的错误:
    journalctl -b -u <target_or_service_name>
    

最佳实践和提示

  • 谨慎使用 network-online.target:如果你的服务在启动前需要网络配置,请将 After=network-online.targetWants=network-online.target 结合使用,并确认适当的 wait-online 单元已启用。仍然在应用程序中为远程依赖关系保留重试逻辑。
  • 理解启动顺序:熟悉从 sysinit.targetbasic.target,再到 multi-user.target/graphical.target 的一般流程。这有助于调试在启动过程早期失败的服务。
  • 谨慎对待 default.target:更改 default.target 可能会显著改变系统的启动行为。始终先在非生产环境中测试自定义配置。
  • 对非关键依赖关系使用 Wants=:对于有用但并非目标被视为“启动”所必需的服务,请使用 Wants= 而不是 Requires=。这可以防止单个可选服务故障级联并阻止整个目标激活。

应牢记的心智模型

目标是一个命名状态,而不是一个守护进程。default.target 决定正常的启动目的地。multi-user.target 是通常的服务器状态。graphical.target 添加了显示栈。rescue.targetemergency.target 是修复工具。自定义目标是在它们使操作更清晰时的分组工具。

当与目标相关的问题发生时,避免首先责怪目标。询问哪个单元被拉入,哪个排序规则延迟了它,以及哪个依赖关系失败了。systemctl catsystemctl list-dependenciessystemctl showjournalctl -b 通常比阅读通用启动图更快地回答这些问题。