← back to latest

Check whether a process is still alive without sending it anything

kill -0 4721 && echo alive || echo gone

anatomy

kill
Send a signal to a process. The name is misleading: kill is the general-purpose signal sender, and SIGTERM is only its default. In bash, zsh, dash, and BusyBox ash it is a shell builtin, which is why it also accepts job specs like `%1`. `/bin/kill` exists too, and behaves the same for this job.
-0
Signal number zero, the null signal. There is no signal 0, so nothing is delivered and the target never notices. The kernel still does all the checking it would do before a real signal: does this PID exist, and are you allowed to signal it. The exit status reports the result of that check and nothing else. The strictly POSIX spelling is `kill -s 0`, but `-0` works in every shell you are likely to meet.
4721
The PID to ask about, usually read from a pidfile or saved from `$!` when you started the process. Leave out the `-0` by accident and this same line sends SIGTERM to 4721, so type the flag before the number every time.
&& echo alive || echo gone
Turns the exit status into a word. kill exits 0 if the check passed and 1 if it failed, and prints the reason on stderr. It is not a true if/else: `||` runs if anything before it fails, which is safe here because `echo alive` does not realistically fail.

Sample output

$ kill -0 4721 && echo alive || echo gone
alive
$ kill -0 4721 && echo alive || echo gone
bash: kill: (4721) - No such process
gone

The first run says the backup job on PID 4721 is still going. By the second run it has finished, and the error line tells you why the check failed: no such process. That reason matters, because there is a second way to fail that means the opposite.

When you would reach for it

A deploy script needs to know whether the daemon named in /run/app.pid is really running or whether the pidfile is stale from a crash. A long backup is running in another terminal and you want a loop that waits for it before starting the next step. wait only works on children of the current shell, and pgrep matches by name, which is the wrong question when you already know the exact PID you care about.

Gotchas

  • Failing the check does not always mean the process is gone. Ask about a process owned by another user, without root, and the kernel refuses with EPERM: bash prints kill: (1) - Operation not permitted and exits 1, the same status as a dead PID. So echo gone is wrong for every root-owned daemon. Read the stderr message, or run the check as the process owner, before trusting a failure.
  • A zombie passes the check. A process that has exited but has not been reaped by its parent still holds its PID, so kill -0 succeeds on it. ps -o stat= -p 4721 prints Z for that case. Children of your interactive shell get reaped almost immediately, but a buggy parent can leave zombies sitting for days.
  • PIDs get reused. On a busy box, or after a reboot, the number in a stale pidfile can belong to a completely different process, and kill -0 reports it alive without complaint. The check confirms that some process owns the PID, not that it is the one you meant. Pair it with ps -o comm= -p when the answer matters.
  • The error text differs between shells. bash prints kill: (4721) - No such process, dash says kill: No such process, and zsh, the default on macOS, words it in its own way. Scripts should branch on the exit status, not by grepping the message, except to tell EPERM from a dead PID, where you have no other option short of root.

Variants

$ while kill -0 4721; do sleep 5; done; echo "4721 has exited"

Wait for a process your shell did not start. The loop ends on the first failed check, which prints one No such process line. If the first line says Operation not permitted instead, the process is not yours and the loop never waited at all

$ ps -o pid=,comm= -p "$(cat /run/app.pid)"

Check a pidfile and confirm the name at the same time. A stale pidfile whose PID was recycled shows up here as the wrong command name, which kill -0 cannot catch. The = after each column suppresses the header row

$ tail --pid=4721 -f /dev/null && echo "4721 has exited"

GNU tail as a blocking wait on any PID, no loop required. Linux only; on macOS this needs gtail from brew install coreutils

lineage

The null signal comes from the kill(2) system call, not the command. POSIX spells it out in the system call's description: if the signal is 0, error checking is performed but no signal is actually sent, and the null signal can be used to check the validity of a PID. The kill command inherited the trick because it is a thin wrapper around that call. The name itself is a leftover from early Unix, where nearly every signal a user could send ended the process, so the command that sent them was named for the usual outcome. By the time signals were being used to ask daemons to reload their config or rotate their logs, the name was too entrenched to change.