Signal-Driven I/O — Setup & Demo

 

Signal-Driven I/O — Setup & Demo
Chapter 63 · Alternative I/O Models · The Linux Programming Interface
🔌 SIGIO Signal
🔧 F_SETOWN / O_ASYNC
📄 Demo Code Walk-through

What is Signal-Driven I/O?

In normal blocking I/O your program stops and waits until data is ready. In non-blocking I/O your program keeps polling in a loop asking “is data ready yet?” Signal-driven I/O is a smarter approach — your program tells the kernel “Hey, signal me when data is ready” and then goes off doing other work. When data actually arrives the kernel sends a SIGIO signal to your process. Your signal handler runs, reads the data, and your main loop is never blocked.

This is especially powerful when you want your program to be responsive to input without burning CPU in a polling loop.

📈 How Signal-Driven I/O Works

Three actors are involved: your program, the kernel, and the file descriptor (e.g. stdin, socket).

Step Who Does It What Happens System Call / Flag
1 Your Program Install SIGIO signal handler sigaction(SIGIO, ...)
2 Your Program Tell kernel which process to signal fcntl(fd, F_SETOWN, getpid())
3 Your Program Enable async signaling on fd + make non-blocking fcntl(fd, F_SETFL, O_ASYNC | O_NONBLOCK)
4 Kernel Monitors the fd for I/O events internally (automatic)
5 Kernel When I/O event happens, sends SIGIO to your process (automatic)
6 Your Program SIGIO handler runs, reads available data read() inside handler or sets a flag

Program sets up SIGIO handler
Sets F_SETOWN + O_ASYNC
Does other work freely
Kernel fires SIGIO
Handler reads data

⚠️ Critical: Order of Setup Matters!

You must install the SIGIO handler before enabling signal-driven I/O on the file descriptor. Here is why:

Order What Happens Risk
Handler first, then O_ASYNC SIGIO always has a handler ready Safe ✓
O_ASYNC first, then handler Time window with no handler. Default SIGIO action = terminate process Dangerous ✗

Note: On some UNIX systems, the default action for SIGIO is to ignore it (not terminate). But on Linux the default is termination, so always set the handler first.

Key Terms in This Topic

SIGIO O_ASYNC O_NONBLOCK F_SETOWN F_SETFL F_GETFL sigaction() SA_RESTART signal-driven I/O async I/O gotSigio flag cbreak mode

📄 Full Demo: Signal-Driven I/O on stdin

This example demonstrates signal-driven I/O on standard input (terminal). The main loop does heavy computation, but whenever a key is pressed the SIGIO handler sets a flag. The main loop checks the flag and reads the character. Pressing # exits the program.

The terminal is placed in cbreak mode so each character is available immediately without waiting for Enter to be pressed.

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

/* Global flag set by SIGIO handler */
static volatile sig_atomic_t gotSigio = 0;

/* SIGIO signal handler — just sets a flag, does no real work */
static void sigioHandler(int sig)
{
    gotSigio = 1;
}

int main(void)
{
    struct sigaction sa;
    int flags;
    char ch;
    int done, cnt, j;
    struct termios origTermios;

    /* ---- STEP 1: Install SIGIO handler FIRST ---- */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags   = SA_RESTART;      /* restart syscalls if interrupted */
    sa.sa_handler = sigioHandler;
    if (sigaction(SIGIO, &sa, NULL) == -1) {
        perror("sigaction"); exit(EXIT_FAILURE);
    }

    /* ---- STEP 2: Tell kernel to signal THIS process ---- */
    if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) {
        perror("fcntl F_SETOWN"); exit(EXIT_FAILURE);
    }

    /* ---- STEP 3: Enable O_ASYNC + O_NONBLOCK on stdin ---- */
    flags = fcntl(STDIN_FILENO, F_GETFL);
    if (flags == -1) { perror("fcntl F_GETFL"); exit(EXIT_FAILURE); }

    if (fcntl(STDIN_FILENO, F_SETFL, flags | O_ASYNC | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL"); exit(EXIT_FAILURE);
    }

    /* ---- STEP 4: Place terminal in cbreak mode ----
       In cbreak mode each key press is delivered immediately.
       origTermios saves the original terminal settings for restore later. */
    /* ttySetCbreak() is a helper from the TLPI library —
       see Chapter 62 for its implementation */
    if (ttySetCbreak(STDIN_FILENO, &origTermios) == -1) {
        perror("ttySetCbreak"); exit(EXIT_FAILURE);
    }

    /* ---- STEP 5: Main loop — does "work" and checks flag ---- */
    for (done = 0, cnt = 0; !done; cnt++) {

        /* Simulate some CPU work */
        for (j = 0; j < 100000000; j++)
            continue;

        /* Check flag set by SIGIO handler */
        if (gotSigio) {
            /*
             * Read ALL available input characters.
             * O_NONBLOCK makes read() return EAGAIN when no more data.
             * We stop when: EAGAIN (no more data), EOF, or '#' seen.
             */
            while (read(STDIN_FILENO, &ch, 1) > 0 && !done) {
                printf("cnt=%d; read '%c'\n", cnt, ch);
                done = (ch == '#');  /* '#' is our exit character */
            }
            gotSigio = 0;  /* reset flag */
        }
    }

    /* ---- STEP 6: Restore original terminal settings ---- */
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &origTermios) == -1) {
        perror("tcsetattr"); exit(EXIT_FAILURE);
    }

    printf("Exiting after %d loop iterations\n", cnt);
    exit(EXIT_SUCCESS);
}

