perl.gg / hidden-gems

<!-- category: hidden-gems -->

kill 0 for Process Existence Checks

2026-04-26

Is process 4827 alive? Don't parse ps output. Don't check /proc. Don't shell out. Perl has a one-liner for this.
if (kill 0, $pid) { say "Process $pid exists"; }
Signal zero. The null signal. It doesn't actually signal anything. No process gets interrupted. No handler fires. The kernel just checks whether the target process exists and whether you have permission to signal it. Then it tells you the answer.

One function call. One return value. Done.

Part 1: WHAT SIGNAL 0 MEANS

Every Unix system defines signal 0 as the null signal. It's specified in POSIX. The kill system call with signal 0 performs all the normal error checking (does the process exist? do you have permission to signal it?) but does not actually deliver a signal.
my $result = kill 0, $pid; # $result == 1 -> process exists and you can signal it # $result == 0 -> process doesn't exist OR you can't signal it
It's a probe. A ping. A "hey, are you there?" that doesn't disturb anything. The process never knows you asked.

The kill function in Perl returns the number of processes successfully signaled. For a single PID with signal 0, that's either 1 (exists and you have permission) or 0 (doesn't exist or permission denied).

Part 2: RETURN VALUE SEMANTICS

The return value of kill 0, $pid is straightforward:
my $pid = 12345; if (kill 0, $pid) { say "PID $pid exists and we can signal it"; } else { say "PID $pid either doesn't exist or we can't signal it"; }
But there's a nuance. A return of 0 can mean two different things:
  1. The process does not exist (it died or was never created)
  2. The process exists but you don't have permission to signal it

To tell them apart, check $! after a failure:

use Errno qw(ESRCH EPERM); if (!kill 0, $pid) { if ($! == ESRCH) { say "No such process: $pid"; } elsif ($! == EPERM) { say "Process $pid exists but we can't signal it (permission denied)"; } else { say "Unexpected error checking PID $pid: $!"; } }
ESRCH means "no such process." The PID doesn't correspond to anything running. EPERM means "operation not permitted." The process is alive, but it belongs to another user and you're not root.

This distinction matters. A monitoring script running as a regular user checking a root-owned daemon will get EPERM when the daemon is running and ESRCH when it's dead. Both return 0 from kill, but they mean opposite things.

Part 3: PRACTICAL PID FILE VALIDATION

Most Unix daemons write their PID to a file. The standard check is to read the PID file and verify the process is still alive:
sub is_daemon_running { my ($pid_file) = @_; open my $fh, '<', $pid_file or return 0; my $pid = <$fh>; close $fh; chomp $pid; return 0 unless $pid =~ m~^\d+$~; return 0 unless $pid > 0; return kill 0, $pid; } if (is_daemon_running("/var/run/myapp.pid")) { say "Daemon is running"; } else { say "Daemon is stopped (or stale PID file)"; }
This catches stale PID files too. If the daemon crashed without cleaning up, the PID file still exists but the process is gone. kill 0 tells you the truth.

A more thorough version checks that the PID belongs to the right process, not just any process that happened to reuse the PID:

