kill Command in Linux: Complete Guide to Terminating Processes, Signals, and Parameters

kill command in Linux and it perimeters

I’ve been managing Linux servers for long enough to know that sooner or later, every admin ends up staring at a runaway process eating 100% CPU, or an application that’s frozen and won’t respond to anything. That’s when kill comes in. It’s one of the first commands I teach anyone new to Linux system administration, because it looks deceptively simple but hides a surprising amount of depth once you get into signals, process groups, and how the kernel actually handles termination requests.

In this guide I’m going to walk through everything I know about kill — from the basic syntax to the internals of how signals travel from your shell down to the kernel, all the way to real troubleshooting scenarios I’ve run into on production boxes.

What the kill Command Actually Does

A common misconception is that kill “kills” a process directly. It doesn’t. kill is a small userspace utility whose only job is to send a signal to one or more processes using the kill() system call. What happens after that signal arrives depends entirely on the target process — it might terminate immediately, ignore the signal, run custom cleanup code, or even use the signal for something completely unrelated to shutting down (this is how a lot of daemons implement “reload configuration”).

So the real mental model is: kill is a signal-sending tool, and termination is just the most common outcome of the signals people send.

Basic Syntax

kill [options] <PID> [PID2] [PID3] ...
kill -SIGNAL <PID>
kill -s SIGNAL <PID>
kill -SIGNAL_NUMBER <PID>

You can pass one or more process IDs, and you can specify the signal either by name, by number, or leave it out entirely (which defaults to SIGTERM, signal 15).

Here’s a very simple example I ran on my own test box:

$ sleep 100 &
[1] 676
$ kill -SIGTERM 676
$ ps -p 676
  PID TTY          TIME CMD

The process was there, then after the kill -SIGTERM call, ps shows nothing — it’s gone.

Parameters and Options

Here’s a rundown of the flags I actually use:

OptionDescription
-lList all available signal names
-s SIGNALSpecify signal by name
-SIGNALShorthand for -s SIGNAL
-SIGNAL_NUMBERSend a signal by its number, e.g. -9
-aKill all processes, not just those owned by you (legacy behavior flag on some systems)
-pPrint the PID without sending a signal
--timeout <ms> <signal>Send a signal, wait, then send another if the process is still alive (GNU util-linux extension)

Listing signals is something I do all the time when I forget a name:

$ kill -l | head -5
0
HUP
INT
QUIT
ILL

Signal 0 is special — it doesn’t actually send anything but instead checks whether a process exists and whether you have permission to signal it:

$ kill -0 676 && echo "process alive"
process alive

I use this trick constantly inside shell scripts to check if a PID is still running before deciding whether to act on it.

The Signals That Matter

There are more than 60 signals defined on Linux, but in day-to-day admin work, only a handful come up regularly:

  • SIGHUP (1) — Originally meant “the terminal hung up,” but today it’s most commonly used by daemons as a “reload your configuration” signal. Nginx, Apache, and rsyslog all do this.
  • SIGINT (2) — What you send when you press Ctrl+C in a terminal.
  • SIGQUIT (3) — Similar to SIGINT but also generates a core dump.
  • SIGKILL (9) — The nuclear option. This signal cannot be caught, blocked, or ignored, because it’s handled directly by the kernel before it ever reaches the process’s signal handler.
  • SIGTERM (15) — The default and “polite” way to ask a process to terminate. Well-behaved applications catch this, close file handles, flush data, and exit cleanly.
  • SIGSTOP (19) and SIGCONT (18) — Pause and resume a process. Like SIGKILL, SIGSTOP cannot be caught or ignored.

Here’s the difference between SIGTERM and SIGKILL in practice, tested on my machine:

$ sleep 100 &
PID=$!
$ kill -SIGTERM $PID
$ sleep 1
$ ps -p $PID || echo "process terminated"
process terminated
$ sleep 100 &
PID2=$!
$ kill -9 $PID2
$ sleep 1
$ ps -p $PID2 || echo "process killed with SIGKILL"
process killed with SIGKILL

Both worked here because sleep doesn’t install a custom SIGTERM handler, so the default action (terminate) applies. But if I were killing something like a database process that traps SIGTERM to flush its write-ahead log first, SIGKILL would skip all of that entirely — which is exactly why I treat -9 as a last resort, not a first instinct.

How kill Works Internally

