如何覆盖或配置systemd服务?

许多sysv init脚本使用/etc/default中的相应文件来允许管理员进行配置。可以使用.override文件来修改Upstart作业。现在Ubuntu默认使用systemd,我该如何覆盖或配置systemd单元?

4请注意,当使用空白条目清除ExecStart=时,您不能在其后添加注释,例如:ExecStart= # 清除先前的条目。 这将被视为另一个ExecStart=条目并添加到列表中。附言:由于我的声望较低,我无法对muru的回答进行评论。 - tysik
1个回答

systemd单元不需要遵守/etc/default中的文件。 systemd易于配置,但需要您了解systemd单元文件的语法。

软件包通常在/lib/systemd/system/中提供单元文件。这些文件不应该被编辑。相反,systemd允许您通过在/etc/systemd/system/中创建适当的文件来覆盖这些文件。

对于给定的服务foo,软件包将提供/lib/systemd/system/foo.service。您可以使用systemctl status foo检查其状态,或使用journalctl -u foo查看其日志。要覆盖foo的定义中的某些内容,请执行以下操作:

sudo systemctl edit foo

这将在/etc/systemd/system目录下创建一个以单元名称命名的目录,并在该目录中创建一个override.conf文件(/etc/systemd/system/foo.service.d/override.conf)。您可以使用此文件(或其他/etc/systemd/system/foo.service.d/目录中的.conf文件)添加或覆盖设置。这也适用于非服务单元-您可以执行systemctl edit foo.mount,systemctl edit foo.timer等操作。
覆盖命令参数
以getty服务为例。假设我想让TTY2自动登录到我的用户(这不是建议的做法,只是一个示例)。TTY2由getty@tty2服务运行(tty2是模板/lib/systemd/system/getty@.service的实例)。要做到这一点,我必须修改getty@tty2服务。
$ systemctl cat getty@tty2
# /lib/systemd/system/getty@.service
#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.

[Unit]
Description=Getty on %I
Documentation=man:agetty(8) man:systemd-getty-generator(8)
Documentation=http://0pointer.de/blog/projects/serial-console.html
After=systemd-user-sessions.service plymouth-quit-wait.service
After=rc-local.service

# If additional gettys are spawned during boot then we should make
# sure that this is synchronized before getty.target, even though
# getty.target didn't actually pull it in.
Before=getty.target
IgnoreOnIsolate=yes

# On systems without virtual consoles, don't start any getty. Note
# that serial gettys are covered by serial-getty@.service, not this
# unit.
ConditionPathExists=/dev/tty0

[Service]
# the VT is cleared by TTYVTDisallocate
ExecStart=-/sbin/agetty --noclear %I $TERM
Type=idle
Restart=always
RestartSec=0
UtmpIdentifier=%I
TTYPath=/dev/%I
TTYReset=yes
TTYVHangup=yes
TTYVTDisallocate=yes
KillMode=process
IgnoreSIGPIPE=no
SendSIGHUP=yes

# Unset locale for the console getty since the console has problems
# displaying some internationalized messages.
Environment=LANG= LANGUAGE= LC_CTYPE= LC_NUMERIC= LC_TIME= LC_COLLATE= LC_MONETARY= LC_MESSAGES= LC_PAPER= LC_NAME= LC_ADDRESS= LC_TELEPHONE= LC_MEASUREMENT= LC_IDENTIFICATION=

[Install]
WantedBy=getty.target
DefaultInstance=tty1

特别是,我必须更改当前的ExecStart行。
$ systemctl cat getty@tty2 | grep Exec     
ExecStart=-/sbin/agetty --noclear %I $TERM

要覆盖这个,做如下操作:
sudo systemctl edit getty@tty2

并且添加:
[Service]
ExecStart=
ExecStart=-/sbin/agetty -a muru --noclear %I $TERM

请注意:
1. 在重新设置之前,我不得不明确清除ExecStart,因为它是一个添加设置,类似于其他列表,如Environment(作为整体,而不是每个变量)和EnvironmentFile;与覆盖设置相反,如RestartSecType。只有对于Type=oneshot服务,ExecStart才能具有多个条目。请注意,依赖关系设置,如BeforeAfterWants等也是列表,但不能使用此方法清除。您将不得不覆盖/替换整个服务(见下文)。 2. 我必须使用正确的部分标题。在原始文件中,ExecStart位于[Service]部分中,因此我的覆盖也必须将ExecStart放在[Service]部分中。通常,使用systemctl cat查看实际服务文件将告诉您需要覆盖哪些内容以及它所在的部分。 通常,如果您编辑了一个systemd单元文件,为使其生效,您需要运行:
sudo systemctl daemon-reload

然而,systemctl edit会自动为您完成这个操作。
现在:
$ systemctl cat getty@tty2 | grep Exec
ExecStart=-/sbin/agetty --noclear %I $TERM
ExecStart=
ExecStart=-/sbin/agetty -a muru --noclear %I $TERM

$ systemctl show getty@tty2 | grep ExecS
ExecStart={ path=/sbin/agetty ; argv[]=/sbin/agetty -a muru --noclear %I $TERM ; ... }

如果我这样做:
sudo systemctl restart getty@tty2

