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