Kill Bash Script Foreground Children When a Signal Comes

Kill bash script foreground children when a signal comes

There are some options that come to my mind:

  • When a process is launched from a shell script, both belong to the same process group. Killing the parent process leaves the children alive, so the whole process group should be killed. This can be achieved by passing the negated PGID (Process Group ID) to kill, which is the same as the parent's PID. ej: kill -TERM -$PARENT_PID

  • Do not execute the binary as
    a child, but replacing the script
    process with exec. You lose the
    ability to execute stuff afterwards
    though, because exec completely
    replaces the parent process.

  • Do not kill the shell script process, but the FastCGI binary. Then, in the script, examine the return code and act accordingly. e.g: ./fastcgi_bin || exit -1

Depending on how mod_fastcgi handles worker processes, only the second option might be viable.

How is bash able to kill children processes with CTRL+C

Bash doesn't need any permissions because bash isn't doing anything. When you hit ^C, SIGINT is sent to all processes in the foreground process group by the tty driver. The signal comes from the system, not from another process, so the permission checks relevant to one process sending a signal to another don't apply.

How can I catch SIGINT and have it only kill foreground processes in C?

I think your core question is how to interpose on the standard signal semantics of a process group.
By default all the processes that you fork() remain within the process group you belong to. A system call, setpgid(), can create a new process group, divorcing group signal semantics for the new processes.

Certain signals are delivered to process groups. Notifications from the tty driver are broadcast to the process group which the session leader of the tty currently belongs to. Simple, right :-?

So, what you want to do is use setpgid() in the child processes you start to create a new process group. Any processes they subsequently start will inherit their new process group; so they have no way back to the original, root, process group [ I think, the ground is uneven here ].

I've written a sample program which creates a little set of processes which just hang around sleeping until they are sent a SIG_TERM signal. Each of these processes are placed in their own process group ( waste of resources, but much smaller code ). When you hit Control-C on this program running, it announces its SIG_INT, then delivers a SIG_TERM to all the processes it started. When you hit Control-D, the main process exits.

You can tell this program works if you input Control-C, Control-D, you should find with ps/pgrep that no residual children exist, and the output screen display the correctly configured (NPROC) number of processes receiving a SIG_TERM (15).

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#ifndef NPROC
#define NPROC 7
#endif
static int children[NPROC];
static int nproc;

static void SigPrint(char *act, int signo) {
char buf[100];
int n;
n = sprintf(buf, "[%d]:(%d) -> %s\n", getpid(), signo, act);
write(1, buf, n);
}

static void SigDie(int signo) {
SigPrint("Die", signo);
_exit(1);
}

static void SigDisp(int signo) {
SigPrint("Dispatch", signo);
int i;
for (i = 0; i < nproc; i++) {
if (kill(children[i], SIGTERM) < 0) {
perror("kill");
}
}
}

int main(void) {
signal(SIGINT, SigDisp);
signal(SIGTERM, SigDie);
for (nproc = 0; nproc < NPROC; nproc++) {
children[nproc] = fork();
if (children[nproc] == 0) {
signal(SIGINT, SIG_DFL);
if (setpgid(0, 0) < 0) {
perror("setpgid");
_exit(1);
}
while (sleep(5)) {
}
_exit(0);
} else if (children[nproc] < 0) {
perror("fork");
kill(getpid(), SIGINT);
perror("kill");
_exit(1); /*Just in case... */
}
}
int c;
while ((c = getchar()) != EOF) {
putchar(c);
}
return 0;
}

When you run this, you should get output something like:

^C[15141]:(2) -> Dispatch
[15142]:(15) -> Die
[15143]:(15) -> Die
[15146]:(15) -> Die
[15144]:(15) -> Die
[15145]:(15) -> Die
[15147]:(15) -> Die
[15148]:(15) -> Die

with different values for the [pid] field.

This program shows how to divorce child processes from the notifications of the parents. It could be much better, the model here is quite rich.

C shell, signal caught by parent still goes to child process.

