![systemd 服务关闭并进行磁盘访问](https://linux22.com/image/164042/systemd%20%E6%9C%8D%E5%8A%A1%E5%85%B3%E9%97%AD%E5%B9%B6%E8%BF%9B%E8%A1%8C%E7%A3%81%E7%9B%98%E8%AE%BF%E9%97%AE%20.png)
我想运行一个小备份脚本(btrfs 快照 + 传输到 USB)在系统关闭时。
以下服务在关闭时执行得非常好,但在重新启动时却无法按预期执行:
[Unit]
Description=Backup on poweroff to external encrypted USB disk.
DefaultDependencies=no
Before=shutdown.target
[Service]
Type=oneshot
ExecStart=/usr/bin/sleep 30
TimeoutSec=infinity
RemainAfterExit=yes
[Install]
WantedBy=poweroff.target
一旦该服务按预期工作,其中/usr/bin/sleep 30
将被实际的备份脚本替换。
但为了使备份脚本正常工作,我需要在启动之前使用 cryptsetup 解密外部 USB 磁盘。这一切都是通过/etc/crypttab
( noauto
) 和 systemd (通过)的组合在后台处理的systemd-cryptsetup-generator
,它会自动生成一个 systemd 单元文件systemd-cryptsetup@<name>.service
,我可以在上面的单元中引用该文件。
但是一旦我将以下要求添加到 [unit] 部分:
[email protected]
[email protected]
在系统关闭/断电期间,该服务不再启动。
通过手动启动服务systemctl start poweroff-backup.service
没有问题,因此服务文件本身没有问题...:-/
我思考问题是poweroff.target
viasystemd-poweroff.service
需要umount.target
反过来卸载所有设备(包括 cryptsetup '坐骑') 通过组合冲突者=和之后=systemd mount 和 cryptsetup 单元的语句。但是添加另一个要求umount.target
不会改变任何东西,例如:
Before=umount.target
没有任何额外的要求,我可以清楚地看到该服务总是在umount.target
达到后立即启动。
答案1
好吧,我发现了这个问题,从中我能够构建一个正确工作的解决方案,该解决方案仍然遵循 systemd 工作流程(大部分情况下)。
问题
以下服务虽然乍一看是正确的,但永远不会被执行:
# poweroff-backup.service
[Unit]
Description=Backup on poweroff to external encrypted USB disk.
DefaultDependencies=no
Before=shutdown.target
# This causes the service to be discarded/skipped due to
# a dependency cycle conflict. See below...
[email protected]
[email protected]
Before=umount.target
[Service]
Type=oneshot
# Note: The script executed here needs access to '/', '/tmp'
# (or Systemd's PrivateTemp) and '/dev/mapper/ext' which is the
# decrypted USB partition managed by /etc/crypttab and
# systemd-cryptsetup-generator (as [email protected])
ExecStart=/usr/bin/sleep 30
TimeoutSec=infinity
# Note: This does not make any difference
RemainAfterExit=yes
[Install]
WantedBy=poweroff.target
该服务永远不会启动的原因是,之间存在依赖循环问题/冲突,其中 shutdown.target 与 cryptsetup.target 冲突,以确保在关闭(断电/重新启动)之前分离所有加密磁盘和 poweroff-backup.service需要通过 poweroff.target 激活它。poweroff.target -[require]-> shutdown.target -[conflicts]-> cryptsetup.target <-[require]- [email protected] <-[require]- poweroff-backup.service <-[wants]- poweroff.target
解决方案
知道了这一点,明显的问题在于我错误的假设,即 systemd 可以通过知道 poweroff-backup.service 是一个“一击“一旦 poweroff-backup.service 被激活并转换为非活动/完成后,系统可以首先解决该服务,同时继续正常关闭转换。
因此,这里有一个使用 ExecStop= 而不是 ExecStart= 的服务,它允许我们避免任何依赖循环,但有一些小警告:
# poweroff-backup.service
[Unit]
Description=Backup external encrypted USB disk on poweroff.
[email protected]
After=multi-user.target
[email protected]
[Service]
Type=oneshot
ExecStart=/usr/bin/echo "Waiting for poweroff..."
# Note: We need this, since there is no other way to detect poweroff vs reboot now.
ExecStop=/usr/bin/systemctl list-jobs | /usr/bin/egrep -q 'poweroff.target.*start'
# Note: This should be replaced with your script
ExecStart=/usr/bin/sleep 30
TimeoutSec=infinity
RemainAfterExit=true
[Install]
WantedBy=multi-user.target
这是有效的,因为:
- 防止shutdown.target、cryptsetup.target和poweroff-backup.service引起的依赖循环问题
- 正确的工作顺序仍然保留,因为 systemd 确保关闭顺序与启动顺序相反(如 After= 和 Before= 给出)
我对这个解决方案只有两个小抱怨:
由于该服务在登录时启动,因此它也会在登录时解密外部 USB 磁盘。我希望这种情况仅在执行实际备份脚本时发生,同时仍使用正常的 systemd After= 和 Require= 顺序和依赖机制。这可以通过添加以下内容来处理,但这意味着我没有使用 cryptsetup 服务来解密/分离 USB 磁盘:
ExecStop=/usr/lib/systemd/systemd-cryptsetup attach ext [...] ExecStop=/usr/lib/systemd/systemd-cryptsetup detach ext
我没有任何方法可以通过引用 shutdown.target 有条件地执行 ExecStop= 仅在关闭时执行,并且必须依赖于使用的一些辅助命令
systemctl list-jobs
。
问题:有人知道如何改进这些点吗?
完整的服务文件
这是我当前的服务文件,只有在安装 USB 磁盘后,通过将 (WantedBy=) 连接到 USB 设备(通过其 UUID)而不是 multi-user.target 来启动服务:
# poweroff-backup.service
[Unit]
Description=Backup external encrypted USB disk on poweroff.
[email protected]
Requires=-.mount
Requires=tmp.mount
After=dev-disk-by\<uuid here>.device
[email protected]
After=-.mount
After=tmp.mount
[Service]
Type=oneshot
ExecStart=/usr/bin/echo "Waiting for poweroff to trigger snapshot and archive transfer..."
ExecStop=/usr/bin/systemctl list-jobs | /usr/bin/egrep -q 'poweroff.target.*start'
ExecStop=-/usr/local/bin/btrfs-snapshots.sh --device='UUID=<uuid>' @ @boot @home
ExecStop=/usr/local/bin/btrfs-archive.sh --source='UUID=<uuid>' --target=/dev/mapper/ext
TimeoutSec=infinity
RemainAfterExit=true
[Install]
WantedBy=dev-disk-by\<uuid here>.device
答案2
我的用例略有不同,我想确保所有其他服务都停止并在重新启动期间使用 Rsync 执行备份。以下内容在 Ubuntu 22.04 上对我有用。我将这个答案留在这里给那些希望将来在关机时运行脚本的人。
服务文件如下所示:
[Unit]
Description=Run my custom task at shutdown
DefaultDependencies=no
Before=systemd-reboot.service
After=final.target
[Service]
Type=oneshot
ExecStart=/usr/local/scripts/custom_script.sh
TimeoutStartSec=0
[Install]
WantedBy=systemd-reboot.service
我的custom_script.sh
样子如何:
#!/bin/sh
ps aux > /usr/local/scripts/processes.txt
df -Th > /usr/local/scripts/df.txt
mount > /usr/local/scripts/mount.txt
sleep 180
由于这只是为了测试,我运行是
sudo chmod 777 -R /usr/local/scripts/
为了防止出现权限问题。
以下是我测试的步骤:
- 为了确保脚本在关机和重新启动时运行,我
sleep 180
在custom_script.sh
.接下来,我关闭操作系统。仅 3 分钟后操作系统就完全关闭。当脚本运行(等待)时,会显示 Ubuntu 加载徽标。我启动了系统并重新启动了它。显示相同的 Ubuntu 加载徽标,3 分钟后系统重新启动。这证明该脚本正在关机/或重新启动时运行。 - 确保所有其他服务均已停止;我记录了
custom_script.sh
运行时正在运行的所有进程ps aux > processes.txt
。的输出processes.txt
不包含其他服务。这证明所有其他服务都已停止。 - 为了确保文件系统仍然已安装,我使用
df -Th > df.txt
和记录了安装情况mount > mount.txt
。文件的输出显示我的所有分区均已安装并处于读写模式。