Understanding the internals genuinely changed how carefully I use this command. Here’s the flow:

  1. When you run kill -TERM 1234, the shell’s kill builtin (or /bin/kill binary) calls the kill(2) system call with the PID and signal number.
  2. The kernel checks permissions — you generally need to own the target process (same UID) or be root, unless the process has specifically granted permission otherwise.
  3. If permitted, the kernel sets a bit in the target process’s pending signal bitmask (task_struct->pending).
  4. The next time the target process is scheduled to run, or the next time it’s interruptible (e.g., blocked in a system call marked interruptible), the kernel delivers the signal.
  5. Delivery means the kernel checks the process’s signal disposition table: is this signal being ignored, using the default action, or caught by a handler function? It acts accordingly.

This is why SIGKILL and SIGSTOP are unique — the kernel doesn’t even check a disposition table for them. There is no way to register a handler for SIGKILL because the kernel enforces the default action unconditionally.

It’s also worth knowing that kill can target more than a single PID:

  • kill -TERM 0 sends the signal to every process in the caller’s process group.
  • kill -TERM -1 (when run as root) sends the signal to every process the caller has permission to signal.
  • kill -TERM -PGID (a negative PID) sends the signal to an entire process group, which is extremely useful for killing a whole pipeline or job tree at once.
$ sleep 300 | cat &
[1] 1200
$ kill -TERM -1200

That kills both sleep and cat in one shot, because they share a process group ID.

Real-World Use Cases

Killing a hung SSH session’s remote process — I’ve had scripts that leave orphaned processes behind after a connection drops. Finding the PID with ps aux | grep and sending SIGTERM cleans it up.

Gracefully restarting a service that doesn’t have a restart hook — sending SIGHUP to a daemon like rsyslogd triggers it to reopen its log files and reread its config without a full restart:

kill -HUP $(pgrep rsyslogd)

Killing zombie parents to clean up defunct children — sometimes a <defunct> process lingers because its parent hasn’t called wait(). Killing the parent (carefully) can let init/systemd reap the orphaned zombie.

Automation in shell scripts — I regularly write cleanup logic like this:

#!/bin/bash
PIDFILE=/var/run/myapp.pid
if [ -f "$PIDFILE" ]; then
  PID=$(cat "$PIDFILE")
  if kill -0 "$PID" 2>/dev/null; then
    kill -TERM "$PID"
    sleep 5
    kill -0 "$PID" 2>/dev/null && kill -9 "$PID"
  fi
  rm -f "$PIDFILE"
fi

This pattern — try SIGTERM, wait, escalate to SIGKILL only if needed — is the same approach systemd itself uses when stopping units, and it’s the pattern I recommend to anyone writing init scripts or process supervisors.

kill vs killall vs pkill

People often confuse these three, so here’s how I distinguish them:

  • kill operates strictly on numeric PIDs (or process groups).
  • killall operates on process names, killing every process matching that name exactly. Handy but risky on multi-tenant machines where two unrelated things might share a binary name.
  • pkill matches processes using pattern matching against the command line, plus filters like user, terminal, or parent PID — more flexible than killall, closer to pgrep‘s matching engine.
kill 1523           # by PID
killall nginx        # by exact process name
pkill -u www-data nginx  # by name + owning user

I default to kill with explicit PIDs in scripts because it’s unambiguous — there’s no chance of accidentally matching an unrelated process.

Troubleshooting Common Problems

“Operation not permitted” — you’re trying to signal a process you don’t own and you’re not root. Use sudo kill if you have the rights to do so responsibly.

Signal sent but process won’t die — the process might be stuck in an uninterruptible sleep state (D state in ps), usually waiting on I/O (a hung NFS mount, a failing disk). No signal except SIGKILL — and sometimes not even that — will affect a process in D state until the I/O operation completes or times out. I’ve had to fix the underlying storage issue before the process would finally exit.

PID reused after the process already exited — on a busy system, PIDs get recycled quickly. If you cache a PID and act on it later without checking, you risk sending a signal to a completely different, newer process. Always verify with kill -0 or check /proc/<pid>/cmdline before signaling based on a stored PID.

Zombie processes that won’t go away with any signal — a zombie has already terminated; it’s just an entry in the process table waiting for its parent to collect its exit status. You cannot kill a zombie because it’s not really “alive” — you have to deal with the parent process instead.

Performance and Security Implications

From a performance standpoint, kill itself is essentially free — sending a signal is a fast syscall. The real cost is in what happens afterward: a process handling SIGTERM might spend seconds flushing buffers, closing sockets, or writing state to disk. On high-throughput services, I plan for this by giving processes a generous grace period (many container orchestrators default to 30 seconds) before escalating to SIGKILL.

Security-wise, the permission model matters a lot on shared systems. Regular users can only signal processes they own, which prevents one tenant from disrupting another’s services. As root, you can signal anything, which is exactly why the kill -1 (all processes) and kill -9 -1 combinations are so dangerous — they can take down an entire system, including the shell you’re typing in.

