Intro

Unix signals are one of the oldest forms of inter-process communication. Born in early UNIX at Bell Labs (1970s), they were designed as a simple way for the kernel to tap a process on the shoulder. Unlike pipes or sockets, signals don’t carry payloads — just a notification.

They remain the universal way the OS tells programs when it’s time to stop, reload, or respond to user actions. Servers, daemons, shells, even containers all depend on them. Think of signals as lifecycle control, while other IPC mechanisms handle the heavy lifting.

Fun Fact: The very first “signals” were just conventions that most but not all programs hard-wired for job control (Ctrl+C, Ctrl+Z). Everything else—portability, universality, reloads, graceful quits—are clever conventions layered on later.

closeup of 1970s illuminated buttons on a computer station
Ctrl-C, Ctrl-C... why isn't it responding?
Still from WarGames (1983, MGM). Used under fair use for educational purposes.

Unix IPC signals are easy to use but there are a lot of exceptions and footnotes. I tried to make the following as clear as possible without going full-on manual.

Signals vs other Unix-land IPC

The more they over-think the plumbing the easier it is to stop up the drain. –anonymous fortune

  • Signals are almost all one-bit notifications from the kernel. They don’t carry payloads, just a type (e.g. SIGTERM), and they interrupt a process asynchronously. The exception being POSIX real-time signals like sigqueue which can carry a small int/pointer. Use them for lifecycle events (quit, reload, stop) and simple control.

  • Pipes (man 7 pipe) are byte streams between processes, usually set up with | in the shell. Data flows in one direction, buffered by the kernel (half-duplex). Bidirectional requires two pipes. Think of ls | grep foo — that’s a pipe doing work. Pipes are synchronous: the reader blocks until there’s data. There are also named pipes or FIFOs, made with mkfifo, which persist in the filesystem and let unrelated processes talk. Same half-duplex rule applies.

  • Sockets (man 7 socket) generalize pipes into a full-duplex/bidirectional, network-style interface. They can talk locally (UNIX domain sockets) or across machines (TCP/UDP). Most client-server software is built on sockets.

  • Message Queues & Shared Memory (man 7 mq_overview, man 7 shm_overview) are heavier IPC mechanisms. They allow structured data passing or even memory regions mapped between processes. Used when performance matters or when large state must be shared.

  • DBus (freedesktop.org) is a high-level message bus, running in user space via a broker daemon. Think of it as an application-layer IPC system: processes send structured requests and events through a central hub. Desktop environments and system services (NetworkManager, systemd, GNOME) rely on it. DBus “signals” are logical user-space events, not kernel ones.

Pro-Tip: If you’re just controlling a process, signals are usually enough. If you need to talk to it, look at sockets or DBus.

Usage Cheatsheet

Common signals you’ll run into:

Signal Trigger / Use case Default action Notes
SIGINT Ctrl+C in a terminal Terminate Apps can catch to clean up.
SIGTERM kill <pid> or systemctl stop Terminate The polite way to ask a process to quit.
SIGHUP Terminal hangup; config reloads Terminate Many daemons repurpose it as “reload config.”
SIGQUIT Ctrl+\ Core dump + exit some like nginx use it for graceful quit.
SIGKILL kill -9 <pid> Terminate (uncatchable) No cleanup, can’t be trapped.
SIGTSTP Ctrl+Z Stop (suspend) Background job control; fg to resume.

You can list them all with kill -l and you will get a list like this:

└─$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

Note: the list and numbers shown are from Linux. Signal names are portable, but numbers and availability differ on BSD/macOS/Solaris.

Most of the time you only care about the common signals, and modern tools let you use their names (SIGTERM, SIGHUP, etc.) directly.
The old habit of using numbers (kill -9 or kill -1) still works, but it’s less portable since signal numbers vary across Unix flavors. Stick to names unless you’re typing a quick shortcut.

