我有一个 Bash 脚本,看起来与此类似:
#!/bin/bash
echo "Doing some initial work....";
/bin/start/main/server --nodaemon
现在,如果运行脚本的 bash shell 收到 SIGTERM 信号,它还应该向正在运行的服务器发送 SIGTERM(这会阻塞,因此不可能出现陷阱)。那可能吗?
答案1
尝试:
#!/bin/bash
_term() {
echo "Caught SIGTERM signal!"
kill -TERM "$child" 2>/dev/null
}
trap _term SIGTERM
echo "Doing some initial work...";
/bin/start/main/server --nodaemon &
child=$!
wait "$child"
通常,bash
在子进程执行时会忽略任何信号。使用 启动服务器&
会将其置于 shell 作业控制系统的后台,并$!
保存服务器的 PID(与 和 一起使用wait
)kill
。然后调用wait
将等待具有指定 PID(服务器)的作业完成,或任何要发射的信号。
当 shell 接收到SIGTERM
(或服务器独立退出)时,wait
调用将返回(使用服务器的退出代码退出,或者在收到信号的情况下使用信号号 + 128 退出)。之后,如果 shell 收到 SIGTERM,它将_term
在退出之前调用指定为 SIGTERM 陷阱处理程序的函数(其中我们进行任何清理并使用 手动将信号传播到服务器进程kill
)。
答案2
Bash 不会将 SIGTERM 之类的信号转发给当前正在等待的进程。如果你想结束你的脚本继续进入你的服务器(允许它处理信号和其他任何东西,就像你直接启动服务器一样),你应该使用exec
,这将将 shell 替换为正在打开的进程:
#!/bin/bash
echo "Doing some initial work....";
exec /bin/start/main/server --nodaemon
如果您出于某种原因需要保留 shell(即您需要在服务器终止后进行一些清理),您应该使用trap
、wait
和的组合kill
。看传感器史密斯的回答。
答案3
安德烈亚斯·维森指出如果您不需要从调用中返回(就像OP的示例中一样),只需通过命令调用exec
就足够了(@Stuart P. Bentley 的回答)。否则,“传统” trap 'kill $CHILDPID' TERM
(@cuonglm 的答案)是一个开始,但wait
调用实际上在陷阱处理程序运行之后返回,这仍然可能在子进程实际退出之前。因此,建议进行“额外”调用wait
(@user1463361的回答)。
虽然这是一项改进,但它仍然存在竞争条件,这意味着进程可能永远不会退出(除非信号发送器重试发送 TERM 信号)。漏洞窗口位于注册陷阱处理程序和记录子进程的 PID 之间。
以下内容消除了该漏洞(打包在函数中以供重用)。
prep_term()
{
unset term_child_pid
unset term_kill_needed
trap 'handle_term' TERM INT
}
handle_term()
{
if [ "${term_child_pid}" ]; then
kill -TERM "${term_child_pid}" 2>/dev/null
else
term_kill_needed="yes"
fi
}
wait_term()
{
term_child_pid=$!
if [ "${term_kill_needed}" ]; then
kill -TERM "${term_child_pid}" 2>/dev/null
fi
wait ${term_child_pid} 2>/dev/null
trap - TERM INT
wait ${term_child_pid} 2>/dev/null
}
# EXAMPLE USAGE
prep_term
/bin/something &
wait_term
答案4
对上述答案补充几点:
- 使用“&”在后台启动进程并按照建议等待其 pid@cuonglm将使处理程序可以在子进程运行时执行,但子进程将失去捕获任何输入的能力,因为一旦子进程分离,stdin 将立即关闭。要强制 stdin 保持打开状态,您可以添加一个通过管道传输到子进程的无限循环。看这个帖子。
然后读取当前 shell 中的输入并将其写入子进程的 proc 文件,以便它进入其标准输入:
(while true; do sleep 10000; done) | /bin/start/main/server --nodaemon &
child_pid=$!
while :
do
result=$(kill -0 $mypid > /dev/null 2>&1)
if [ $? -ne 0 ] ; then
# process is gone
break
else
# read input in the current shell and store it in a variable. The timeout only works with Bash, not with Bourne-Shell. You will need to find a way to read stdin instead and sleep 1 sec between each loop
read -t 1 input
# echo the input to the proc file of the runuser process so it goes to its stdin
echo $input > /proc/$child_pid/fd/0 2>/dev/null
fi
done
wait $child_pid
注意:这在 Linux 上运行得很好,但对于其他 Unix 平台可能需要一些调整。
编辑:更简单的方法是将标准输入复制到文件描述符,然后可以将其用作后台进程的标准输入:
exec 3<&0
/bin/start/main/server --nodaemon <&3 &
exec 是@建议的第二个解决方案斯图尔特·本特利,但有时您需要使用新的 PID 创建进程,或者使用的命令可能不允许您选择并使用新的 PID 甚至新的 PGID 创建进程(例如使用 -l 选项的 runuser 就是这种情况) 。
1) 和 2) 的替代方法是将信号发送到进程组,而不是针对特定的 PID。
这可以通过在子进程的 PID 之前使用带有减号 (-) 的 Kill 来完成:
kill -TERM -$child_pid
Bash 确实可以让您捕获针对进程组的信号,而无需在后台启动该进程。此方法不会失去子进程读取 stdin 的能力。如果您的孩子在不同的进程组中运行,这也是一个很好的解决方案,因为处理程序会让您将信号转发给孩子。限制是该组的其他成员也会收到信号,这可能是一个问题,也可能不是问题,具体取决于具体情况。