Signal Fundamentals
Signals are asynchronous notifications delivered by the kernel to a process. They interrupt whatever the process is doing -- right now, at any instruction boundary. They are Unix's oldest form of inter-process communication, and they are everywhere: Ctrl+C, child process death, illegal memory access, broken pipes. You cannot write robust systems code without understanding them.
What Signals Are
A signal is a small integer sent from the kernel (or another process) to a target process. When a signal arrives, the process can:
- Run a handler function (custom code).
- Accept the default action (terminate, core dump, ignore, or stop).
- Block the signal temporarily (it stays pending).
Kernel / Other Process
|
| signal (e.g., SIGINT)
v
+-----------------+
| Target Process |
| |
| Normal code | <-- interrupted
| ... |
| Handler runs | <-- if installed
| ... |
| Normal code | <-- resumes
+-----------------+
Common Signals
| Signal | Number | Default Action | Trigger |
|---|---|---|---|
SIGHUP | 1 | Terminate | Terminal hangup |
SIGINT | 2 | Terminate | Ctrl+C |
SIGQUIT | 3 | Core dump | Ctrl+\ |
SIGILL | 4 | Core dump | Illegal instruction |
SIGABRT | 6 | Core dump | abort() |
SIGFPE | 8 | Core dump | Divide by zero (integer) |
SIGKILL | 9 | Terminate | Uncatchable kill |
SIGSEGV | 11 | Core dump | Bad memory access |
SIGPIPE | 13 | Terminate | Write to broken pipe |
SIGALRM | 14 | Terminate | alarm() timer |
SIGTERM | 15 | Terminate | Polite termination request |
SIGCHLD | 17 | Ignore | Child process stopped/exited |
SIGCONT | 18 | Continue | Resume stopped process |
SIGSTOP | 19 | Stop | Uncatchable stop |
SIGTSTP | 20 | Stop | Ctrl+Z |
SIGUSR1 | 10 | Terminate | User-defined |
SIGUSR2 | 12 | Terminate | User-defined |
Caution:
SIGKILL(9) andSIGSTOP(19) cannot be caught, blocked, or ignored. The kernel enforces this. Do not waste time trying to handle them.
Default Actions
There are four possible default actions:
- Terminate: Process exits.
- Core dump: Process exits and writes a core file (if enabled).
- Ignore: Signal is silently discarded.
- Stop: Process is suspended (like Ctrl+Z).
SIGCHLD and SIGURG default to ignore. Most signals default to terminate.
Sending Signals
From the shell:
$ kill -TERM 1234 # Send SIGTERM to PID 1234
$ kill -9 1234 # Send SIGKILL (uncatchable)
$ kill -SIGUSR1 1234 # Send SIGUSR1
$ kill -0 1234 # Test if process exists (no signal sent)
From C:
/* send_signal.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(void)
{
pid_t pid = fork();
if (pid == 0) {
printf("Child %d: waiting for signal...\n", getpid());
pause(); /* Suspend until any signal arrives */
printf("Child: this line never reached (default SIGTERM kills)\n");
_exit(0);
}
sleep(1);
printf("Parent: sending SIGTERM to child %d\n", pid);
kill(pid, SIGTERM);
int status;
waitpid(pid, &status, 0);
if (WIFSIGNALED(status))
printf("Child killed by signal %d\n", WTERMSIG(status));
return 0;
}
The signal() Function (And Why You Should Not Use It)
The original BSD/POSIX signal() function installs a handler:
/* signal_old.c -- for demonstration only */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static void handler(int sig)
{
/* UNSAFE: printf is not async-signal-safe -- demo only */
printf("Caught signal %d\n", sig);
}
int main(void)
{
signal(SIGINT, handler);
printf("PID %d: Press Ctrl+C (3 times to exit via default)\n", getpid());
for (int i = 0; i < 30; i++) {
printf("tick %d\n", i);
sleep(1);
}
return 0;
}
Caution:
signal()has portability problems. On some systems it resets the handler toSIG_DFLafter each delivery (System V behavior). On others it does not (BSD behavior). The behavior ofsignal()is implementation-defined by POSIX. Always usesigaction()instead (covered in the next chapter).
A Signal Demo: Handling SIGINT and SIGTERM
Here is a proper pattern using a flag (still using signal() for simplicity --
we will fix this with sigaction() in the next chapter):
/* graceful_shutdown.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static volatile sig_atomic_t got_signal = 0;
static void handler(int sig)
{
got_signal = sig;
}
int main(void)
{
signal(SIGINT, handler);
signal(SIGTERM, handler);
printf("PID %d: Running... (Ctrl+C to stop)\n", getpid());
while (!got_signal) {
/* Main work loop */
printf("Working...\n");
sleep(2);
}
printf("Received signal %d, shutting down gracefully.\n", got_signal);
/* Cleanup code here */
return 0;
}
The key type is volatile sig_atomic_t -- an integer type guaranteed to be
read and written atomically with respect to signal delivery.
Main thread: Signal delivery:
while (!got_signal) { handler(SIGINT) {
work(); got_signal = SIGINT;
sleep(2); }
}
| |
+---- reads got_signal ---+
Try It: Modify
graceful_shutdown.cto count how many times Ctrl+C is pressed. After 3 presses, exit. Print the count in the main loop.
SIGCHLD: Child Process Notifications
When a child process exits, the kernel sends SIGCHLD to the parent. This is
how servers avoid blocking on waitpid():
/* sigchld_demo.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <errno.h>
static void sigchld_handler(int sig)
{
(void)sig;
int saved_errno = errno;
/* Reap all dead children (non-blocking) */
while (waitpid(-1, NULL, WNOHANG) > 0)
;
errno = saved_errno;
}
int main(void)
{
signal(SIGCHLD, sigchld_handler);
/* Spawn 3 children that exit at different times */
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
sleep(i + 1);
printf("Child %d (PID %d) exiting\n", i, getpid());
_exit(0);
}
printf("Spawned child %d: PID %d\n", i, pid);
}
/* Parent does other work */
for (int i = 0; i < 5; i++) {
printf("Parent working (tick %d)...\n", i);
sleep(2);
}
return 0;
}
Caution: The
SIGCHLDhandler must callwaitpid()in a loop withWNOHANG. Multiple children can exit before the handler runs, but signals are not queued (standard signals, at least). OneSIGCHLDdelivery might represent multiple dead children.
SIGPIPE: Broken Pipes
When you write to a pipe or socket whose read end is closed, the kernel sends
SIGPIPE. The default action kills the process.
/* sigpipe_demo.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main(void)
{
/* Ignore SIGPIPE -- check write() return instead */
signal(SIGPIPE, SIG_IGN);
int pipefd[2];
pipe(pipefd);
/* Close the read end immediately */
close(pipefd[0]);
/* Write to the broken pipe */
const char *msg = "Hello, pipe!\n";
ssize_t n = write(pipefd[1], msg, strlen(msg));
if (n < 0) {
printf("Write failed: %s (errno=%d)\n", strerror(errno), errno);
/* errno == EPIPE */
} else {
printf("Wrote %zd bytes\n", n);
}
close(pipefd[1]);
return 0;
}
Driver Prep: Kernel drivers handle signals indirectly. When a user-space process receives a signal while blocked in a system call, the kernel returns
EINTR(orERESTARTSYSinternally). Driver code must check forsignal_pending(current)and return-ERESTARTSYSso the VFS layer can restart or abort the system call.
Rust: The signal-hook Crate
Rust has no built-in signal handling in std. The signal-hook crate provides
safe abstractions.
// signal_hook_demo.rs // Cargo.toml: // [dependencies] // signal-hook = "0.3" use signal_hook::consts::{SIGINT, SIGTERM}; use signal_hook::flag; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::Duration; fn main() { let running = Arc::new(AtomicBool::new(true)); // Register signal handlers that set the flag to false flag::register(SIGINT, Arc::clone(&running)).expect("register SIGINT"); flag::register(SIGTERM, Arc::clone(&running)).expect("register SIGTERM"); println!("PID {}: Running... (Ctrl+C to stop)", std::process::id()); while running.load(Ordering::Relaxed) { println!("Working..."); thread::sleep(Duration::from_secs(2)); } println!("Signal received, shutting down."); }
Rust Note:
signal-hookuses atomic flags internally, avoiding the async-signal-safety pitfalls of C handlers. Theflag::registerfunction installs a minimal handler that just flips anAtomicBool. No unsafe code is needed in user code.
Rust: Using nix for Signals
The nix crate wraps the POSIX signal API:
// nix_signal_demo.rs // Cargo.toml: nix = { version = "0.29", features = ["signal", "process"] } use nix::sys::signal::{self, Signal, SigHandler}; use nix::unistd::Pid; use std::thread; use std::time::Duration; extern "C" fn handler(sig: libc::c_int) { // Minimal handler -- only async-signal-safe operations // We just use write() to fd 1 (not println!) let msg = b"Signal caught!\n"; unsafe { libc::write(1, msg.as_ptr() as *const libc::c_void, msg.len()); } let _ = sig; } fn main() { // Install handler for SIGUSR1 unsafe { signal::signal(Signal::SIGUSR1, SigHandler::Handler(handler)) .expect("signal"); } let pid = nix::unistd::getpid(); println!("PID {}: send SIGUSR1 to me", pid); println!(" kill -USR1 {}", pid); // Also send it to ourselves thread::sleep(Duration::from_secs(1)); signal::kill(Pid::this(), Signal::SIGUSR1).expect("kill"); thread::sleep(Duration::from_secs(1)); println!("Done."); }
Listing Signals on Your System
/* list_signals.c */
#include <stdio.h>
#include <string.h>
#include <signal.h>
int main(void)
{
for (int i = 1; i < NSIG; i++) {
const char *name = strsignal(i);
if (name)
printf("%2d %s\n", i, name);
}
return 0;
}
$ gcc -o list_signals list_signals.c && ./list_signals
1 Hangup
2 Interrupt
3 Quit
...
Try It: Run
kill -lin your shell to see the full signal list. Compare it with the output oflist_signals. Note the real-time signals at the end (32+).
Signal Delivery Flow
Event occurs (Ctrl+C, child dies, bad memory access, kill())
|
v
Kernel sets signal as "pending" for target process
|
v
Process is scheduled to run (or already running)
|
v
Kernel checks: is signal blocked?
| |
YES NO
| |
v v
Signal stays Check disposition:
pending SIG_DFL / SIG_IGN / handler
|
+---------+----------+
| | |
SIG_DFL SIG_IGN handler()
| | |
Default Discard Run handler,
action then resume
Knowledge Check
-
Name two signals that cannot be caught or ignored.
-
What is the default action for
SIGCHLD? Why is this important for servers that fork child processes? -
Why is
volatile sig_atomic_trequired for variables shared between a signal handler and the main program?
Common Pitfalls
-
Ignoring SIGPIPE: Network servers must ignore
SIGPIPEor they will die when a client disconnects mid-write. Usesignal(SIGPIPE, SIG_IGN)and checkwrite()return values. -
Not saving/restoring errno in handlers: Signal handlers can clobber
errno. Save it on entry, restore on exit. -
Assuming signals are queued: Standard signals (1-31) are not queued. If two
SIGCHLDsignals arrive before the handler runs, you get one delivery. Always loop inwaitpid()withWNOHANG. -
Using printf in handlers: It is not async-signal-safe. Use
write()to a file descriptor if you must produce output. -
Forgetting that sleep() is interrupted:
sleep(),read(),write(), and other blocking calls return early withEINTRwhen a signal is caught. Always retry or handle the short return. -
Catching SIGSEGV to "handle" crashes: You can catch it, but you cannot safely resume. The faulting instruction will re-execute and fault again unless you fix the underlying memory issue (which you almost certainly cannot do portably).