From bash, I am spawning two processes. These two processes depend on each other. I want both to exit if either one dies. What is the cleanest way to do that? Currently I have the following:

# start process a

# start process b

# kill process b if process a exits
wait $a_pid
echo "a_pid died, killing process b"
kill -9 $b_pid

But this only helps process b exit if process a dies. How to I also make process a exit if process b dies?

  • a third process checking the alive status of the mentioned 2 processes and killing the remaining process if one of them go away for any reason. You can build this function into both programs and have them poll each other and die gracefully if the other one disappears, but this is something your programs will have to do, while doing their actual jobs. Hence my suggestion about a separate process – MelBurslan Feb 2 '16 at 20:29
  • You can use kill -0 $PID to determine if a process is still alive. Knowing this, you can then: while /bin/true; do if ! kill -0 $pid_a; then kill -9 $pid_b; exit; fi; elif ! kill -0 $pid_b; then kill -9 $pid_a; exit; fi; done. That said, you really should not ever have to use kill -9; you should send SIGTERM rather than SIGKILL to allow the process to clean up after itself. – DopeGhoti Feb 2 '16 at 20:46
  • Use SIGCHLD. trap '"$@"' CHLD; job1&set kill "$!" && job2 &set "$@" "$!"; wait – mikeserv Feb 2 '16 at 20:47
  • @mikeserv, that would work with zsh, not bash. – Stéphane Chazelas Feb 2 '16 at 21:07
  • @StéphaneChazelas - seems like it should... Whatd i get wrong? I figured the wait was enough to hold it over to get both... Just cuffed it from a phone in a waiting room though... And if you say so i believe it... I am curious about why though... – mikeserv Feb 2 '16 at 21:09

With zsh:

trap '
  trap - CHLD
  (($#pids)) && kill $pids 2> /dev/null

sleep 2 & pids+=$!
sleep 1 & pids+=$!
sleep 3 & pids+=$!


(here using sleep as test commands).

With bash it would seem the CHLD trap is only run when the m option is on. You don't want to start your jobs under that option though as that would run them in separate process groups. Also note that resetting the handler within the handler doesn't seem to work with bash. So the bash equivalent would be something like:

trap '
  if ! "$gotsigchld"; then
    ((${#pids[@]})) && kill "${pids[@]}" 2> /dev/null

sleep 2 & pids+=("$!")
sleep 1 & pids+=("$!")
sleep 3 & pids+=("$!")

set -m
set +m
  • I was hesitant to use job control after reading stackoverflow.com/questions/690266/…, but this solution of turning job control on and off around the wait is perfect – peskal Feb 2 '16 at 21:46
  • @pesckal - you only need to worry about if your script ought to be backgroundable for the most part. If so, look at at. – mikeserv Feb 2 '16 at 22:34

Of those I tested, and as near as I can tell, three shells do pretty much the right thing with regards to SIGCHLD and wait: yash, dash, and mksh. You see, wait is supposed to be interruptible; when setting up a signal handler you need that handler either to be doing a wait(), a sleep(), or a read() portably (though apparently sleep() might behave strangely if the interruption comes of a previous call to alarm()). Any (not blocked/ignored) signal should stop a wait().

The shell implementations of such things shouldn't differ terribly in my opinion, but... some do. Particularly bash behaves the worst of any of bash, ksh93, dash, mksh, yash, or zsh. zsh and ksh93 almost get the following sequence right, but they fail to preserve the exit status of the first process to exit. It's not terrible - though zsh does also complain about being asked to wait on the most recently exited pid anyway.

Here's what I did:

unset IFS
script=$(cat <<""
        PS4="$0 + "                                           
        trap '  for p                                     ### loop over bgd pids
                do      shift                             ### clear current pid
                        if      kill -0 "$p" 2>/dev/null  ### still running?
                        then    set --  "$@" "$p"         ### then append again
                        else    wait    "$p"              ### else get return
                                exit    "$(kill "$@")$?"  ### kill others; exit
                done'   CHLD                              ### wait til CHLD
        for n   in      $(shuf -i 3-7)                    ### randomize order
        do      (sleep "$n";exit "$n")& set "$@" "$!"     ### sleep 3 exits 3
        done;   set -x; wait                              ### debug, wait


The above should work not only to kill all remaining backgrounded children of a shell as soon as one returns, but also to propagate the first returned child's exit code to that of the parent shell. It should work because wait should return immediately with a backgrounded process's exit status if called for a child process which has not yet been waited upon. And because the SIGCHLD is what terminates the first wait the second wait should mark the first time the first returned child is actually waited. At least, simply put it should be. The more complicated the shell implementation, though, the less reliable such logic proves to be, it would seem.

That is the $script each of the shells ran when I did...

for sh in yash zsh ksh bash mksh dash
do  time  "$sh" +m -c "$script"                           ### no job control

bash is the only shell which does not exit within three seconds. zsh and ksh93 both (in my opinion, incorrectly) exit 0, but otherwise do quit within three seconds. The others exit 3 within 3 seconds. Here are the test results:

yash + wait
yash + shift
yash + wait 19111
yash + kill 19112 19113 19116 19117
yash + exit 3

real    0m3.013s
user    0m0.007s
sys     0m0.000s

zsh + wait
zsh + p=19124
zsh + shift
zsh + kill -0 19124
zsh + set -- 19125 19127 19129 19132 19124
zsh + p=19125
zsh + shift
zsh + kill -0 19125
zsh + wait 19125
zsh:wait:12: pid 19125 is not a child of this shell
zsh + kill 19127 19129 19132 19124
zsh + exit 0

real    0m3.023s
user    0m0.017s
sys     0m0.000s

ksh + wait
ksh + shift
ksh + kill -0 19137
ksh + 2> /dev/null
ksh + set -- 19138 19139 19140 19141 19137
ksh + shift
ksh + kill -0 19138
ksh + 2> /dev/null
ksh + wait 19138
ksh + kill 19139 19140 19141 19137
ksh + exit 0

real    0m3.018s
user    0m0.000s
sys     0m0.010s

bash + wait

real    0m7.018s
user    0m0.007s
sys     0m0.007s

mksh + wait
mksh + shift
mksh + 2>/dev/null 
mksh + kill -0 19157
mksh + set -- 19158 19159 19160 19161 19157
mksh + shift
mksh + 2>/dev/null 
mksh + kill -0 19158
mksh + set -- 19159 19160 19161 19157 19158
mksh + shift
mksh + 2>/dev/null 
mksh + kill -0 19159
mksh + set -- 19160 19161 19157 19158 19159
mksh + shift
mksh + 2>/dev/null 
mksh + kill -0 19160
mksh + set -- 19161 19157 19158 19159 19160
mksh + shift
mksh + 2>/dev/null 
mksh + kill -0 19161
mksh + wait 19161
mksh + kill 19157 19158 19159 19160
mksh + exit 3

real    0m3.022s
user    0m0.003s
sys     0m0.000s

dash + wait
dash + shift
dash + kill -0 19165
dash + set -- 19166 19168 19170 19173 19165
dash + shift
dash + kill -0 19166
dash + wait 19166
dash + kill 19168 19170 19173 19165
dash + exit 3

real    0m3.008s
user    0m0.000s
sys     0m0.000s

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.