Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

bash shutdown hook; or, kill all background processes when main process is killed

Tags:

bash

process

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...

like image 285
fabien Avatar asked Aug 16 '13 18:08

fabien


People also ask

How do you stop background processes in Linux?

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.

What does $! Mean in Linux?

$! 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.)

What kill 0 does?

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.

How do you kill a process in a script?

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 .


2 Answers

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
like image 53
phs Avatar answered Oct 27 '22 11:10

phs


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/

like image 24
konsolebox Avatar answered Oct 27 '22 10:10

konsolebox