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.

SignalTypical Meaning
SIGINTInterrupt from the terminal, commonly Ctrl+C
SIGTERMRequest for a process to terminate
SIGQUITQuit request from the terminal
SIGALRMAlarm timer expired
SIGHUPTerminal or controlling-session hangup
SIGCHLDChild process state changed

Including signal.h

The signal-related declarations provided by the C standard library are available through signal.h.

C
#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.

C
#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.

C
#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.

C
#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.

C
#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.

C
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.

C
#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.

C
#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.

C
#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.

C
#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.

C
#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.

C
#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

FeatureSignalsTypical Language Exceptions
PurposeAsynchronous process notificationsProgram-level error/control flow
SourceOS, terminal, timer, or another processProgram execution
TimingMay arrive asynchronouslyUsually occurs during normal execution
Typical UseInterrupts and process eventsRecoverable 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.

Note: Note: Basic signal handling is part of standard C, while APIs such as sigaction, sigprocmask, pause, and alarm are POSIX facilities commonly available on Linux and other Unix-like systems.