在 Bash 中将 SIGTERM 转发给子进程

在 Bash 中将 SIGTERM 转发给子进程

我有一个 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(与 和 一起使用waitkill。然后调用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(即您需要在服务器终止后进行一些清理),您应该使用trapwait和的组合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

对上述答案补充几点:

  1. 使用“&”在后台启动进程并按照建议等待其 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 &
  1. exec 是@建议的第二个解决方案斯图尔特·本特利,但有时您需要使用新的 PID 创建进程,或者使用的命令可能不允许您选择并使用新的 PID 甚至新的 PGID 创建进程(例如使用 -l 选项的 runuser 就是这种情况) 。

  2. 1) 和 2) 的替代方法是将信号发送到进程组,而不是针对特定的 PID。

这可以通过在子进程的 PID 之前使用带有减号 (-) 的 Kill 来完成:

kill -TERM -$child_pid

Bash 确实可以让您捕获针对进程组的信号,而无需在后台启动该进程。此方法不会失去子进程读取 stdin 的能力。如果您的孩子在不同的进程组中运行,这也是一个很好的解决方案,因为处理程序会让您将信号转发给孩子。限制是该组的其他成员也会收到信号,这可能是一个问题,也可能不是问题,具体取决于具体情况。

相关内容