Control which process gets cancelled by Ctrl+C

After many, many hours of searching the face of the Internet, I have found the answer.

  1. Linux has the notion of a process group.

  2. The TTY driver has a notion of Foreground Process Group.

  3. When you press Ctrl+C, the TTY sends SIGINT to every process in the Foreground Process Group. (See also this blog entry.)

This is why both the compiled binary and the script that launches it both get clobbered. In fact I only want the main application to receive this signal, not the startup scripts.

The solution is now obvious: We need to put the application in a new process group, and make it the Foreground Process Group for this TTY. Apparently the command to do that is

setsid -c <applcation>

And that is all. Now when the user presses Ctrl+C, SIGINT will be sent to the application (and any children it may have) and nobody else. Which is what I wanted.

  • setsid by itself puts the application in a new process group (indeed, an entire new "session", which is apparently a group of process groups).

  • Adding the -c flag makes this new process group become the "foreground" process group for the current TTY. (I.e., it gets SIGINT when you press Ctrl+C)

I've seen a lot of conflicting information about when Bash does or does not run processes in a new process group. (In particular, it appears to be different for "interactive" and "non-interactive" shells.) I've seen suggestions that you can maybe get this to work with clever pipe trickery... I don't know. But the approach above seems to work for me.


In your initiating bash script.

  • keep track of the PID of the second program

  • catch the SIGINT

  • when you have caught a SIGINT, send a SIGINT to the second program PID


As I mentioned in the comment to f01, you should be sending SIGTERM to the child process. Here are a couple of scripts that show how to trap ^C and send a signal to a child process.

First, the parent.

traptest

#!/bin/bash

# trap test
# Written by PM 2Ring 2014.10.23

myname=$(basename "$0")
child=sleeploop

set_trap()
{
    sig=$1
    msg="echo -e \"\n$myname received ^C, sending $sig to $child, $pid\""
    trap "$msg; kill -s $sig $pid" SIGINT
}
trap "echo \"bye from $myname\"" EXIT

echo "running $child..."
./$child 5  &
pid=$!

# set_trap SIGINT
set_trap SIGTERM
echo "$child pid = $pid"

wait $pid
echo "$myname finished waiting"

And now, the child.

sleeploop

#!/bin/bash

# child script for traptest
# Written by PM 2Ring 2014.10.23

myname=$(basename "$0")
delay="$1"

set_trap()
{
    sig=$1
    trap "echo -e '\n$myname received $sig signal';exit 0" $sig
}

trap "echo \"bye from $myname\"" EXIT
set_trap SIGTERM
set_trap SIGINT

#Select sleep mode
if false
then
    echo "Using foreground sleep"
    Sleep()
    {
        sleep $delay
    }
else
    echo "Using background sleep"
    Sleep()
    {
        sleep "$delay" &
        wait $!
    }
fi

#Time to snooze :)
for ((i=0; i<5; i++));
do
    echo "$i: sleeping for $delay"
    Sleep
done

echo "$myname terminated normally"

If traptest sends SIGTERM things behave nicely, but if traptest sends SIGINT then sleeploop never sees it.

If sleeploop traps SIGTERM, and sleep mode is foreground, then it can't respond to the signal until it wakes up from the current sleep. But if sleep mode is background, it will respond immediately.

Tags:

Bash

Signals