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
fi
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
done
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
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 usekill -9
; you should sendSIGTERM
rather thanSIGKILL
to allow the process to clean up after itself.trap '"$@"' CHLD; job1&set kill "$!" && job2 &set "$@" "$!"; wait
zsh
, notbash
.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...