The main tool for sending signals is the kill command which despite its name, it doesn’t always “kill” a process. By default it throws SIGTERM, but you can specify any signal. Think of it as the userland signal throwing tool that just defaults to kill:

# First, find the PID of the process you want to affect
ps aux | grep nginx

# Then send it a signal
kill -TERM <pid>    # send SIGTERM (default)
kill -HUP <pid>     # send SIGHUP
kill -KILL <pid>    # send SIGKILL (can’t be trapped)
killall -HUP nginx  # send SIGHUP to all nginx processes

Pro-Tip: With kill -9 <pid> you must supply the PID of every process you want to kill. That’s why tools like pkill and killall are so handy.


Beyond kill: other ways to signal processes

Signals aren’t just for kill. The shell and related tools let you affect processes in different ways:

  • Job Control (in the shell)

    • Ctrl+Z → sends SIGTSTP (suspend) via the terminal driver.

    • bg → resumes a job in the background (sends SIGCONT).

    • fg → brings a job back to the foreground.

    • jobs -l → shows current job table with PIDs.

      # Start a long-running process in the foreground
      sleep 600
      
      # Press Ctrl+Z in the terminal
      # Output will look like:
      [1]+  Stopped    sleep 600
      
      # See it in the job table (with PID)
      jobs -l
      [1]+  12345 Stopped    sleep 600
      
      # Resume it in the background
      bg %1
      [1]+  12345 Running    sleep 600 &
      
      # Bring it back to the foreground
      fg %1
      sleep 600
      
    Notes:
    • bg and fg only work on processes started from the current shell.
    • If the process expects input, running it in the background (bg) won’t magically make it non-interactive — it may still block waiting for input.
    • Background jobs still write to the terminal by default. Use output redirection (> file 2>&1) if you don’t want them spamming your shell.
    • If you close the terminal, jobs will get SIGHUP unless you’ve used nohup or disown.
    • In bash/zsh you can reference jobs by number %1, %2, or by name substring: fg %sleep.

  • nohup
    Run a command that keeps going even after you log out of the shell:

    nohup hugo server -D &
    

    This ignores SIGHUP, so the process keeps running if you log out of your shell or disconnect an SSH session. Output (both stdout and stderr) gets redirected to nohup.out by default unless you specify otherwise.

    Notes:
    • Logging out: the process keeps running in the background.
    • Logging back in (or SSH-ing again): the process is still alive, but it won’t show up in your jobs list since that’s tied to the original shell. Use ps aux | grep hugo or pgrep hugo to find it.
    • Reboot: nohup does not survive a reboot. For persistence across restarts, use a service manager like systemd, supervisord, or Docker.
    • Good use case: quick, long-running tasks during a session (like nohup hugo server -D & while you hack on a blog). Not a replacement for proper service management.

  • disown (bash/zsh only)
    Removes a job from the shell’s job table so it won’t receive SIGHUP. The process keeps running, but the shell forgets about it.

    sleep 600 &
    jobs -l
    disown %1
    jobs -l    # now the job no longer shows up
    

    Since it’s a shell builtin, there’s no man disown. Use:

    help disown   # bash
    

    or in zsh:

    man zshbuiltins | less +/disown
    

  • renice
    Adjust scheduling priority (not a signal, but related process control).

    • Lower nice value = higher CPU priority (use for important processes).
    • Higher nice value = lower CPU priority (good for heavy background tasks).
    • Niceness ranges from -20 (highest priority) to 19 (lowest priority). Default is 0
    renice -n 19 -p <pid>   # very "nice" → gets less CPU time
    renice -n 0 -p <pid>    # reset to normal priority
    renice -n -5 -p <pid>   # increase priority (root only)
    

  • killall / pkill
    Send signals by process name or regex pattern, instead of hunting PIDs manually:

    kill -USR1 <pid>       # send a specific signal to a given PID (works on one or a list of PIDs)
    kill -HUP 1234 1235    # send HUP to multiple PIDs 
    killall -TERM vim      # send TERM to all vim processes
    
    pkill -HUP nginx       # send HUP to all processes named nginx
    pkill -HUP -f 'nginx.*worker'   # send SIGHUP to any process whose command matches the regex "nginx.*worker"
    pkill -TERM -f 'jupyter-notebook'   # restart all Jupyter notebook servers (if multiple users)
    

  • systemctl
    Modern services often live under systemd. Commands like systemctl stop, reload, and restart rely on signals under the hood, but with extra policy:

    • stop → sends SIGTERM, then escalates to SIGKILL if the process doesn’t exit within TimeoutStopSec=.
    • reload → runs ExecReload= from the unit file, or if unset, sends the service’s ReloadSignal= (often SIGHUP).
    • restart → does a stop, then start (so SIGTERM → SIGKILL if needed, then new process).

