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 withexec
. You lose the
ability to execute stuff afterwards
though, becauseexec
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:
- the subprocess is registered in the job table (i.e. has not been
disown
-ed) - the shell was NOT started in interactive mode
- 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
Sort a File Based on a Column in Another File
How to Flush Stdout of a Running Process
Detecting If The Monitor Is Powered Off
How to Prevent Git from Committing Two Files with Names Differing Only in Case
How to Run Sh File from Another Sh File
Why Strace Shows Eagain (Resource Temporarily Unavailable)
How to Install G++ on Centos Without Root
Kill Bash Script Foreground Children When a Signal Comes
Generate Disk Usage Graphs/Charts with Cli Only Tools in Linux
What Are Coding Conventions for Using Floating-Point in Linux Device Drivers
Using Pthread Mutex Shared Between Processes Correctly
Linux Directory Starting with Dot
Prevent R from Using Virtual Memory on Unix/Linux
Python3 No Such File or Directory
How to Set Environment Variable Within Gdb Using Shell Command
Git Clone Using Ssh Failed in Windows Due to Permission Issue