15

Is there any way to invoke a subprocess so that it and all its descendants are sent an interrupt, just as if you Ctrl-C a foreground task? I’m trying to kill a launcher script that invokes a long-running child. I’ve tried kill -SIGINT $child (which doesn’t send the interrupt to its descendants so is a no-op) and kill -SIGINT -$child (which works when invoked interactively but not when running in a script).

Here’s a test script. The long-running script is test.sh --child. When you call test.sh --parent, it invokes test.sh --child & and then tries to kill it. How can I make the parent kill the child successfully?

#!/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

I know that you can modify the long-running child to trap signals, send them to its subprocess, and then wait (from Bash script kill background (grand)children on Ctrl+C), but is there any way without modifying the child script?

Community
  • 1
  • 1
yonran
  • 18,156
  • 8
  • 72
  • 97

4 Answers4

17

For anyone wondering, this is how you launch childs in the background and kill them on ctrl+c:

#!/usr/bin/env bash
command1 &
pid[0]=$!
command2 &
pid[1]=$!
trap "kill ${pid[0]} ${pid[1]}; exit 1" INT
wait
vvo
  • 2,653
  • 23
  • 30
  • 2
    This is useful, although I found that `kill` would only kill the processes specified in the arguments, and not their descendants. – Flimm Dec 28 '16 at 11:04
7
somecommand &

returns a pid of the child in $!

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

I would start with this model rather than with bash job control (bg, fg, jobs). Normally init inherits and reaps orphan processes. What problem are you trying to solve?

tripleee
  • 175,061
  • 34
  • 275
  • 318
jim mcnamara
  • 16,005
  • 2
  • 34
  • 51
  • I realize that I can modify the child script to pass interrupts on to the processes that it spawns. I was just wondering whether it’s possible to send interrupts to descendants without modifying the child script. – yonran Feb 04 '13 at 22:36
  • 1
    FYI: Line 2 should have a $! not $1 – Tully Aug 23 '14 at 01:32
  • INT did not work for me, I used EXIT instead on OS X – Michal Mar 20 '15 at 11:53
7

Read this : How to send a signal SIGINT from script to script ? BASH

Also from 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. 

So bash differentiates background processes from foreground processes by the process group ID. If the process group id is equal to process id, then the process is a foreground process, and will terminate when it receives a SIGINT signal. Otherwise it will not terminate (unless it is trapped).

You can see the process group Id with

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

Thus, when you run a background process (with &) from within a script, it will ignore the SIGINT signal, unless it is trapped.

However, you can still kill the child process with other signals, such as SIGKILL, SIGTERM, etc.

For example, if you change your script to the following it will successfully kill the child process:

#!/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

Output:

$ ./test.sh --parent
kill 2187
./test.sh: line 10:  2187 Terminated              "$0" --child
Community
  • 1
  • 1
user000001
  • 32,226
  • 12
  • 81
  • 108
3

You can keep using SIGINT with background tasks with an easy little twist: Put your asynchronous subprocess call in a function or { }, and give it setsid so it has its own process group.

Here's your script keep it's whole first intention:

  • using and propagating SIGINT and not using another signal

  • modifying only the calling from: "$0" --child & to { setsid "$0" --child; } &

  • adding the code necessary to get the PID of your child instance, which is the only process in the background subshell.

Here's your code:

#!/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."

Here's the important doc part from bash manual

Process group id effect on background process (in Job Control section of doc):

[...] 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-generated signals.

Default handler for SIGINT and SIGQUIT (in Signals section of doc):

Non-builtin commands run by bash have signal handlers set to the values inherited by the shell from its parent. When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers.

and about modification of traps (in trap builtin doc):

Signals ignored upon entry to the shell cannot be trapped or reset.

vaab
  • 9,685
  • 7
  • 55
  • 60