Pro-Tip:
Backgrounding (&, bg) isn’t just convenience — it literally controls signals (SIGSTOP, SIGCONT) at the kernel level.

a closeup of a DEC PDP-11 switch panel
It was a simpler time

Bash Traps

A common pattern is to trap several signals at once, so your script can clean up or log before exiting.

Here’s an example script that spins up a background worker, and makes sure it’s killed cleanly on exit or interrupt:

#!/usr/bin/env bash
# demo-trap.sh

# Start a dummy background job
sleep 600 &
worker_pid=$!
echo "Worker started with PID $worker_pid"

# Define cleanup function
cleanup() {
    echo "Caught signal, cleaning up..."
    kill -TERM "$worker_pid" 2>/dev/null
    wait "$worker_pid" 2>/dev/null
    echo "Worker stopped. Exiting."
    exit 0
}

# Trap multiple signals
trap cleanup INT TERM HUP

# Main loop
while true; do
    echo "Main script running. Press Ctrl+C to stop."
    sleep 10
done
  • INT → user pressed Ctrl+C
  • TERM → process was asked to stop politely (kill <pid> or systemctl stop)
  • HUP → hangup (often used as “reload”)

Pro-Tip: You can trap multiple signals in one line. Here, trap cleanup INT TERM HUP ensures your cleanup runs whether you press Ctrl+C, close the terminal, or stop it from another shell.


Python Signal Handling

Profanity is the one language all programmers know best. –anonymous fortune

Python has the signal module, which lets you trap signals much like Bash.
Typical use case: catch SIGINT (Ctrl+C) or SIGTERM (kill) to shut down cleanly.

Example:

#!/usr/bin/env python3
import signal
import sys
import time

# Define handler function
def handle_signal(signum, frame):
    print(f"Caught signal {signum}, cleaning up...")
    # do cleanup here
    sys.exit(0)

# Register handlers
signal.signal(signal.SIGINT, handle_signal)   # Ctrl+C
signal.signal(signal.SIGTERM, handle_signal)  # kill <pid>

print("Running. Press Ctrl+C or send SIGTERM to stop.")

# Simulate work
while True:
    time.sleep(1)
  • SIGINT → user pressed Ctrl+C
  • SIGTERM → process asked to stop politely (kill <pid>)
Pro-Tip:
  • Not every signal is catchable in Python. For example, SIGKILL and SIGSTOP can never be trapped — the kernel enforces those.
  • Only the main thread can set handlers, and signals are always delivered to that thread.
  • On Windows, only a limited set of signals work (SIGINT, SIGBREAK, and a fake SIGTERM).
  • In async code, you can use loop.add_signal_handler(signal.SIGTERM, callback) to integrate with asyncio.

Overview topics

References

Conclusion

That’s it — a quick and dirty guide to signals in Unix-land.
Signals are one of those Unix fundamentals that everybody bumps into, but they can still trip you up if you don’t know the details.

Next time you press Ctrl+C, background a process, or reload a daemon, remember: you’re working with a mechanism that’s been around since the 1970s and is still quietly running the show today.

📬 Got a favorite signal trick or a war story about a kill -9 gone wrong? Send it my way: feedback@adminjitsu.com