- Published on
Understanding Signals in Linux: From Ctrl+C to Process Communication
- Authors
- Name
- Pulathisi Kariyawasam
- @RandhanaK

If you’ve ever used the terminal and pressed Ctrl+C, you probably thought you were “killing” the program.
But the truth is, you were sending a signal a simple notification that the process can react to.
Signals are a lightweight but powerful way for processes to communicate in Unix and Linux systems. They don’t transfer data, but they let processes know that something important has happened.
In this article, we’ll dive into:
- What signals are and how they work
- The most common signals you’ll encounter
- How processes can handle signals with C programs
- Practical ways signals are used in real systems
What Are Signals?
A signal is a software interrupt sent to a process. It’s the operating system’s way of telling a program that an event occurred.
Think of it like a traffic horn. You don’t explain in words what to do the horn is just a quick notification. The other driver then decides how to react.
In the same way:
- The OS sends a signal.
- The process either reacts, ignores, or terminates depending on the signal.
Common Signals You’ll See
Linux defines many signals, but here are the ones you’ll run into most often:
Signal | Number | Sent By | Default Action |
---|---|---|---|
SIGINT | 2 | Ctrl+C in the terminal | Terminates the process |
SIGTERM | 15 | kill <pid> | Terminates the process (can be caught/handled) |
SIGKILL | 9 | kill -9 <pid> | Forcefully kills process (cannot be caught or ignored) |
SIGHUP | 1 | Terminal closed / hangup | Terminates process or reloads config |
SIGSTOP | 19 | kill -STOP <pid> or Ctrl+Z | Pauses the process |
SIGCONT | 18 | kill -CONT <pid> or fg | Resumes a paused process |
Notice the difference between SIGTERM and SIGKILL:
SIGTERM
is a polite request: “Please shut down.”SIGKILL
is the hammer: “Shut down now, no questions.”
Trying Signals from the Terminal
Let’s say you start a long-running program, like sleep 1000
:
sleep 1000
Now open another terminal and find its process ID (PID):
ps aux | grep "[s]leep 1000"
You’ll see a line like this:
user 12345 0.0 0.0 1234 432 ? S 12:00 0:00 sleep 1000
Here, 12345
is the PID. You can send signals to it:
# Ask it to stop nicely (default is SIGTERM)
kill 12345
# Forcefully kill it (cannot be caught or ignored)
kill -9 12345 # SIGKILL
# Pause (stop) the process
kill -STOP 12345 # SIGSTOP
# Resume a stopped process
kill -CONT 12345 # SIGCONT
Tip:
kill -l
lists all signal names and numbers on your system.
Handling Signals in C
Programs don’t have to accept the default action. They can catch certain signals and run cleanup code before exiting.
Here’s a minimal C program that catches SIGINT (Ctrl+C) and exits cleanly:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
volatile sig_atomic_t stop = 0;
void on_sigint(int sig) {
(void)sig; // unused
write(STDOUT_FILENO, "\nReceived SIGINT. Cleaning up...\n", 34);
stop = 1;
}
int main(void) {
signal(SIGINT, on_sigint); // register handler
while (!stop) {
write(STDOUT_FILENO, ".", 1);
sleep(1);
}
// Do any final cleanup here
write(STDOUT_FILENO, "\nDone.\n", 7);
return 0;
}
Compile and run:
gcc signals_basic.c -o signals_basic
./signals_basic
Press Ctrl+C. Instead of an abrupt stop, the program prints a message and exits cleanly.
SIGTERM
vs SIGKILL
- SIGTERM (15): A polite request to stop. Programs can catch this, finish work, and exit.
- SIGKILL (9): The operating system immediately ends the process. Programs cannot catch or ignore it.
Graceful services (databases, web servers, background workers) should handle SIGTERM to close files, flush buffers, and release locks.
Example handler for SIGTERM using the safer sigaction
API:
#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
volatile sig_atomic_t shutting_down = 0;
static void term_handler(int sig) {
(void)sig;
shutting_down = 1;
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = term_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // restart some interrupted syscalls
sigaction(SIGTERM, &sa, NULL);
puts("Service running. Send SIGTERM to stop...");
while (!shutting_down) {
// do work
sleep(1);
}
puts("Shutting down gracefully: closing resources...");
// close files, flush, free memory, etc.
return 0;
}
Send a graceful stop from another terminal:
kill -TERM <pid>
Handling Multiple Signals
You can handle several signals by registering handlers for each:
struct sigaction sa_int = {0}, sa_term = {0};
sa_int.sa_handler = on_sigint;
sa_term.sa_handler = term_handler;
sigaction(SIGINT, &sa_int, NULL);
sigaction(SIGTERM, &sa_term, NULL);
For short, predictable work in handlers, prefer async-signal-safe functions (like write
) instead of printf
.
Temporarily Blocking Signals (Signal Masks)
Sometimes you need to protect a critical section from interruption:
sigset_t block, prev;
sigemptyset(&block);
sigaddset(&block, SIGINT);
// Block SIGINT
sigprocmask(SIG_BLOCK, &block, &prev);
// critical section
// ...
// Restore previous mask
sigprocmask(SIG_SETMASK, &prev, NULL);
SIGHUP
Reloading Configuration with Many daemons use SIGHUP to mean “reload configuration.” Here’s a tiny example:
static volatile sig_atomic_t reload = 0;
static void hup_handler(int sig) {
(void)sig;
reload = 1;
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = hup_handler;
sigaction(SIGHUP, &sa, NULL);
for (;;) {
if (reload) {
puts("Reloading configuration...");
reload = 0;
// reread config file here
}
sleep(1);
}
}
Trigger a reload from another terminal:
kill -HUP <pid>
trap
Bash: Handling Signals with You can also handle signals in shell scripts using trap
:
#!/usr/bin/env bash
set -euo pipefail
cleanup() {
echo "Cleaning up..."
}
trap cleanup SIGINT SIGTERM
echo "Working... press Ctrl+C to stop"
sleep 1000
Run it and press Ctrl+C the cleanup
function runs before the script exits.
Debugging: See Signals in Action
A few handy tools and commands:
# Show signals received by a process
strace -e trace=signal -p <pid>
# Send a user-defined signal (for custom actions)
kill -USR1 <pid>
# Send a signal to processes by name
pkill -TERM myservice
killall -HUP nginx
Note:
SIGUSR1
andSIGUSR2
are great for custom, app-specific actions (e.g., rotate logs).
Signals in Containers and Services
- Docker sends
SIGTERM
to PID 1 in the container ondocker stop
, thenSIGKILL
if it doesn’t exit within the timeout. - systemd services receive
SIGTERM
onsystemctl stop <service>
. Implement handlers so your service shuts down cleanly.
Quick Reference (Cheat Sheet)
Name | Number* | Default Action | Typical Use |
---|---|---|---|
SIGHUP | 1 | Terminate (often reload) | Reload config for daemons |
SIGINT | 2 | Terminate | Ctrl+C from terminal |
SIGTERM | 15 | Terminate | Graceful shutdown request |
SIGKILL | 9 | Kill | Immediate stop (cannot handle) |
SIGSTOP | 19 | Stop | Pause execution (cannot handle) |
SIGCONT | 18 | Continue | Resume a stopped process |
SIGUSR1/2 | 10/12 | Terminate | App-defined actions |
*Signal numbers can vary across platforms; prefer names like -TERM
, -KILL
.
Key Takeaways
- Ctrl+C sends SIGINT it’s a signal, not magic.
- Use SIGTERM for graceful stops; reserve SIGKILL for last resort.
- Handle signals with
sigaction
for safer, more predictable behavior. trap
in Bash and tools likestrace
help you test and debug.- In production, always clean up: close files, flush data, and release resources before exiting.
Practice: Your Turn
- Run a long task (
sleep 1000
). Usekill
,kill -STOP
, andkill -CONT
to control it. - Compile the C samples and send
SIGINT
,SIGTERM
, andSIGHUP
to see different behaviors. - Add a
SIGUSR1
handler to toggle verbose logging at runtime.