Chapter 21.1 — Designing Signal Handlers

 

Chapter 21.1 — Designing Signal Handlers
Linux Systems Programming · EmbeddedPathashala
21.1
Section
3
Code Examples
8
Interview Qs

What is a Signal Handler?

A signal handler is a function that your program registers to be called automatically when the kernel delivers a specific signal to your process. For example, when you press Ctrl+C, the kernel sends SIGINT to the foreground process — if you registered a handler, it runs instead of the default action (which is to terminate the process).

Writing signal handlers correctly is tricky because a signal can arrive at any point in your program’s execution, interrupting whatever it was doing. This creates hidden concurrency problems even in single-threaded programs.

Key Terms in This Chapter
Signal Handler sigaction() SIGINT SIGCHLD SA_NODEFER Pending Signal Signal Mask Blocked Signal Global Flag Pattern volatile

21.1 — Two Golden Rules for Signal Handler Design

Because a signal can arrive at any time and interrupt any piece of code, signal handlers must be kept as simple as possible. There are two widely accepted design patterns:

Design Pattern 1 — Flag + Poll
Signal arrives
Handler sets global flag = 1
Main loop checks flag & reacts
Design Pattern 2 — Cleanup + Exit
Signal arrives
Handler does cleanup (close fds, etc.)
_exit() or nonlocal goto
Why keep handlers simple? The simpler the handler, the less chance of race conditions, reentrancy bugs, and accidental use of unsafe functions.

Code Example 1 — Flag Pattern (Most Common, Safest)

This is the most recommended approach. The handler only sets a flag. All real work is done in the main loop.

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

/* Use volatile sig_atomic_t for the flag — explained in section 21.1.3 */
static volatile sig_atomic_t got_sigint = 0;

static void sigint_handler(int sig)
{
    /*
     * DO NOT call printf() here — it is NOT async-signal-safe.
     * Just set the flag and return immediately.
     */
    got_sigint = 1;
}

int main(void)
{
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

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

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

    while (1) {
        /* Main loop polls the flag */
        if (got_sigint) {
            printf("\n[Main] Caught SIGINT via flag. Cleaning up...\n");
            /* Safe to do all real work here */
            break;
        }
        sleep(1);
        printf(".");
        fflush(stdout);
    }

    printf("Done.\n");
    return 0;
}
Compile & Run: gcc -Wall -o flag_demo flag_demo.c && ./flag_demo
Press Ctrl+C — the handler fires, sets the flag, and the main loop picks it up cleanly.

21.1.1 — Signals Are Not Queued

When a signal handler is running, the same signal is blocked by default (unless you use SA_NODEFER). If the signal arrives again while the handler is running, it is marked pending. But if it arrives a third time, it is silently dropped — signals do not queue up.

Event What Happens Delivered?
Signal 1 arrives (handler not running) Handler invoked immediately Yes
Signal 2 arrives (handler is running) Marked as pending — delivered after handler returns Yes (once)
Signal 3+ arrives (handler still running) Still only 1 pending — extras are dropped No
Design Impact: You cannot reliably count how many times a signal was sent. If 5 children exit while your SIGCHLD handler is running, you may only see 2 deliveries. Your handler must loop to process all pending children (shown in Chapter 26).

Code Example 2 — Observing Signal Non-Queuing

This program deliberately sleeps inside the handler so you can send multiple signals and observe that only one gets queued.

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

static volatile sig_atomic_t count = 0;

static void sigusr1_handler(int sig)
{
    count++;
    printf("[Handler] Invocation #%d — sleeping 3 seconds inside handler\n",
           (int)count);
    /*
     * We sleep inside the handler. During this 3 seconds,
     * send SIGUSR1 multiple times with: kill -USR1 <pid>
     * You will see that only ONE extra delivery is queued.
     */
    sleep(3);
    printf("[Handler] Waking up from invocation #%d\n", (int)count);
}

int main(void)
{
    struct sigaction sa;
    sa.sa_handler = sigusr1_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;   /* No SA_NODEFER — signal is blocked during handler */

    sigaction(SIGUSR1, &sa, NULL);

    printf("PID = %d. Send SIGUSR1 rapidly: kill -USR1 %d\n",
           (int)getpid(), (int)getpid());
    printf("Watch: no matter how many you send, at most 2 handler calls happen.\n");

    /* Wait for signals */
    while (1) pause();

    return 0;
}
Test it: Open two terminals. Run the program. In the second terminal, run:
for i in $(seq 1 10); do kill -USR1 <pid>; done
You’ll see the handler runs at most twice regardless of how many signals you sent.

Code Example 3 — Self-Pipe Trick (Flag Pattern for select/poll loops)