Found an answer to my own question. I had already tried changing the group id of the background child process using setpid(0,0); and this worked, but created a different problem. After that call, I was no longer catching SIGCHLD signals from the child in the parent. This is because once the process group of the child is changed, it is essentially no longer connected to the parent for signalling purposes. This solved the problem of the background processes catching Cntrl-C (SIGINT) signals from the parent (undesired behavior), but prevented the background process from signalling the parent when complete. Solved one problem only to create another.

Instead, the solution was to detect whether a child was about to be created as a foreground or a background process, and if background, tell it to ignore the SIGINT signal: signal(SIGINT, SIG_IGN);

How to only kill the child process in the foreground?

This is rather simple, but it isn't done with signals. Instead you must use a feature called process groups. Each single job (executable or pipeline or so) will be a separate process group. You can create process groups with setpgid (or on some systems with setpgrp). You can simply set the process group of the child process after fork but before exec, and then store the process group id of this job into the job table.

Now, the process group that is in the foreground is set as the active process group for the terminal (the /dev/tty of the shell) with tcsetpgrp - this is the process group that will receive CTRL+C. Those process groups that belong to the same session, but not to the group set to foreground with tcsetpgrp will be completely oblivious to CTRL+C.

Why is the KILL signal handler not executing when my child process dies

I couldn't find anything in the bash documentation that would explain the observed behavior, so I turned to the source code. Debugging lead to the function notify_of_job_status(). The line that prints the message about a killed subprocess can be reached only if all of the following conditions hold:

  1. the subprocess is registered in the job table (i.e. has not been disown-ed)
  2. the shell was NOT started in interactive mode
  3. the signal that terminated the child process is NOT trapped in the parent shell (see the signal_is_trapped (termsig) == 0 check)

Demonstration:

$ cat test.sh 
echo Starting a subprocess
LC_ALL=C sleep 100 &
Active_pid=$!
case "$1" in
disown) disown ;;
trapsigkill) trap "echo Signal SIGKILL caught" 9 ;;
esac
sleep 1
kill -9 $Active_pid
sleep 1
echo End of script

$ # Demonstrate the undesired message
$ bash test.sh
Starting a subprocess
test.sh: line 14: 15269 Killed LC_ALL=C sleep 100
End of script

$ # Suppress the undesired message by disowning the child process
$ bash test.sh disown
Starting a subprocess
End of script

$ # Suppress the undesired message by trapping SIGKILL in the parent shell
$ bash test.sh trapsigkill
Starting a subprocess
End of script

$ # Suppress the undesired message by using an interactive shell
$ bash -i test.sh
Starting a subprocess
End of script

How this removes the trace of the first test without executing echo Signal SIGKILL ?

The trap is not executed since the KILL signal is received by the sub-process rather than the shell process for which the trap has been set. The effect of the trap on the diagnostics is in the (somewhat arguable) logic in the notify_of_job_status() function.

How do I kill background processes / jobs when my shell script exits?

To clean up some mess, trap can be used. It can provide a list of stuff executed when a specific signal arrives:

trap "echo hello" SIGINT

but can also be used to execute something if the shell exits:

trap "killall background" EXIT

It's a builtin, so help trap will give you information (works with bash). If you only want to kill background jobs, you can do

trap 'kill $(jobs -p)' EXIT

Watch out to use single ', to prevent the shell from substituting the $() immediately.

Bash: spawn child processes that quit when parent script quits

Use wait to have the parent process wait for all the children to exit.

#!/usr/bin/bash

spawnedChildProcess1 &
spawnedChildProcess2 &
spawnedChildProcess3 &

wait

Keyboard signals are sent to the entire process group, so typing Ctl-c will kill the children and the parent.

How can bash script do the equivalent of Ctrl-C to a background task?

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

How to use trap reliably using Bash running foreground child processes

bash must wait for sleep to complete before it can execute the handler. A good workaround is to run sleep in the background, then immediately wait for it. While sleep is uninterruptible, wait is not.

trap 'kill $sleep_pid; echo "TRAP CAUGHT"; exit 1' QUIT

echo starting sleep
sleep 11666 &
sleep_pid=$!
wait
echo ending sleep

echo done

The recording of sleep_pid and using it to kill sleep from the handler are optional.



Related Topics



Leave a reply



Submit