8

If I have the following shell script

sleep 30s

And I hit Ctrl+C when the shell script is running, the sleep dies with it.

If I have the following shell script

sleep 30s &
wait

And I hit Ctrl+C when the shell script is running, the sleep continues on, and now has a parent of 1.

Why is that? Doesn't bash propagate Ctrl+C to all the children?

EDIT: If I have the following script

/usr/bin/Xvfb :18.0 -ac -screen 0 1180x980x24 &
wait

where I am spawning a program, this time Ctrl+C on the main process kills the Xvfb process too.

So how/why is Xvfb different from sleep?

In the case of some processes I see that they get reaped by init, in some cases they die. Why does sleep get reaped by init? Why does Xvfb die?

Kusalananda
  • 333,661

3 Answers3

10

tl;dr; the Xvfb process sets a signal handler for SIGINT and exits when it receives such a signal, but the sleep process doesn't, so it inherits the "ignore" state for SIGINT as it was set by the shell running the script before executing the sleep binary.

When a shell script is run, the job control is turned off, and background processes (the ones started with &) are simply run in the same process group, with SIGINT and SIGQUIT set to SIG_IGN (ignored) and with their stdin redirected from /dev/null.

This is required by the standard:

If job control is disabled (see the description of set -m) when the shell executes an asynchronous list, the commands in the list shall inherit from the shell a signal action of ignored (SIG_IGN) for the SIGINT and SIGQUIT signals.

If the signal disposition is set to SIG_IGN (ignore), that state will be inherited through fork() and execve():

Signals set to the default action (SIG_DFL) in the calling process image shall be set to the default action in the new process image. Except for SIGCHLD, signals set to be ignored (SIG_IGN) by the calling process image shall be set to be ignored by the new process image.

  • 2
    POSIX also forbid shells to allow undoing a SIG_IGN. zsh ignores that (unhelpful) requirement, so in zsh, you can work around it with (trap - INT; sleep 30) &. In other shells, you'd need to use zsh -c 'trap - INT; exec sleep 30' & or use perl or other things that don't have that silly requirement. – Stéphane Chazelas Oct 30 '18 at 17:10
  • 3
    In recent versions of bash, the problem can be worked around by starting the command as a coproc instead of with &: { coproc sleep 30 < /dev/null >&4 4>&-; } 4>&1 which doesn't seem to ignore SIGINT/SIGQUIT... – Stéphane Chazelas Oct 30 '18 at 17:18
5

From the bash man page:

Background processes are those whose process group ID differs from the terminal’s; such processes are immune to keyboard-generated signals

You could handle this in different ways; first, to kill the listed jobs:

#!/bin/bash
trap 'kill $(jobs -p)' INT
sleep 30s &
wait

Alternatively, send a kill to all of the processes in the same process group:

#!/bin/bash
trap 'kill 0' INT
sleep 30s &
wait
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
  • That doesn't always seem to be the case though. For instance, if I have this in my shell script usr/bin/Xvfb :18.0 -ac -screen 0 1180x980x24 &; wait and I hit Ctrl+C on the parent process, it does call Xvfb too. Why is that? By this logic that process should continue to live, no? – Hari Sundararajan Oct 30 '18 at 02:22
  • @HariSundararajan. Are you executing your script remotely through putty or similar? – fpmurphy Oct 30 '18 at 05:47
  • no, I am sitting in front of the linux box, with the kvm attached to it, and running commands on an xterm. – Hari Sundararajan Oct 30 '18 at 06:52
1

Bash does not forward signals like SIGINT or SIGTERM to processes it is currently waiting on.

One common workaround is to do a trap wait wait as shown in the following example:

int_handler()
{
    kill -TERM "${child_pid}" > /dev/null 2>&1
}

trap 'int_handler' INT

echo "Sleeping ... "
sleep 200 &

child_pid=$!
wait ${child_pid} > /dev/null 2>&1
trap - INT
wait ${child_pid} > /dev/null 2>&1
fpmurphy
  • 4,636