You could inspect what the (non-blocking) waitpid returns. If it's the $pid, the process could be reaped successfully, which means it was no longer running.
In your case here, you're calling the waitpid just a tad too early, so the preceding kill TERM hasn't been delivered/succeeded yet. And as the waitpid $pid => WNOHANG couldn't yet reap at that moment, the process still exists as a zombie at the time you call kill 0 (like in your original case).
Compare:
Child reacts to SIGTERM:
#!/usr/bin/perl -wl
use strict;
use POSIX;
my $pid = fork;
if ( $pid ) {
kill TERM => $pid;
# wait for kill TERM to take effect
select undef, undef, undef, 0.01;
my $reaped = waitpid $pid => WNOHANG;
if ($reaped == $pid) {
print "already gone."; # <---
} else {
print "trying harder...";
kill 9 => $pid;
}
} else {
sleep 3;
}
Child ignores SIGTERM:
#!/usr/bin/perl -wl
use strict;
use POSIX;
my $pid = fork;
if ( $pid ) {
# give child some time to set up its $SIG{TERM} handler
select undef, undef, undef, 0.01;
kill TERM => $pid;
select undef, undef, undef, 0.01;
my $reaped = waitpid $pid => WNOHANG;
if ($reaped == $pid) {
print "already gone.";
} else {
print "trying harder..."; # <---
kill 9 => $pid;
}
} else {
$SIG{TERM} = 'IGNORE';
sleep 3;
}
|