sub validate_pid_file { my ($pid_file, $process_name) = @_; open my $fh, '<', $pid_file or return (0, "no PID file"); my $pid = <$fh>; close $fh; chomp $pid; return (0, "invalid PID in file") unless $pid =~ m~^\d+$~; if (!kill 0, $pid) { unlink $pid_file; # clean up stale PID file return (0, "process $pid not found (stale PID file removed)"); } # verify it's the right process (Linux-specific) if (-r "/proc/$pid/cmdline") { open my $cmd_fh, '<', "/proc/$pid/cmdline" or return (1, "running"); my $cmdline = <$cmd_fh>; close $cmd_fh; if ($cmdline !~ m~\Q$process_name\E~) { return (0, "PID $pid is not $process_name (PID reuse)"); } } return (1, "running as PID $pid"); } my ($ok, $msg) = validate_pid_file("/var/run/nginx.pid", "nginx"); say "$msg";

Part 4: PROCESS MONITORING LOOPS

Build a simple process watchdog with kill 0:
#!/usr/bin/env perl use strict; use warnings; use feature 'say'; use POSIX qw(strftime); my $pid_file = $ARGV[0] or die "Usage: $0 <pid_file>\n"; my $check_interval = 10; # seconds say "Monitoring PID file: $pid_file"; while (1) { if (!-f $pid_file) { say timestamp() . " PID file missing!"; sleep $check_interval; next; } open my $fh, '<', $pid_file or do { warn timestamp() . " Cannot read $pid_file: $!\n"; sleep $check_interval; next; }; my $pid = <$fh>; close $fh; chomp $pid; if (kill 0, $pid) { # process is fine, check again later } else { say timestamp() . " Process $pid is DOWN!"; # could trigger restart, send alert, etc. restart_daemon(); } sleep $check_interval; } sub timestamp { return strftime("%Y-%m-%d %H:%M:%S", localtime); } sub restart_daemon { say timestamp() . " Restarting daemon..."; system("systemctl restart myapp"); }
Simple. No external monitoring tools. No agent to install. Just kill 0 in a loop.

Part 5: MULTIPLE PID CHECKS

kill can check multiple PIDs at once. The return value is the count of processes that exist:
my @pids = (1001, 1002, 1003, 1004); my $alive = kill 0, @pids; say "$alive out of " . scalar(@pids) . " processes are alive";
Use this to check a pool of worker processes:
sub check_workers { my (@pids) = @_; my @alive; my @dead; for my $pid (@pids) { if (kill 0, $pid) { push @alive, $pid; } else { push @dead, $pid; } } return (\@alive, \@dead); } my @workers = (4501, 4502, 4503, 4504); my ($alive, $dead) = check_workers(@workers); say "Alive: " . join(", ", @$alive); say "Dead: " . join(", ", @$dead);

Part 6: COMPARISON TO /proc/$pid ON LINUX

On Linux, you can also check for a process by testing if /proc/$pid exists:
# Linux-specific if (-d "/proc/$pid") { say "Process exists"; }
This works, but it has drawbacks:
METHOD PORTABLE? PERMISSION CHECK? RACE SAFE? kill 0, $pid Yes (POSIX) Yes No* -d "/proc/$pid" Linux only No No* parse `ps` output Sort of No No* * None of them are truly race-safe. The process can die between the check and your next action.
kill 0 wins on portability and permission checking. It works on Linux, macOS, BSD, Solaris, AIX, and every other POSIX system. The /proc approach is Linux-specific (and partially available on some BSDs).

kill 0 also tells you about permissions. If the process exists but you can't signal it, $! is EPERM. The /proc check doesn't distinguish between "exists but not mine" and "exists and is mine."

Part 7: RACE CONDITIONS

Every process existence check has a race condition. The process can die between the check and your next action:
if (kill 0, $pid) { # process existed a microsecond ago # it might be dead by now kill 'TERM', $pid; # might signal the wrong process! }
This is called a TOCTOU bug (Time Of Check, Time Of Use). You checked. Time passed. You used the result. Between check and use, the world changed.

For most monitoring and health-check purposes, this doesn't matter. You're checking periodically. If the process dies between checks, you'll catch it on the next round.

For critical operations like "send SIGTERM to this specific process," the race condition is real but unavoidable. There is no atomic "check and signal" operation in POSIX. The standard approach is to accept the race and handle errors:

if (!kill 'TERM', $pid) { if ($! == ESRCH) { say "Process already gone"; } else { warn "Failed to signal $pid: $!"; } }
Don't check-then-signal. Just signal. Handle the error. The signal itself tells you whether the process was there.

Part 8: REAPING ZOMBIES

There's a gotcha. kill 0 returns true for zombie processes.

A zombie is a process that has exited but hasn't been reaped by its parent. It still has an entry in the process table. The kernel keeps it around so the parent can call waitpid and collect the exit status.

my $pid = fork(); if ($pid == 0) { # child: exit immediately exit 0; } # parent: child is now a zombie sleep 1; if (kill 0, $pid) { say "PID $pid exists"; # true! it's a zombie } # reap the zombie waitpid($pid, 0); if (kill 0, $pid) { say "still exists"; # false now } else { say "gone after reap"; # this prints }
If you're managing child processes, always waitpid to reap them. Otherwise kill 0 will report them as alive when they're actually zombies. They're dead. They just haven't been buried yet.

For processes you didn't fork (like checking a daemon's PID file), zombies are someone else's problem. The daemon's parent should be reaping its children.

Part 9: BUILDING A SIMPLE LOCKFILE

Combine PID files and kill 0 for a simple process lockfile:
use Fcntl qw(:flock); sub acquire_lock { my ($lock_file) = @_; # check for existing lock if (-f $lock_file) { open my $fh, '<', $lock_file or return 1; # can't read, assume unlocked my $old_pid = <$fh>; close $fh; chomp $old_pid; if ($old_pid =~ m~^\d+$~ && kill 0, $old_pid) { die "Already running as PID $old_pid\n"; } # stale lock file, remove it unlink $lock_file; } # write our PID open my $fh, '>', $lock_file or die "Cannot create lock: $!\n"; print $fh "$$\n"; close $fh; return 1; } sub release_lock { my ($lock_file) = @_; unlink $lock_file; } # usage acquire_lock("/tmp/myapp.lock"); # do work... say "Working as PID $$"; release_lock("/tmp/myapp.lock");
The kill 0 check prevents a stale lock from blocking your script forever. If the previous instance crashed without cleaning up, the lock file exists but the process is gone. kill 0 detects this and lets you reclaim the lock.

Part 10: THE TAKEAWAY

kill 0, $pid is the right way to check if a process exists in Perl. It's POSIX standard. It's portable across every Unix. It returns a clean boolean. It distinguishes between "not found" and "permission denied" through $!.
+---------------------------------+ | kill 0, $pid | | | | Returns 1: process exists, | | you can signal it | | | | Returns 0: check $! | | ESRCH -> no such process | | EPERM -> exists, not yours | +---------------------------------+ "The quietest kill is no kill at all. Signal zero asks the question without pulling the trigger." .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
Don't parse ps. Don't ls /proc. Don't shell out to pgrep. Just kill 0. The kernel already knows the answer. Ask it directly.

perl.gg