🔍 Code Breakdown — What Each Part Does
Code / Call Purpose Why It Matters
volatile sig_atomic_t gotSigio Flag shared between handler and main loop sig_atomic_t guarantees atomic read/write across signal boundary
SA_RESTART Auto-restart interrupted system calls Without this, calls like read() return EINTR when SIGIO fires
F_SETOWN, getpid() Set this process as the owner of the fd Kernel needs to know which process to send SIGIO to
O_ASYNC Enable signal generation on I/O events Without this, no SIGIO is ever sent
O_NONBLOCK Make read() non-blocking Inside handler we drain all data; EAGAIN tells us no more data waiting
ttySetCbreak() Put terminal in single-character mode Without cbreak, terminal line-buffers input — SIGIO fires only after Enter
gotSigio = 0 after reading Reset the flag Prevents re-reading on next loop iteration when no new input arrived
tcsetattr(TCSAFLUSH) Restore terminal to original settings Must always restore terminal on exit or the shell becomes unusable

⚖️ Signal-Driven I/O vs Busy Polling
❌ Busy Polling (Bad)
while (1) {
    ret = read(fd, buf, 1);
    if (ret == -1 && errno == EAGAIN)
        continue; /* wasted CPU */
    if (ret > 0)
        process(buf);
}

Wastes 100% CPU in a tight loop asking kernel repeatedly if data is ready.

✔ Signal-Driven (Better)
/* In signal handler */
void handler(int sig) {
    gotSigio = 1;
}
/* Main loop does real work */
while (!done) {
    do_useful_work();
    if (gotSigio) {
        read_data(); /* only when needed */
        gotSigio = 0;
    }
}

CPU is free for real work. Kernel interrupts you only when data is actually ready.

🏫 Interview Questions — Signal-Driven I/O Setup
Q1. What is signal-driven I/O and how is it different from non-blocking I/O?
In non-blocking I/O, your program continuously polls the fd using read() and checks for EAGAIN. In signal-driven I/O, the kernel sends SIGIO to your process when data is ready. Your program does not poll at all — it gets notified only when there is something to read. This makes signal-driven I/O more CPU-efficient than polling.
Q2. What are the three setup steps required to use signal-driven I/O on a file descriptor?
1. Install a SIGIO signal handler using sigaction().
2. Set the fd owner using fcntl(fd, F_SETOWN, getpid()).
3. Enable O_ASYNC (and usually O_NONBLOCK) on the fd using fcntl(fd, F_SETFL, ...).
Q3. Why must the SIGIO handler be installed BEFORE enabling O_ASYNC on the fd?
The default action for SIGIO is to terminate the process. If you enable O_ASYNC first, there is a race condition: if I/O becomes available before the handler is installed, the kernel sends SIGIO and the default action kills your process. Always install the handler first to close this window.
Q4. Why is the gotSigio flag declared as volatile sig_atomic_t?
sig_atomic_t guarantees the flag can be read and written atomically — the compiler and hardware cannot split the operation. volatile tells the compiler not to cache the value in a register; the main loop must always re-read it from memory because it could change at any time (when the signal handler fires).
Q5. Why do we need both O_ASYNC and O_NONBLOCK?
O_ASYNC causes the kernel to send SIGIO when I/O is possible. O_NONBLOCK makes read() return immediately with EAGAIN when no more data is available. Without O_NONBLOCK, draining all available data inside the signal handler would block on the last read() call waiting for more input that may never come.
Q6. What is SA_RESTART and why is it useful here?
SA_RESTART tells the kernel to automatically restart slow system calls (like read(), write(), select()) that were interrupted by a signal delivery. Without it, those calls return -1 with errno set to EINTR, and your code must manually check for EINTR and retry — adding boilerplate everywhere.
Q7. Why is cbreak mode needed for the demo program?
By default, terminals operate in canonical (line-buffered) mode — input is delivered to the application only after the user presses Enter. In cbreak mode, each character is made available immediately as it is typed. Without cbreak mode, SIGIO would only fire after the user pressed Enter, which defeats the purpose of the demo showing per-keystroke detection.
Q8. What is the purpose of resetting gotSigio = 0 after reading data?
After reading all available data (draining the input until EAGAIN), we reset the flag so the main loop does not try to read again on the next iteration — there is no new data to read until the kernel signals us again. The flag will be set to 1 again by the signal handler when new data arrives.

Next: Setting the File Descriptor Owner

Learn about F_SETOWN, F_GETOWN, process groups, and the glibc limitation with small group IDs.

Next Topic → EmbeddedPathashala Home

Leave a Reply

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