Skip to content

C Signals — Handling Operating System Interrupts with signal() and sigaction()

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about C Signals. We cover key concepts, practical examples, and best practices to help you master this topic.

C signal handling uses signal() or sigaction() to register handler functions for operating system interrupts (SIGINT, SIGTERM, SIGSEGV), enabling graceful cleanup, logging, and controlled termination.

What You Will Learn

  • What signals are and the common signal types
  • Registering handlers with signal() and sigaction()
  • Writing async-signal-safe handler functions
  • Blocking and unblocking signals with sigprocmask
  • Handling SIGSEGV for crash diagnostics
  • The limitations and dangers of signal handlers

Why It Matters

Signals are the operating system's way of notifying a process of events: the user pressed Ctrl+C, a timer expired, a child process exited, or the program accessed invalid memory. Ignoring signals means your program terminates abruptly without cleanup. Proper signal handling lets you save state, close files, release resources, and log diagnostic information. Durga Antivirus Pro's scan daemon catches SIGTERM to finish the current file scan before exiting, preventing files from being left in an inconsistent state.

Real-World Use

A database server receives SIGTERM when the system shuts down. Its signal handler sets a global flag, the main loop checks the flag between queries, completes the current transaction, flushes the write-ahead log, closes all database files, and exits cleanly. Without the handler, the server would be killed immediately, potentially corrupting the database.

Learning Path

flowchart LR
  A[Error Handling] --> B[Signals\nYou are here]
  B --> C[Multithreading]
  style B fill:#f90,color:#fff

Basic Signal Handling with signal()

The simplest way to handle signals is with the signal() function:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

volatile sig_atomic_t keep_running = 1;

void handle_sigint(int sig) {
    // Keep signal handlers simple and async-signal-safe
    keep_running = 0;
}

int main() {
    // Register handler for SIGINT (Ctrl+C)
    signal(SIGINT, handle_sigint);

    printf("Running. Press Ctrl+C to stop...\n");

    while (keep_running) {
        printf("Working...\n");
        sleep(1);
    }

    printf("\nCleanup complete. Exiting.\n");
    return 0;
}

When you press Ctrl+C, the handler sets keep_running = 0 and the main loop exits cleanly.

The signal() function has platform-dependent behavior. sigaction() is the POSIX-standard, portable way to set up signal handlers with precise control:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

volatile sig_atomic_t shutdown_requested = 0;

void handle_signal(int sig) {
    shutdown_requested = 1;
}

void setup_signal_handler(int sig) {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_signal;
    sigemptyset(&sa.sa_mask);  // Don't block additional signals

    // SA_RESTART: automatically restart interrupted system calls
    sa.sa_flags = SA_RESTART;

    if (sigaction(sig, &sa, NULL) == -1) {
        perror("sigaction");
        exit(1);
    }
}

int main() {
    setup_signal_handler(SIGINT);
    setup_signal_handler(SIGTERM);

    printf("Server running (PID: %d). Send SIGTERM or press Ctrl+C.\n", getpid());

    int counter = 0;
    while (!shutdown_requested) {
        printf("Tick %d\n", ++counter);
        sleep(1);
    }

    printf("Shutting down gracefully...\n");
    sleep(2);  // Simulate cleanup
    printf("Done.\n");

    return 0;
}

Async-Signal-Safe Functions

Signal handlers run asynchronously -- they can interrupt your code at any point. Only a limited set of functions are safe to call from a signal handler:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

// NOT async-signal-safe: printf, malloc, free, fprintf
// ASYNC-SIGNAL-SAFE: write, read, open, close, _exit, sig_atomic_t

void safe_handler(int sig) {
    // write() is async-signal-safe
    const char *msg = "Caught signal!\n";
    write(STDOUT_FILENO, msg, strlen(msg));

    // Set a flag for the main program
    // sig_atomic_t is guaranteed to be atomic
    volatile static sig_atomic_t flag = 1;
}

// Wrong: using printf in a signal handler
void unsafe_handler(int sig) {
    // printf is NOT async-signal-safe!
    // If the signal interrupts printf in the main program,
    // the program may deadlock or crash.
    printf("Caught signal %d\n", sig);
}

Safe operations in signal handlers:

  • Read and set volatile sig_atomic_t variables
  • Call write() on file descriptors already open
  • Call _exit() (not exit())
  • Call signal() to reset a handler

Blocking Signals with sigprocmask

Temporarily block signals during critical sections:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

volatile sig_atomic_t got_signal = 0;

void handler(int sig) {
    got_signal = 1;
}

int main() {
    signal(SIGINT, handler);

    // Block SIGINT during critical section
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);

    printf("Entering critical section (SIGINT blocked)...\n");
    sigprocmask(SIG_BLOCK, &block_set, &old_set);

    // Simulate critical work
    sleep(5);
    printf("Critical section complete.\n");

    // Restore previous signal mask (unblock SIGINT)
    sigprocmask(SIG_SETMASK, &old_set, NULL);

    printf("SIGINT unblocked. Pending signals will be delivered.\n");
    sleep(2);

    return 0;
}

