Right, let’s talk about signals. You’ve probably already used them, even if you didn’t realize it. Hitting Ctrl+C in a terminal to stop a runaway program? That’s you sending a signal. They’re the operating system’s slightly clunky, occasionally infuriating, but utterly essential way of poking a process in the ribs to get its attention. Think of them as administrative inter-process tweets: short, limited to a predefined set of messages, and utterly impossible to ignore.

The kernel is the ultimate messenger here. When you (or another process) decide to send signal XYZ to process 1234, you’re not walking up to that process and whispering in its ear. You’re telling the kernel, “Hey, tell process 1234 about XYZ.” The kernel then delivers the note. This is crucial because it means the target process doesn’t get to just say, “I’m busy, go away.” The kernel will interrupt whatever that process is doing to notify it. What the process does with that information, however, is a different story.

The Polite Request: SIGTERM vs. The Brick: SIGKILL

This is the most important distinction to internalize, so let’s get it out of the way first.

SIGTERM (signal 15) is the default, polite request to shut down. It’s the equivalent of your manager saying, “Hey, could you please wrap up what you’re doing? We need to close up the office.” When a process receives a SIGTERM, its signal handler is invoked. A well-behaved application will use this chance to clean up: closing files cleanly, finishing database transactions, telling connected clients it’s leaving, etc. Then it should exit gracefully. This is why you should almost always try kill <pid> (which sends SIGTERM) before escalating to the nuclear option.

SIGKILL (signal 9) is the nuclear option. It’s the kernel’s sledgehammer. The process does not get a signal handler for SIGKILL. The kernel simply steps in, destroys the process, and reclaims all its resources. It’s the manager coming over, pulling the power cord on your computer, and escorting you from the building. No saving, no goodbyes. This is why you use it as a last resort when a process ignores a SIGTERM. It gets the job done, but you might be left with corrupted files or half-finished work. There’s no defense against it; a process cannot ignore or handle SIGKILL. It’s the ultimate authority.

# The polite way. Always try this first.
$ kill 4242

# The process is ignoring you? Time for the brick.
$ kill -9 4242
# or, more explicitly
$ kill -SIGKILL 4242

The Everyday Signals: SIGINT and SIGHUP

You interact with these constantly.

SIGINT (signal 2) is the “Interrupt” signal. This is what your terminal sends to the foreground process when you smash Ctrl+C. Its default action is to terminate the process, but just like with SIGTERM, the process can catch it and decide to do something else (like ask “Are you sure?”). This is why sometimes Ctrl+C doesn’t instantly kill a program—it’s handling the signal.

SIGHUP (signal 1), “Hang Up,” is a fascinating relic that’s found new purpose. Its original meaning was literally “your user’s terminal line has hung up” (think a dial-up modem disconnecting). The default action is to terminate the process. However, its most common modern use is to tell a daemon to “reload your configuration, please.” This is a brilliant hack. Why? Because you can tell nginx or ssh-agent to reload without actually stopping and starting it, achieving zero downtime for configuration changes. It works because these daemons are specifically written to handle SIGHUP and interpret it not as “die,” but as “wake up and re-read your files.”

# Tell nginx to reload its config gracefully (uses SIGHUP)
$ sudo nginx -s reload

# You can also send SIGHUP directly to a daemon's PID if you know it
$ kill -SIGHUP 1234

Handling Signals in Your Own Code

So, what if you don’t want your program to just die when someone presses Ctrl+C? You write a handler. Here’s the classic way to do it in Python, which demonstrates the concept beautifully. You set up a function to run when a specific signal is received.

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

def graceful_shutdown(signum, frame):
    """Handle SIGTERM and SIGINT."""
    print(f"\nReceived signal {signum}. Shutting down gracefully...")
    # Perform cleanup here: close files, notify others, etc.
    time.sleep(2) # Simulating some cleanup work
    print("Cleanup complete. Exiting.")
    sys.exit(0)

# Register our handler for both SIGTERM and SIGINT
signal.signal(signal.SIGTERM, graceful_shutdown)
signal.signal(signal.SIGINT, graceful_shutdown)

print("Program started. PID:", os.getpid())
print("Press Ctrl+C to test or run 'kill <pid>' from another terminal.")

# Keep the program running
while True:
    time.sleep(1)

Run this, then try killing it with kill <pid> from another terminal. You’ll see your cleanup message instead of an abrupt stop. Now try kill -9 <pid>. See? The SIGKILL blows right past your carefully written handler. This is why you can’t just SIGKILL everything—you lose the chance to clean up.

The Gotchas and Best Practices

Here’s the stuff the manuals often gloss over.

First, signal handlers are tricky. You can’t just do anything inside them. The function is called asynchronously, literally interrupting the normal flow of your code. This means you must only use what the standard calls “async-signal-safe” functions. For example, doing a bunch of print() statements or logging.info() calls inside a handler is a great way to occasionally deadlock your program, because those functions are not necessarily re-entrant. Keep handlers small, simple, and set a flag instead of doing real work.

Second, signals can get lost. If you send five SIGTERMs to a process, it doesn’t get a queue of five requests. It gets one. Signals are a notification, not a count. The kernel just tells the process “Hey, someone sent you a SIGTERM at some point.”

Finally, remember the zombie process. A process that has died but whose parent hasn’t yet called wait() to get its exit status is a zombie. Sending signals to a zombie is pointless; it’s already dead. It’s just waiting for its parent to bury it. To kill a zombie, you must kill its parent. It’s morbid, but that’s how it works.

The golden rule is this: use SIGTERM first, always. Respect the protocol. Give processes a chance to clean up their own mess. Only bring out SIGKILL when diplomacy has unequivocally failed. Your filesystems and databases will thank you.