按下 CtrlAltF2,瞧!我将登录到那个TTY上的我的账户。
正如我之前所说,getty@tty2 是一个模板的实例。那么,如果我想要覆盖所有该模板的实例怎么办?可以通过编辑模板本身来实现(删除实例标识符 - 在这种情况下是 tty2):
systemctl edit getty@

覆盖环境

/etc/default文件的常见用途是设置环境变量。通常,/etc/default是一个shell脚本,因此您可以在其中使用shell语言结构。然而,对于systemd来说,情况并非如此。您可以通过两种方式指定环境变量:

通过文件

假设您已经在一个文件中设置了环境变量:

$ cat /path/to/some/file
FOO=bar

然后,您可以添加到覆盖部分中:
[Service]
EnvironmentFile=/path/to/some/file

特别是,如果您的/etc/default/grub只包含赋值而没有shell语法,您可以将其用作EnvironmentFile
通过Environment条目
上述也可以通过以下覆盖来实现:
[Service]
Environment=FOO=bar

然而,当涉及到多个变量、空格等情况时,这可能会变得棘手。请参考我其他回答之一,以了解此类情况的示例。

编辑中的变化

完全替换现有单元

如果您想对现有单元进行大规模更改,以至于实际上是完全替换它,您可以简单地执行以下操作:

systemctl edit --full foo

临时编辑

在systemd文件层次结构中,/run优先于/etc,而/etc又优先于/lib。前面所说的一切也适用于使用/run/systemd/system代替/etc/systemd/system。通常,/run是一个临时文件系统,在重新启动时其内容会丢失,因此如果您只想在重启之前覆盖一个单元,可以执行以下操作:

systemctl edit --runtime foo

撤销更改
你可以简单地删除相应的覆盖文件,并执行systemctl daemon-reload来让systemd读取更新后的单元定义。
你也可以“恢复”所有更改:
systemctl revert foo

进一步阅读

通过这种机制,覆盖 systemd 单元变得非常容易,同样也可以通过简单删除覆盖文件来撤销这些更改。这些并不是唯一可以修改的设置。

以下链接可能会有用:


3在设置非oneshot类型的服务之前,您必须清除变量。这解决了我的问题。 - Colin
有人能告诉我在systemd文档中哪里可以找到关于使用ExecStart=清除继承值的信息,就像这个答案中所示吗? - Mark Edington
5@MarkEdington 来自systemd.service(5)手册,ExecStart部分的翻译:"除非Type=是oneshot,否则必须给出一个命令。当使用Type=oneshot时,可以指定零个或多个命令。可以通过在同一指令中提供多个命令行来指定命令,或者,可以多次指定此指令并具有相同的效果。如果将空字符串分配给此选项,则会重置要启动的命令列表,先前分配此选项的操作将不起作用。" - muru
如何使用systemctl移除覆盖?我不能只是清空文件。 - Tomilov Anatoliy
2@东方,你可以使用sudo rm命令删除覆盖文件,然后执行systemctl daemon-reload命令,或者你可以使用systemctl edit命令,在覆盖文件中用注释替换所有内容。服务文件中的注释以#符号开头。 - muru
8@Orient systemctl revert foo - Ayell
2三种方法(覆盖文件、环境文件、环境变量)的优先顺序是什么?也就是说,对于在这三个地方都定义了的变量,哪个值将是有效的? - Nikolaos Kakouros
如果我想通过添加/更改一些命令行参数来覆盖ExecStart,但是我不知道可执行文件的路径,我该如何做呢?我需要复制可执行文件的路径吗?还是有一种方法可以保留它?另外,如果这是一个累加设置,似乎意味着应该创建一个“默认”命令,可以通过追加更多选项来覆盖先前的选项。 - CMCDragonkai
1@CMCDragonkai 是的,你必须知道路径 - 你可以通过使用 systemctl catsystemctl show 很容易地获取到。它是累加的意思,多个实例会被添加到一个列表中,而不是添加到列表中的某个项目上。 - muru
覆盖中的双重ExecStart是否实际上是必需的,还是只是为了隐藏systemctl show中的先前值? - xenoterracide
3@xenoterracide 这取决于你是想替换之前的值还是只是添加另一个要执行的命令。 - muru
3非常好的回答,除了一个部分 - 它让你相信你可以重置After,基于这句话:“在重新设置之前,我必须明确地清除ExecStart,因为它是一种可加性设置,类似于After...” 尽管它确实是一个列表/可加性设置 - 正如我从其他地方找到的链接 - 但你无法重置依赖类型的设置... - Krisztián Szegi
1@KrisztiánSzegi 啊,已经注意到了,并且我已经相应地更新了文本。我现在看到,在man systemd.unit 的最后一段中明确指定了这一点。 - muru
@muru 很好!感谢你花时间解释清楚这个问题! - Krisztián Szegi
顶级的插件怎么样?我在其他地方看到有关于systemd的文档,但在Ubuntu上(至少在18.04版本),它们似乎不起作用。 - cueedee
1@cuedee 我认为你需要一个更新的systemd版本来实现这个。18.04的manpage没有提到它,但你可以在20.04的manpage中找到相关信息。 - muru
@muru - 你说得对!事实证明,顶级插件需要 systemd 版本 244 或更高。Ubuntu 20 版本有 245,而 18 版本有 237 - cueedee