16

I have a live CD that boots into Linux and runs a small Bash script. The script searches for and runs a second program (which is usually a compiled C++ binary).

You're supposed to be able to abort the second program by pressing Ctrl+C. What should happen is that the second program halts, and the Bash script continues running cleanup. What actually happens is that both the main application and the Bash script terminate. Which is a problem.

So I used the trap builtin to tell Bash to ignore SIGINT. And now Ctrl+C terminates the C++ application, but Bash continues its run. Great.

Oh yeah... Sometimes the "second application" is another Bash script. And in that case, Ctrl+C now does nothing whatsoever.

Clearly my understanding of how this stuff works is wrong... How do I control which process gets SIGINT when the user presses Ctrl+C? I want to direct this signal to just one specific process.

3 Answers 3

12

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.

3
  • 5
    You almost got it... job control is disabled by default when running a script, but you can enable it with set -m. It's a bit cleaner and simpler than using setsid every time you run a child.
    – psusi
    Nov 3, 2014 at 18:31
  • @psusi Thanks for the tip! I only need to run one child, so it's no big deal. I now know where to look in the Bash manual though... Nov 3, 2014 at 19:11
  • Ironically, I have the reverse problem where I want the parent to catch sigint, but it does not because of "clever pipe trickery." Also progress group -> process group Mar 11, 2015 at 8:06
3

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

3
  • 1
    That may not help. If you trap SIGINT in the parent script then the child scripts will not be able to receive SIGINT, whether you send it from the parent or from another shell. But that's not as bad as it sounds, because you can send SIGTERM to the child, which is apparently the preferred signal to use, anyway.
    – PM 2Ring
    Oct 23, 2014 at 5:46
  • 1
    Why is SIGTERM "preferred"? Nov 3, 2014 at 14:41
  • They are almost similar. SIGINT is the signal sent by the controlling terminal/user eg Ctrl+C. SIGTERM is what you can also send if you want the process to terminate. More thoughts here en.wikipedia.org/wiki/Unix_signal
    – f01
    Nov 4, 2014 at 3:39
2

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.

1
  • Thank you for this great example, it helped me a lot in understanding how I can improve my script :) Nov 13, 2018 at 8:30

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .