C Signals and Signal Handling: Handle Interrupts and System Events
Signals are asynchronous notifications delivered to a running program to indicate that an event has occurred. They are commonly used on Unix-like systems for events such as terminal interrupts, termination requests, child-process notifications, and timers.
What is a Signal?
A signal is a notification sent to a process. The operating system or another process can generate signals, and a program can choose to handle some of them.
| Signal | Typical Meaning |
|---|---|
| SIGINT | Interrupt from the terminal, commonly Ctrl+C |
| SIGTERM | Request for a process to terminate |
| SIGQUIT | Quit request from the terminal |
| SIGALRM | Alarm timer expired |
| SIGHUP | Terminal or controlling-session hangup |
| SIGCHLD | Child process state changed |
Including signal.h
The signal-related declarations provided by the C standard library are available through signal.h.
#include <stdio.h>
#include <signal.h>
int main(void)
{
printf("Signal handling example\n");
return 0;
}
Handling SIGINT with signal
The signal function provides a simple interface for installing a signal handler. SIGINT is commonly generated when a user presses Ctrl+C in a terminal.
#include <stdio.h>
#include <signal.h>
void handleInterrupt(int signalNumber)
{
(void)signalNumber;
printf("\nInterrupt received.\n");
}
int main(void)
{
signal(SIGINT, handleInterrupt);
printf("Press Ctrl+C to send SIGINT.\n");
while (1)
{
}
return 0;
}
Ignoring a Signal
Some signals can be explicitly ignored using SIG_IGN.
#include <signal.h>
int main(void)
{
signal(SIGINT, SIG_IGN);
while (1)
{
}
return 0;
}
Restoring the Default Action
SIG_DFL can be used to request the default signal disposition.
#include <signal.h>
int main(void)
{
signal(SIGINT, SIG_DFL);
return 0;
}
Handling SIGTERM
SIGTERM is commonly used by process-management tools to request graceful termination. A program can record that termination was requested and perform cleanup in its normal control flow.
#include <stdio.h>
#include <signal.h>
#include <stdbool.h>
static volatile sig_atomic_t stop = 0;
void handleTerminate(int signalNumber)
{
(void)signalNumber;
stop = 1;
}
int main(void)
{
signal(SIGTERM, handleTerminate);
while (!stop)
{
/* Perform application work. */
}
printf("Shutdown requested. Cleaning up...\n");
return 0;
}
Why Use volatile sig_atomic_t?
A signal handler may interrupt normal program execution at an unexpected point. volatile sig_atomic_t is the standard C mechanism for communicating a simple flag safely between a signal handler and ordinary program flow.
static volatile sig_atomic_t received = 0;
void handler(int signalNumber)
{
(void)signalNumber;
received = 1;
}
Sending a Signal with raise
The raise function sends a signal to the current program.
#include <stdio.h>
#include <signal.h>
void handler(int signalNumber)
{
printf("Signal received: %d\n", signalNumber);
}
int main(void)
{
signal(SIGUSR1, handler);
raise(SIGUSR1);
return 0;
}
Signal Handlers Should Be Minimal
Signal handlers execute asynchronously and have strict restrictions on what they can safely do. A robust handler should generally perform a minimal operation, such as setting a sig_atomic_t flag.
#include <signal.h>
static volatile sig_atomic_t interrupted = 0;
static void handleSignal(int signalNumber)
{
(void)signalNumber;
interrupted = 1;
}
int main(void)
{
signal(SIGINT, handleSignal);
while (!interrupted)
{
/* Main application loop. */
}
return 0;
}
Using sigaction on POSIX Systems
On POSIX systems, sigaction provides more control and is generally preferred over signal for production Unix-like applications.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static volatile sig_atomic_t stop = 0;
static void handleSignal(int signalNumber)
{
(void)signalNumber;
stop = 1;
}
int main(void)
{
struct sigaction action = {0};
action.sa_handler = handleSignal;
sigemptyset(&action.sa_mask);
if (sigaction(SIGINT, &action, NULL) == -1)
{
perror("sigaction");
return 1;
}
while (!stop)
{
pause();
}
printf("Stopping normally.\n");
return 0;
}
Using SIGALRM
On POSIX systems, alarm can schedule a SIGALRM signal after a specified number of seconds.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static volatile sig_atomic_t alarmTriggered = 0;
static void handleAlarm(int signalNumber)
{
(void)signalNumber;
alarmTriggered = 1;
}
int main(void)
{
signal(SIGALRM, handleAlarm);
alarm(5);
while (!alarmTriggered)
{
pause();
}
printf("Timer expired.\n");
return 0;
}
Blocking Signals with sigprocmask
POSIX programs can temporarily block selected signals using signal masks. This can be useful when a critical section must not be interrupted by particular signals.
#include <signal.h>
int main(void)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1)
{
return 1;
}
/* Critical section. */
sigprocmask(SIG_UNBLOCK, &set, NULL);
return 0;
}
Signal Handling and Cleanup
A common pattern is to let the handler set a flag and allow the main program to perform cleanup after it notices the flag.
#include <stdio.h>
#include <signal.h>
static volatile sig_atomic_t stop = 0;
static void handleInterrupt(int signalNumber)
{
(void)signalNumber;
stop = 1;
}
int main(void)
{
signal(SIGINT, handleInterrupt);
/* Initialize resources here. */
while (!stop)
{
/* Main work. */
}
/* Release resources here, outside the handler. */
printf("Resources cleaned up.\n");
return 0;
}
Signals That Cannot Be Caught or Ignored
On POSIX systems, SIGKILL and SIGSTOP cannot be caught, blocked, or ignored by a process. They are reserved for process-control purposes.
Signal Handling vs Exceptions
| Feature | Signals | Typical Language Exceptions |
|---|---|---|
| Purpose | Asynchronous process notifications | Program-level error/control flow |
| Source | OS, terminal, timer, or another process | Program execution |
| Timing | May arrive asynchronously | Usually occurs during normal execution |
| Typical Use | Interrupts and process events | Recoverable program errors |
Common Mistakes
- Doing complex work inside a signal handler
- Calling non-async-signal-safe functions from a handler
- Using ordinary shared variables instead of sig_atomic_t for simple handler flags
- Assuming all signals behave identically on every operating system
- Forgetting that some signals cannot be caught or ignored
- Relying on signal handler execution for complicated resource management
Best Practices
- Keep signal handlers extremely small
- Use volatile sig_atomic_t for simple communication flags
- Perform cleanup in normal program flow
- Prefer sigaction for POSIX applications
- Document which signals your program handles
- Use signal masks when carefully controlling signal delivery is required
- Avoid unsafe library operations inside handlers
Real-World Applications
- Graceful application shutdown
- Terminal interrupt handling
- Unix daemon management
- Process supervision
- Timer notifications
- Child-process management
- System-level utilities
Practice Exercises
- Create a program that handles Ctrl+C
- Build a program that gracefully responds to SIGTERM
- Create a signal-based shutdown flag
- Use sigaction to handle SIGINT
- Create a timer using SIGALRM on a POSIX system
- Experiment with blocking and unblocking SIGINT
- Build a simple long-running process that shuts down cleanly
Conclusion
Signal handling gives C programs a way to respond to asynchronous system and process events. For reliable programs, keep handlers minimal, communicate through sig_atomic_t flags when appropriate, and perform substantial work and cleanup in the normal execution flow.