我有一个 shell 脚本,它在后台运行三个程序,在前台运行几个程序,然后运行trap
和wait
,并且我已经设置了一个单元文件,以便systemd
可以启动它并在失败时重新启动它。
然而,我发现,如果一个进程终止,它不会杀死该脚本中的所有内容并重新启动它。对于此应用程序,如果其中任何一个死亡,则必须重新启动它们。
我看到两条合理的路径:
- 配置单元文件并可能更改脚本,以便检测到异常并杀死所有异常,然后重新运行脚本。我不知道该怎么做。
- 将三个后台进程中的每一个进程配置为具有单独文件的自己的单元
.service
。但我不知道如何编写.service
文件来杀死并重新启动其中任何一个失败的文件。我知道我可以安排它们的依赖关系,以便它们按顺序启动,但我不知道如何让它在 #2 死亡时杀死 #1,反之亦然。
我不想写一个管理器或让程序弄清楚并自行终止 - 这就是目的systemd
- 我希望我只是错过了正确的咒语。
.服务文件:
[Unit]
Description=Foobar Interface
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/user/scripts
ExecStart=/home/user/scripts/myscript.sh
Restart=always
[Install]
WantedBy=multi-user.target
重击脚本:
#!/usr/bin/env bash
tty_port=/dev/ttyUSB0
#Clean up any old running processes
pkill -f "cat ${tty_port}"
pkill transport
pkill backgroundprogram
#Configure the target
source /home/user/somescript.sh
foregroundprogram
#Set up the serial port
stty -F $tty_port 115200
#Read from the port in the background
cat $tty_port &
tty_pid=$!
#Wait for tty device to waken
sleep 15
#Send commands to tty device
echo "command1" > $tty_port
sleep 1
echo "command2" > $tty_port
sleep 1
#Start up the transport
/home/user/transport &>> /dev/null &
transport_pid=$!
#Wait a bit for the transport to start
sleep 1
#Start up the main process
/home/user/backgroundprogram &
background_pid=$!
#Wait a bit for it to start
sleep 1
#Finally, start the tty device
echo "command3" > $tty_port
trap "kill ${background_pid} ${tty_pid} ${transport_pid}; exit 1" INT
wait
这一切都有效,写入日志等,但是当三个进程中的任何一个失败时,它都会继续运行,不会杀死并重新启动所有进程。
答案1
然而,我发现,如果一个进程终止,它不会杀死该脚本中的所有内容并重新启动它。对于此应用程序,如果其中任何一个死亡,则必须重新启动它们。
systemd 正在监视您的 shell 脚本,而不是它的子脚本。你不会想systemd 响应子进程的退出,因为这会导致重新启动每次运行命令时。考虑一下,如果我有一个运行的 shell 脚本...
date
我刚刚生成了一个子进程,它运行,然后退出。我不希望这触发我的流程主管采取任何行动。
如果您希望 systemd 监视子进程,请为每个进程创建一个单独的单元文件:
- 一台用于配置和读取串口的单元
- 一个为
/home/user/transport
- 一个为
/home/user/backgroundprogram
您可以使用 systemd 依赖项来确保服务的正确启动顺序(并确保如果您停止一个服务,它们都会停止),并且您可以使用指令从文件EnvironmentFile
加载配置(例如)。$tty_port
您可能会将一些设置命令(“向 tty 设备发送命令...”)放入一行ExecStartPre
,或者它们可能会获得自己的Type=oneshot
服务。
答案2
如果您可以将主脚本拆分为单独的服务,您可以像这样轻松解决它:
在下面的示例中,我有三个重生服务:s1、s2 和 s3,并通过目标 s.target 将它们作为一个组进行控制。
注意:
如果将三个服务配置为Requires
如果您按照s.target 中的全部该组中的参与进程将重新启动。
或者,如果您按照 s.target 中的方式配置它们Wants
,那么如果其中一个崩溃并重生,则仅重新启动该单独的进程。
对于每个服务,创建一个服务文件s1、s2、s3:
/etc/systemd/system/s1.service :
[Unit]
Description=my worker s1
After=network.target
Before=foobar.service
PartOf=s.target
[Service]
Type=simple
ExecStart=/usr/local/bin/s1.sh
Restart=always
(注意:如果您的服务相同,您可以创建一 [电子邮件受保护]文件而不是多个文件。在手册中查找使用 @ 和 %i 的服务实例。)
现在创建需要 s1、s2 和 s3 服务的主目标(组)文件:
/etc/systemd/system/s.target :
[Unit]
Description=main s service
Requires=s1.service s2.service s3.service
# or
# Wants=s1.service s2.service s3.service
[Install]
WantedBy=multi-user.target
完毕。
像往常一样,你现在必须运行systemctl daemon-reload
。
现在您可以启动您的服务并systemctl start s.target
启动 s1、s2 和 s3。
您可以通过systemctl stop s.target
停止 s1、s2 和 s3 来停止所有三个服务。
当然,您可以像往常一样启动/停止/重新启动/状态各个服务:
systemctl status s1
如果您终止 s1、s2 或 s3 进程,它将自动重新生成(Restart=always)。
如果您使用Requires
,则组中的所有进程都将重新启动。
PS:systemctl enable s.target
如果您想在启动时启动服务,请运行。
PS:不幸的是,当使用 systemctl 时,您不能像使用“s1”那样使用简写词“s”来表示“s.target”,而不是键入完整的“s1.service”。当你想管理该组时,你必须输入“s.target”。
答案3
#!/usr/bin/env/python3
# POSIX shell and bash < 4.3 doesn't want to do this.
# https://unix.stackexchange.com/questions/285156/exiting-a-shell-script-if-certain-child-processes-exit
#
# If you haven't written python3 before, be aware the string type
# is Unicode (UTF-8). Python 3.0 aborts on invalid UTF-8.
# Python 3.1 aims to round-trip invalid UTF-8 using "surrogateescape".
# Python 3.2 may accept non-UTF-8 encoding according to your locale.
# ...
#
# * Functions should be better tested.
#
# * Doesn't bother killing (and waiting for) child processes.
# Assumes systemd does it for us.
# Convenient, but I'm not 100% happy about it.
#
# * Otherwise direct translation of nasty script, e.g. use of "sleep".
import sys
import os
import time
tty_port = "/dev/ttyS0" # or: tty_port = sys.environ["tty_port"]
def die(msg):
sys.exit(msg)
# Run program in background
def bg(*argv):
pid = os.fork()
if pid == 0:
# Child process: exec or die
# Either way, we never return from this function.
try:
os.execvp(argv[0], argv)
except Exception as e:
# By convention, child always uses _exit()
sys._exit(e)
assert False
return pid
def __fg(*argv):
pid = bg(*argv)
(_, status) = os.waitpid(pid, 0)
return status
# Run program, wait for exit, die if the program fails
def fg(*argv):
status = __fg(*argv)
if os.WIFEXITED(status):
code = os.WEXITSTATUS(status)
if code != 0:
die("exit status {} from running {}".format(code, argv))
elif os.WIFSIGNALED(status):
die("signal {} when running {}"
.format(os.WTERMSIG(status), argv))
else:
assert False, "Unexpected result from waitpid()"
# Use with care.
# "Any user input that is employed as part of command should be carefully sanitized, to ensure that unexpected shell commands or command options are not executed."
#
def bg_shell(cmd):
return bg("/bin/sh", "-c", cmd)
def fg_shell(cmd):
return fg("/bin/sh", "-c", cmd)
fg("stty", "-F", tty_port, "115200")
tty_pid = bg("cat", tty_port)
print("\"cat {}\" started as pid {}".format(tty_port, tty_pid))
time.sleep(15)
tty_out = open(tty_port, "w")
def tty_print(msg):
tty_out.write(msg)
tty_out.flush()
tty_print("command1")
time.sleep(1)
tty_print("command2")
time.sleep(1)
transport_pid = bg_shell("exec /home/user/transport >/dev/null 2>&1")
print("transport started as pid {}".format(transport_pid))
time.sleep(1)
tty_print("command3")
time.sleep(1)
background_pid = bg("/home/user/backgroundprogram")
print("backgroundprogam started as pid {}".format(background_pid))
(pid, status) = os.wait()
# This could be modified to accept exit code 0 as a success,
# and/or accept exit due to SIGTERM as a success.
if os.WIFEXITED(status):
die("exit status {} from pid {}".format(os.WEXITSTATUS(status)), pid)
elif os.WIFSIGNALED(status):
die("signal {} when running {}".format(os.WTERMSIG(status), pid))
else:
assert False, "Unexpected result from wait()"
答案4
bash
在该命令的最新版本中,wait
有一个选项-n
可以等待任何后台进程结束,然后退出。
此外,由于尚不清楚的原因,cat
偶尔会在启动和等待之间退出,但直到 才宣布结束wait
。所以我jobs
在 wait 之前添加了一个命令, whcih 似乎检查是否cat
已退出。如果有,wait 只关注剩下的两个进程。如果尚未退出,则当三个进程中的任何一个结束时,等待结束。
所以我的脚本中的最后wait
一行被替换为
jobs
wait -n
如果调用 wait 后任何作业结束,则 wait 退出,systemd 终止所有剩余的子进程,并重新启动脚本。