If you press Ctrl+C during the 5-second Critical Section, the signal is pending. It is delivered when sigprocmask unblocks it.

Handling SIGSEGV for Crash Diagnostics

Catch segmentation faults to print a diagnostic message:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <execinfo.h>

void crash_handler(int sig) {
    // write() is async-signal-safe
    const char msg[] = "Caught signal: ";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);

    // Print backtrace (not strictly async-signal-safe but very useful)
    void *buffer[32];
    int frames = backtrace(buffer, 32);
    backtrace_symbols_fd(buffer, frames, STDERR_FILENO);

    // Reset to default handler and re-raise to get core dump
    signal(sig, SIG_DFL);
    raise(sig);
}

int main() {
    signal(SIGSEGV, crash_handler);
    signal(SIGABRT, crash_handler);

    printf("About to crash...\n");

    // Trigger a segmentation fault
    int *p = NULL;
    *p = 42;  // SIGSEGV here

    return 0;
}

Ignoring Signals

Some signals can be safely ignored:

#include <stdio.h>
#include <signal.h>

int main() {
    // Ignore SIGPIPE -- writing to a broken pipe returns EPIPE error instead
    signal(SIGPIPE, SIG_IGN);

    // Ignore SIGCHLD -- avoid zombie processes without calling wait()
    signal(SIGCHLD, SIG_IGN);

    printf("SIGPIPE and SIGCHLD are now ignored.\n");

    // Restore default behavior
    signal(SIGINT, SIG_DFL);
    printf("SIGINT restored to default (Ctrl+C will terminate).\n");

    pause();  // Wait for any signal
    return 0;
}

Common Mistakes

  1. Calling non-async-signal-safe functions in handlers: printf, malloc, free, and most library functions are not safe in signal handlers. Use only write(), sig_atomic_t, and _exit().

  2. Using signal() instead of sigaction(): signal() has different semantics across platforms (System V vs BSD). Use sigaction() for portable, predictable behavior.

  3. Deadlock from reentrant signal: If a signal occurs while its handler is already running, it interrupts itself. Block the signal in sa_mask to prevent reentrancy.

  4. Forgetting volatile for shared variables: Variables modified in a signal handler and read in the main program must be volatile sig_atomic_t to prevent compiler optimizations from Caching the value.

  5. Calling exit() instead of _exit() in a handler: exit() runs cleanup functions that may not be async-signal-safe. Use _exit() to terminate immediately from a handler.

  6. Not handling EINTR: System calls (read, write, sleep, accept) may return -1 with errno = EINTR when a signal interrupts them. Check for EINTR and retry.

  7. Trying to catch SIGKILL or SIGSTOP: These signals cannot be caught, blocked, or ignored. It is impossible to prevent SIGKILL from terminating a process.

Practice Questions

  1. Why is printf unsafe in a signal handler?
  2. What does the SA_RESTART flag in sigaction do?
  3. How does sigprocmask prevent signals from interrupting critical sections?
  4. Why must variables shared between a signal handler and main code be declared volatile sig_atomic_t?
  5. Challenge: Write a program that installs a handler for SIGINT that prints the number of times Ctrl+C has been pressed. After the third press, the program should exit. Use sigaction with proper signal masking to prevent reentrancy.

Mini Project

Build a graceful-shutdown server framework:

  • Define void server_run(int (*handler)(void*), void *arg, int timeout_seconds) that runs a handler function in a loop
  • Install signal handlers for SIGINT, SIGTERM, and SIGHUP
  • On SIGINT/SIGTERM: set a shutdown flag, wait for the current handler to complete (up to 5 seconds), then exit
  • On SIGHUP: re-read configuration (print "Re-reading config")
  • The handler function should check a server_should_stop() function at the start of each iteration
  • Use sigprocmask to block signals during critical internal state updates
  • Print a clear message for each signal received and the action taken
  • Test by running the server and sending signals with kill -SIGTERM <pid>

FAQ

What is the difference between SIGTERM and SIGKILL?

SIGTERM (15) can be caught, blocked, or ignored -- it requests graceful termination. SIGKILL (9) cannot be caught or blocked -- it forces immediate termination by the kernel.

Can I send a custom signal between processes?

Yes. SIGUSR1 and SIGUSR2 are reserved for user-defined purposes. Use kill(pid, SIGUSR1) to send them. This is a common IPC mechanism.

What happens if a signal handler causes another signal?

If a new signal is raised in a handler, it depends on whether that signal is masked. If masked, it becomes pending. If unmasked, it is delivered recursively, which may cause a stack overflow.

How do I get a stack trace in a signal handler?

Use backtrace() and backtrace_symbols_fd() from execinfo.h (GNU libc). These are not strictly async-signal-safe but work in practice on Linux for diagnostic purposes.

What signals can I not handle?

SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. SIGKILL always terminates. SIGSTOP always suspends. This is enforced by the kernel.

What is Next

Proceed to Multithreading to learn about POSIX threads for concurrent programming. Then explore Network Sockets for TCP/IP communication.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse C