Signal-Driven I/O Let the Kernel Notify You with SIGIO

 

Signal-Driven I/O
Chapter 63 — Part 3: Let the Kernel Notify You with SIGIO

What Is Signal-Driven I/O?

With select() and poll(), your program has to keep asking the kernel “is anything ready yet?” — that is polling behavior even if the call blocks. Signal-driven I/O flips this around: you tell the kernel which file descriptor to watch, and the kernel sends your process a signal the moment that fd becomes ready. Your program does not need to sit in a loop calling select/poll.

Think of it like ordering food at a restaurant with a buzzer. Instead of walking to the counter every 5 seconds asking “is my food ready?”, you sit down and the buzzer vibrates when it is ready. That buzzer is the signal.

How It Works — Three-Step Setup

1
Establish a signal handler for SIGIO (or a chosen realtime signal). This function runs when the fd becomes ready.
2
Set the owner process using fcntl(fd, F_SETOWN, pid). This tells the kernel which process should receive the signal.
3
Enable signal generation by setting the O_ASYNC flag with fcntl(fd, F_SETFL, flags | O_ASYNC). Now the kernel will send signals when fd becomes ready.

Basic Setup — Enabling Signal-Driven I/O

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

/* Step 1: Signal handler */
void sigio_handler(int sig)
{
    /* Do NOT call blocking functions here.
       Just set a flag and handle in main loop. */
    write(STDOUT_FILENO, "SIGIO received!\n", 16);
}

int setup_signal_driven_io(int fd)
{
    int flags;

    /* Step 1: Establish handler for SIGIO */
    struct sigaction sa;
    sa.sa_handler = sigio_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGIO, &sa, NULL) == -1)
        return -1;

    /* Step 2: Set our process as the owner (receives the signal) */
    if (fcntl(fd, F_SETOWN, getpid()) == -1)
        return -1;

    /* Step 3: Enable async I/O by setting O_ASYNC flag */
    flags = fcntl(fd, F_GETFL);
    if (flags == -1)
        return -1;
    if (fcntl(fd, F_SETFL, flags | O_ASYNC) == -1)
        return -1;

    return 0;
}

When Does the Signal Fire?

File Descriptor Type Signal Generated When…
Terminal (tty) Input becomes available
FIFO / Pipe Data available to read, or write end becomes writable
UDP Socket Datagram arrives or error occurs
TCP Socket Connection arrives on listening socket; data, FIN, or URG data on connected socket; various state changes

Complete SIGIO Example — Watch a Terminal

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

/* Volatile so the compiler does not optimize away changes in signal handler */
static volatile sig_atomic_t got_sigio = 0;

static void sigio_handler(int sig)
{
    got_sigio = 1;  /* Set flag — do actual I/O in main loop */
}

int main(void)
{
    struct sigaction sa;
    int flags;

    /* Enable raw mode so each character generates input immediately */
    struct termios orig_termios, raw;
    tcgetattr(STDIN_FILENO, &orig_termios);
    raw = orig_termios;
    raw.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);

    /* Step 1: Install SIGIO handler */
    sa.sa_handler = sigio_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGIO, &sa, NULL) == -1) {
        perror("sigaction"); exit(1);
    }

    /* Step 2: This process receives SIGIO for stdin */
    if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) {
        perror("F_SETOWN"); exit(1);
    }

    /* Step 3: Enable O_ASYNC on stdin */
    flags = fcntl(STDIN_FILENO, F_GETFL);
    if (fcntl(STDIN_FILENO, F_SETFL, flags | O_ASYNC) == -1) {
        perror("F_SETFL O_ASYNC"); exit(1);
    }

    printf("Type something (q to quit):\n");

    /* Main loop — the process can do other work here */
    /* We are NOT blocked waiting for I/O */
    while (1) {
        if (got_sigio) {
            char ch;
            ssize_t n;

            got_sigio = 0;  /* Reset flag */

            /* Read all available characters */
            while ((n = read(STDIN_FILENO, &ch, 1)) > 0) {
                if (ch == 'q') {
                    tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
                    printf("\nExiting.\n");
                    exit(0);
                }
                printf("Got: '%c'\n", ch);
            }
        }

        /* Simulate doing other work */
        printf("Main loop iteration (process is NOT blocked)\n");
        sleep(1);
    }

    return 0;
}

The Problem with Basic SIGIO

Problem 1 — Signal not queued: If two fds become ready before your handler runs, you only get ONE SIGIO signal. Signals are not queued — the second SIGIO gets lost. You would miss one ready fd.

Problem 2 — No info in handler: The basic SIGIO handler does not tell you WHICH fd became ready, or whether it is readable or writable. If you are watching 5 fds, your handler must check them all with nonblocking reads.

Solution: Use a realtime signal instead of SIGIO.

Realtime Signals — The Better Solution

Linux lets you change the signal used for notification with fcntl(fd, F_SETSIG, signum). If you pick a realtime signal (SIGRTMIN to SIGRTMAX), two important things happen:

  • Signals are queued — if multiple events happen, each one queues a separate signal. No events are lost.
  • siginfo_t is available — the signal handler receives a siginfo_t structure containing si_fd (which fd triggered) and si_code (POLL_IN, POLL_OUT, etc.)

Using Realtime Signals with F_SETSIG

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

#define MY_RT_SIG  (SIGRTMIN + 1)   /* Use SIGRTMIN+1 as our notification signal */

static void rt_signal_handler(int sig, siginfo_t *si, void *ucontext)
{
    /* si->si_fd tells us WHICH fd became ready */
    /* si->si_code tells us WHAT happened */

    if (si->si_code == POLL_IN) {
        printf("fd %d has data to read\n", si->si_fd);
    } else if (si->si_code == POLL_OUT) {
        printf("fd %d is ready for writing\n", si->si_fd);
    } else if (si->si_code == POLL_HUP) {
        printf("fd %d: hangup\n", si->si_fd);
    } else if (si->si_code == POLL_ERR) {
        printf("fd %d: error\n", si->si_fd);
    }

    /* Now do the actual I/O on si->si_fd */
}

int setup_rt_signal_io(int fd)
{
    struct sigaction sa;
    int flags;

    /* Install handler that accepts siginfo_t — use SA_SIGINFO flag */
    sa.sa_sigaction = rt_signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;   /* Required to get siginfo_t argument */
    if (sigaction(MY_RT_SIG, &sa, NULL) == -1)
        return -1;

    /* Set owner */
    if (fcntl(fd, F_SETOWN, getpid()) == -1)
        return -1;

    /* Change the signal from SIGIO to our realtime signal */
    if (fcntl(fd, F_SETSIG, MY_RT_SIG) == -1)
        return -1;

    /* Enable O_ASYNC */
    flags = fcntl(fd, F_GETFL);
    if (fcntl(fd, F_SETFL, flags | O_ASYNC) == -1)
        return -1;

    return 0;
}

int main(void)
{
    int sockfd;
    /* ... create socket, bind, etc. ... */

    /* Watch the socket with realtime signal notification */
    /* setup_rt_signal_io(sockfd); */

    /* Main loop can do other work — signal handler handles I/O events */
    while (1) {
        pause();  /* Wait for any signal */
    }

    return 0;
}

siginfo_t Fields Used in Signal-Driven I/O

typedef struct {
    int      si_signo;  /* Signal number */
    int      si_code;   /* POLL_IN, POLL_OUT, POLL_ERR, POLL_HUP, POLL_MSG */
    int      si_fd;     /* File descriptor that triggered the signal */
    /* ... many other fields ... */
} siginfo_t;

/* si_code values for I/O signals */
POLL_IN    /* Data available to read */
POLL_OUT   /* Output buffers available (writable) */
POLL_MSG   /* Input message available (STREAMS) */
POLL_ERR   /* I/O error */
POLL_PRI   /* High priority input available */
POLL_HUP   /* Device disconnected */

Signal-Driven I/O vs epoll

Aspect Signal-Driven I/O epoll
Notification model Signal to process (async) Synchronous — epoll_wait() returns
Complexity High — signal-safety rules, reentrancy Low — linear event loop
Which event type? Via si_code in siginfo_t Directly via events bitmask
Signal queue overflow Possible — fallback to SIGIO Not applicable

Interview Questions

Q1: What are the three steps to enable signal-driven I/O on a file descriptor?
First, install a signal handler for SIGIO (or a realtime signal) using sigaction(). Second, set the process as the owner of the fd using fcntl(fd, F_SETOWN, getpid()). Third, set the O_ASYNC flag on the fd using fcntl(fd, F_SETFL, flags | O_ASYNC). After these three steps, the kernel will send the signal whenever the fd becomes ready.
Q2: Why would you use a realtime signal instead of SIGIO?
Standard signals like SIGIO are not queued — if two events arrive before the handler runs, only one signal is delivered. Realtime signals are queued, so every event gets its own signal. Also, with SA_SIGINFO flag, the realtime signal handler receives a siginfo_t argument which contains si_fd (which file descriptor triggered) and si_code (whether it was readable or writable), so you know exactly what happened without checking all fds.
Q3: What is SA_SIGINFO and why is it needed?
SA_SIGINFO is a flag passed to sigaction() that changes the signal handler signature. Instead of void handler(int sig), the handler becomes void handler(int sig, siginfo_t *si, void *ucontext). This gives you the extended signal information — including si_fd and si_code for I/O signals — which is essential for knowing which fd triggered the signal when monitoring multiple descriptors.
Q4: Why should you not call blocking functions inside a SIGIO handler?
Signal handlers run asynchronously — they can interrupt any point in your main program, even in the middle of a malloc() call or printf(). Many standard library functions are not async-signal-safe (not reentrant). If your signal handler calls them and your main code is also inside them, you get undefined behavior, deadlocks, or data corruption. The safe pattern is to set a volatile sig_atomic_t flag in the handler and do the actual I/O work in the main loop after checking the flag.
Q5: Signal-driven I/O is described as “edge-triggered”. What does that mean in this context?
Edge-triggered means you get notified when the state changes — when I/O becomes possible — not continuously while it remains possible. With SIGIO, you get one signal when data arrives. If you do not read all the data, you do not get another signal until more new data arrives. This is different from select()/poll() which are level-triggered — they keep reporting the fd as ready as long as data remains unread.

Leave a Reply

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