Compatibility Across Distributions

The behavior of kill is remarkably consistent across Debian, Ubuntu, RHEL, CentOS, Fedora, Arch, and other major distributions, since it’s specified by POSIX and implemented similarly whether you’re using the util-linux version or your shell’s builtin. The most common difference I run into is that bash, zsh, and dash all ship their own kill builtin, which can behave slightly differently from /bin/kill — particularly around signal listing format. If you need the standalone binary specifically, run command kill or the full path /bin/kill / /usr/bin/kill.

Process States and How They Interact With Signals

Understanding the process state column in ps output makes signal troubleshooting far more precise. The common states are:

  • R (Running) — actively executing or ready to run.
  • S (Sleeping, interruptible) — waiting on something like I/O or a timer, but can wake up early to handle a signal.
  • D (Uninterruptible sleep) — waiting on I/O in a way that ignores nearly everything, including SIGKILL, until the underlying operation completes.
  • T (Stopped) — paused due to SIGSTOP, SIGTSTP, or a debugger attach.
  • Z (Zombie) — already terminated, waiting for its parent to collect the exit status.

I check this column with ps -o pid,stat,cmd -p <PID> whenever a kill seems to have no effect — it almost always turns out the target process is in D state, and the real fix is addressing whatever it’s blocked on (a hung mount, a failing disk, a stuck network filesystem call) rather than trying harder with signals.

Signal Handling From the Application’s Perspective

From inside a program, there are exactly three ways a process can respond to a deliverable signal:

  1. Default action — do nothing custom; let the kernel apply its built-in behavior (terminate, core-dump, stop, continue, or ignore, depending on the signal).
  2. Ignore — explicitly discard the signal via signal()/sigaction(), though this is not possible for SIGKILL or SIGSTOP.
  3. Catch — register a handler function that runs when the signal arrives, letting the application do custom cleanup (flush buffers, close connections, log a shutdown message) before deciding whether to exit.

This is exactly why well-engineered services — PostgreSQL, Nginx, systemd units generally — behave gracefully under SIGTERM: they’ve registered a handler that performs an orderly shutdown sequence rather than relying on the kernel’s abrupt default termination behavior.

Automating Graceful Shutdowns With Timeouts

A pattern I rely on heavily in deployment and maintenance scripts combines kill, kill -0 for polling, and a hard timeout:

#!/bin/bash
graceful_kill() {
  local pid="$1"
  local timeout="${2:-10}"
  kill -TERM "$pid" 2>/dev/null || return 0
  for ((i=0; i<timeout; i++)); do
    kill -0 "$pid" 2>/dev/null || return 0
    sleep 1
  done
  echo "Process $pid did not exit after ${timeout}s, sending SIGKILL"
  kill -9 "$pid" 2>/dev/null
}

graceful_kill 4521 15

I’ve used almost this exact function inside deployment tooling for years — it’s essentially a hand-rolled version of what systemd does internally with TimeoutStopSec, and understanding it helps demystify what a service manager is actually doing when a unit takes a while to stop.

Using kill With Process Substitution and pgrep

Since kill needs numeric PIDs, it’s frequently combined with pgrep to target processes by name safely:

kill -TERM $(pgrep -f "myworker.py")

I prefer pgrep -f here because it matches against the full command line, which avoids accidentally missing a process whose executable name alone wouldn’t be distinctive enough. For anything running multiple instances, I add further filters:

kill -TERM $(pgrep -u appuser -f "myworker.py --queue=default")

This narrows the match to a specific user and a specific invocation, which matters a lot on shared or multi-tenant hosts where several nearly-identical processes might be running side by side.

Summary

kill is a signal-delivery tool, not a magic termination switch, and once that clicks, a lot of confusing behavior starts making sense — why some processes ignore SIGTERM, why SIGKILL can’t be caught, why a stuck process might not die even under -9. Get comfortable with signal names, process groups, and the SIGTERM-then-SIGKILL escalation pattern, and you’ll have everything you need for both interactive troubleshooting and reliable automation scripts.

References

  • GNU Coreutils / util-linux kill(1) manual page: man kill
  • Linux signal(7) manual page: man 7 signal
  • Linux kernel documentation on process management: https://www.kernel.org/doc/html/latest/
  • POSIX specification for kill: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/kill.html
Total
2
Shares

Leave a Reply

Previous Post
bg command in Linux and it perimeters

bg Command in Linux: Complete Guide to Background Job Control and Parameters

Next Post
halt command in Linux and it perimeters

halt Command in Linux: Complete Guide to Stopping the System and Parameters

Related Posts