I have a bash that runs endless commands as background processes:
#!/bin/bash
function xyz() {
# some awk command
}
endlesscommand "param 1" | xyz & # async
pids=$!
endlesscommand "param 2" | xyz & # async
pids="$pids "$!
endlesscommand "param 3" | xyz # sync so the script doesn't leave
The only way to stop this script is (must be) Ctrl-C or kill and when that happens, I need to kill all the background processes listed in the $pids variable.
How do I do that?
If it was possible to catch the kill signal on the main process and execute a function when that happens (shutdown hook), I would do something like:
for $pid in $pids; do kill $pid; done;
But I can't find how to do this...
The basic command used to kill a process in Linux is kill. This command works in conjunction with the ID of the process – or PID – we want to end. Besides the PID, we can also end processes using other identifiers, as we'll see further down.
$! is the process ID of the last job run in the background. $$ is the process ID of the script itself. (Both of the above are links to the Advanced Bash Scripting Guide on TDLP.)
Killing 0 isn't killing the pid 0. Instead it is an option in kill to kill all processes in the current group. With your command you are killing everything in the process group ID (GID) of the shell that issued the kill command.
Kill a process with Taskkill To stop a process by its ID, use taskkill /F /PID <PID> , such as taskkill /F /ID 312 7 if 3127 is the PID of the process that you want to kill. To stop a process by its name, use taskkill /IM <process-name> /F , for example taskkill /ID mspaint.exe /F .
Here's a trap that doesn't need you to track pids
:
trap 'jobs -p | xargs kill' EXIT
EDIT: @Barmar asked if this works within non-sourced scripts, where job control isn't usually available. It does. Consider this script:
$ cat no-job-control
#! /bin/bash
set -e -o pipefail
# Prove job control is off
if suspend
then
echo suspended
else
echo suspension failed, job control must be off
fi
echo
# Set up the trap
trap 'jobs -p | xargs kill' EXIT
# Make some work
(echo '=> Starting 0'; sleep 5; echo '=> Finishing 0') &
(echo '=> Starting 1'; sleep 5; echo '=> Finishing 1') &
(echo '=> Starting 2'; sleep 5; echo '=> Finishing 2') &
echo "What's in jobs -p?"
echo
jobs -p
echo
echo "Ok, exiting now"
echo
When run we see the pids of the three group leaders, and then see them killed:
$ ./no-job-control
./no-job-control: line 6: suspend: cannot suspend: no job control
suspension failed, job control must be off
=> Starting 0
What's in jobs -p?
=> Starting 1
54098
54099
54100
Ok, exiting now
=> Starting 2
./no-job-control: line 31: 54098 Terminated: 15 ( echo '=> Starting 0'; sleep 5; echo '=> Finishing 0' )
./no-job-control: line 31: 54099 Terminated: 15 ( echo '=> Starting 1'; sleep 5; echo '=> Finishing 1' )
./no-job-control: line 31: 54100 Terminated: 15 ( echo '=> Starting 2'; sleep 5; echo '=> Finishing 2' )
If we instead comment out the trap
line and re-run, the three jobs do not die and in fact print out their final messages a few seconds later. Notice the returned prompt interleaved with the final outputs.
$ ./no-job-control
./no-job-control: line 6: suspend: cannot suspend: no job control
suspension failed, job control must be off
=> Starting 0
What's in jobs -p?
54110
54111
54112
=> Starting 1
Ok, exiting now
=> Starting 2
$ => Finishing 0
=> Finishing 2
=> Finishing 1
You can make use of pgrep and a function to kill all processes created under the main process like this. This would not only kill the direct child processes but also those created under it.
#!/bin/bash
function killchildren {
local LIST=() IFS=$'\n' A
read -a LIST -d '' < <(exec pgrep -P "$1")
local A SIGNAL="${2:-SIGTERM}"
for A in "${LIST[@]}"; do
killchildren_ "$A" "$SIGNAL"
done
}
function killchildren_ {
local LIST=()
read -a LIST -d '' < <(exec pgrep -P "$1")
kill -s "$2" "$1"
if [[ ${#LIST[@]} -gt 0 ]]; then
local A
for A in "${LIST[@]}"; do
killchildren_ "$A" "$2"
done
fi
}
trap 'killchildren "$BASHPID"' EXIT
endlesscommand "param 1" &
endlesscommand "param 2" &
endlesscommand "param 3" &
while pgrep -P "$BASHPID" >/dev/null; do
wait
done
As for your original code, it would be better to just use arrays, and you also don't need to use a for loop:
#!/bin/bash
trap 'kill "${pids[@]}"' EXIT
pids=()
endlesscommand "param 1" & # async
pids+=("$!")
endlesscommand "param 2" & # async
pids+=("$!")
endlesscommand "param 3" & # syncing this is not a good idea since if the main process would end along with it if it ends earlier.
pids+=("$!")
while pgrep -P "$BASHPID" >/dev/null; do
wait
done
Original function reference: http://www.linuxquestions.org/questions/blog/konsolebox-210384/bash-functions-to-list-and-kill-or-send-signals-to-process-trees-34624/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With