When your main loop uses select() or poll() and can’t periodically check a flag, the handler writes one byte to a pipe. The read-end of the pipe is added to the monitored fds — this is the self-pipe trick.

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/select.h>
#include <fcntl.h>
#include <string.h>

static int pipe_fds[2];  /* pipe_fds[0] = read end, pipe_fds[1] = write end */

static void sigint_handler(int sig)
{
    /*
     * write() is async-signal-safe — safe to call here.
     * We write exactly 1 byte to wake up select().
     */
    char byte = 1;
    write(pipe_fds[1], &byte, 1);
}

int main(void)
{
    /* Create the self-pipe */
    if (pipe(pipe_fds) == -1) { perror("pipe"); return 1; }

    /* Make write end non-blocking to avoid deadlock in handler */
    int flags = fcntl(pipe_fds[1], F_GETFL);
    fcntl(pipe_fds[1], F_SETFL, flags | O_NONBLOCK);

    /* Install handler */
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGINT, &sa, NULL);

    printf("PID=%d. Waiting in select()... Press Ctrl+C\n", (int)getpid());

    while (1) {
        fd_set rfds;
        FD_ZERO(&rfds);
        FD_SET(pipe_fds[0], &rfds);   /* monitor the pipe read end */

        int ret = select(pipe_fds[0] + 1, &rfds, NULL, NULL, NULL);
        if (ret == -1) { perror("select"); break; }

        if (FD_ISSET(pipe_fds[0], &rfds)) {
            char buf[16];
            read(pipe_fds[0], buf, sizeof(buf));  /* drain the pipe */
            printf("\n[Main] Woken by SIGINT via self-pipe. Exiting cleanly.\n");
            break;
        }
    }

    close(pipe_fds[0]);
    close(pipe_fds[1]);
    return 0;
}
Why this works: write() is async-signal-safe. The single byte wakes select() without any unsafe function calls in the handler. This is the standard pattern for event-driven servers.

Interview Questions — Signal Handler Design
Q1. What are the two main design patterns for signal handlers and when would you use each?
Pattern 1 (flag + poll): handler sets a volatile sig_atomic_t flag; main loop checks it. Use when the main loop can poll. Pattern 2 (cleanup + exit): handler does minimal cleanup then terminates or does nonlocal goto. Use when you need to abort on a fatal signal like SIGSEGV.
Q2. Are signals queued in Linux? What happens if the same signal is sent 5 times while the handler is running?
No. Standard signals are not queued. If sent 5 times while the handler runs, at most 1 delivery is queued (marked pending). The rest are silently dropped. The handler will run at most once more after the current invocation returns.
Q3. What is SA_NODEFER and when is it useful?
By default, when a signal handler runs, that signal is automatically blocked (added to the process signal mask). SA_NODEFER disables this — the signal is not blocked during the handler. Useful when you want a handler to be re-entrant (e.g., a handler that uses setjmp/longjmp and must restart immediately).
Q4. What is the self-pipe trick and why is it needed?
When a main loop blocks in select()/poll(), it cannot check a flag. The self-pipe trick solves this: the signal handler writes one byte to a pipe’s write end (safe — write() is async-signal-safe), and the pipe’s read end is included in the monitored fd set. The write wakes select(), allowing the main loop to respond to the signal.
Q5. Why should signal handlers be kept simple?
Because a signal can arrive at any point in program execution, complex handlers risk: (1) calling non-async-signal-safe functions causing corruption, (2) race conditions updating shared data, (3) reentrancy bugs if the handler interrupts itself or a library function.
Q6. What is the difference between a blocked signal and a pending signal?
A blocked signal is one in the process’s signal mask — it will not be delivered even if sent. A pending signal is one that has been sent but not yet delivered (because it is currently blocked). When the signal is unblocked, the pending signal is delivered.
Q7. Can printf() be safely called from a signal handler?
No. printf() uses internal buffered I/O data structures (from the stdio library) that are not async-signal-safe. If the signal interrupts the main program while it is inside printf(), and the handler also calls printf(), those internal structures can become corrupted. Use write() (async-signal-safe) for output from handlers.
Q8. A SIGCHLD signal is sent when a child exits. If 3 children exit simultaneously, how many times will the SIGCHLD handler run?
Possibly only once or twice — not three times. Standard signals are not queued, so multiple simultaneous SIGCHLDs may collapse into one pending delivery. This is why the SIGCHLD handler must loop calling waitpid() with WNOHANG until it returns 0, to reap all terminated children in one handler invocation.

Next: Reentrancy & Async-Signal-Safe Functions →

Chapter 21.1.2 — Reentrancy Back to Index

Leave a Reply

Your email address will not be published. Required fields are marked *