bash脚本如何执行与Ctrl-C等效的操作来终止后台任务?

15

有没有一种方法可以调用子进程,以便像Ctrl-C前台任务一样发送中断信号给它及其所有后代? 我正在尝试杀死一个启动器脚本,该脚本调用了一个长时间运行的子进程。我尝试过 kill -SIGINT $child(不会将中断信号发送给其后代,因此是无操作),以及kill -SIGINT -$child (在交互式调用时有效,但在运行脚本时无效)。

下面是一个测试脚本。长时间运行的脚本是test.sh --child。当您调用test.sh --parent时,它会调用test.sh --child &,然后尝试杀死它。如何使父进程成功杀死子进程?

#!/bin/bash

if [ "$1" = "--child" ]; then
sleep 1000

elif [ "$1" = "--parent" ]; then
"$0" --child &
for child in $(jobs -p); do
  echo kill -SIGINT "-$child" && kill -SIGINT "-$child"
done
wait $(jobs -p)

else
echo "Must be invoked with --child or --parent."
fi

我知道你可以修改长期运行的子进程来trap信号,将它们发送到其子进程,然后等待(来自Bash script kill background (grand)children on Ctrl+C),但是否有不修改子脚本的方法?

4个回答

17

如果有人想知道,这是如何在后台启动子进程并在按下ctrl+c时终止它们的方法:

#!/usr/bin/env bash
command1 &
pid[0]=$!
command2 &
pid[1]=$!
trap "kill ${pid[0]} ${pid[1]}; exit 1" INT
wait

2
这很有用,尽管我发现kill只会杀死指定参数中的进程,而不是它们的子孙进程。 - Flimm

7

阅读此文:如何在BASH脚本之间发送SIGINT信号?

另见 info bash

   To facilitate the implementation of the user interface to job  control,
   the operating system maintains the notion of a current terminal process
   group ID.  Members of this process group (processes whose process group
   ID is equal to the current terminal process group ID) receive keyboard-
   generated signals such as SIGINT.  These processes are said  to  be  in
   the  foreground.  Background processes are those whose process group ID
   differs from the terminal's; such processes are immune to keyboard-gen‐
   erated signals. 

在it技术中,bash通过进程组ID来区分后台进程和前台进程。如果进程组ID等于进程ID,那么该进程是前台进程,并且会在接收到SIGINT信号时终止。否则它不会终止(除非被捕获)。

您可以使用以下命令查看进程组ID

ps x -o  "%p %r %y %x %c "

因此,当您从脚本内部运行后台进程(使用&)时,它将忽略SIGINT信号,除非它被捕获。但是,您仍然可以使用其他信号(例如SIGKILLSIGTERM等)来杀死子进程。
例如,如果您将脚本更改为以下内容,则可以成功杀死子进程:
#!/bin/bash

if [ "$1" = "--child" ]; then
  sleep 1000
elif [ "$1" = "--parent" ]; then
  "$0" --child &
  for child in $(jobs -p); do
    echo kill "$child" && kill "$child"
  done
  wait $(jobs -p)

  else
  echo "Must be invoked with --child or --parent."
fi

输出:

$ ./test.sh --parent
kill 2187
./test.sh: line 10:  2187 Terminated              "$0" --child

7
somecommand &

返回子进程的pid,存储在$!中。

somecommand &
pid[0]=$!
anothercommand &
pid[1]=$!
trap "kill ${pid[0]} ${pid[1]}; exit 1" INT
wait

我建议从这个模型开始而不是使用bash的作业控制(bg、fg、jobs)。通常init会继承和清除孤儿进程。你想要解决什么问题?


我意识到我可以修改子脚本以将中断传递给它生成的进程。我只是想知道是否有可能在不修改子脚本的情况下向后代发送中断。 - yonran
1
FYI:第二行应该是$!而不是$1。 - Tully
INT 在我的 OS X 上无法工作,我使用了 EXIT。 - Michal
2
这不应该是 trap "kill ...." INT 而不是 trap INT "kill ...." 吗? - Flimm

3
您可以使用一个简单的小技巧与后台任务一起继续使用SIGINT:将您的异步子进程调用放在一个函数或{}中,并给它设置setsid,以便它拥有自己的进程组。
这是您的脚本,保持其最初的意图:
  • 使用和传播SIGINT而不使用其他信号

  • 仅修改来自:"$0" --child &{ setsid "$0" --child; } &

  • 添加必要的代码以获取您的子实例的PID,它是后台子shell中唯一的进程。

这是您的代码:
#!/bin/bash

if [ "$1" = "--child" ]; then
sleep 1000

elif [ "$1" = "--parent" ]; then
{ setsid "$0" --child; } &
subshell_pid=$!
pids=$(ps -ax -o ppid,pid --no-headers |
    sed -r 's/^ +//g;s/ +/ /g' |
    grep "^$subshell_pid " | cut -f 2 -d " ");
for child in $pids;  do
  echo kill -SIGINT "-$child" && kill -SIGINT "-$child"
done
wait $subshell_pid

else
echo "Must be invoked with --child or --parent."

以下是bash手册中与进程组id对后台进程的影响相关的重要部分(文档中的作业控制部分):

[...] 进程组ID等于当前终端进程组ID的进程,例如 SIGINT 由键盘产生会收到信号。这些进程称为前台进程。与终端不同进程组ID的进程称为后台进程。此类进程无法接受键盘产生的信号。

关于信号(Signals)的内容:

Bash运行的非内置命令的信号处理程序按shell从其父进程继承的值设置。当作业控制不起作用时,异步命令忽略SIGINT和SIGQUIT以及继承的这些处理程序。

关于陷阱(trap)内置的修改:

进入shell时忽略的信号无法被捕获或重置


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接