Why Refine Signal-Driven I/O?
Basic signal-driven I/O using SIGIO works, but it has a limitation when you monitor many file descriptors at once: when SIGIO fires, you do not know which fd triggered it. You have to scan all your file descriptors to find which one has data.
The refined approach uses realtime signals together with SA_SIGINFO to have the kernel tell you exactly which fd caused the signal, in a queued, ordered manner. This is the foundation for extremely high-performance I/O notification in servers handling thousands of connections.
Consider a server that monitors 5000 socket file descriptors with signal-driven I/O, all using plain SIGIO:
| Problem | Details |
|---|---|
| No fd identity in SIGIO | SIGIO handler receives signal number only. No way to know which of 5000 fds triggered it. |
| Signal coalescing | Standard signals (like SIGIO) are not queued. If 100 fds become ready simultaneously, only one SIGIO may be delivered. |
| Fallback needed | When the realtime signal queue overflows, the kernel falls back to SIGIO — which you still need to handle as a catch-all. |
To take full advantage of signal-driven I/O with fd-specific notification, do two things:
fcntl(fd, F_SETSIG, sig) to specify a realtime signal (e.g., SIGRTMIN+1) to be delivered instead of SIGIO when I/O is possible.SA_SIGINFO flag in sigaction() so the handler receives a siginfo_t struct containing the fd number and event type.The F_SETSIG fcntl operation tells the kernel which signal to send instead of SIGIO when I/O becomes possible on that specific fd:
/* Specify SIGRTMIN+1 instead of SIGIO for fd */
if (fcntl(fd, F_SETSIG, SIGRTMIN + 1) == -1) {
perror("fcntl F_SETSIG");
exit(1);
}
You can read back the currently configured signal with F_GETSIG:
int sig = fcntl(fd, F_GETSIG);
if (sig == -1) {
perror("fcntl F_GETSIG");
}
printf("Signal for fd: %d (SIGRTMIN = %d)\n", sig, SIGRTMIN);
| F_SETSIG value | Behavior |
|---|---|
| 0 (zero) | Reset to default: SIGIO is sent (standard behavior) |
| SIGIO | Same as default, but with SA_SIGINFO, siginfo_t is populated |
| SIGRTMIN to SIGRTMAX | Realtime signal used — signals are queued, fd info included in siginfo_t |
When you install a handler with SA_SIGINFO, the handler receives a siginfo_t structure as its second argument. For I/O signals, this structure tells you which fd triggered the event and what kind of event it was.
#include <signal.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
/* Handler that receives full siginfo_t */
static void rtSigioHandler(int sig, siginfo_t *si, void *ucontext)
{
/*
* si_fd — the file descriptor that caused the event
* si_band — a bitmask of events (like POLLIN, POLLOUT, POLLERR)
* uses the same bits as poll()
*/
printf("Signal %d on fd %d, events: %ld\n",
sig, si->si_fd, si->si_band);
if (si->si_band & POLLIN)
printf(" Data available to read on fd %d\n", si->si_fd);
if (si->si_band & POLLOUT)
printf(" Space available to write on fd %d\n", si->si_fd);
if (si->si_band & POLLERR)
printf(" Error condition on fd %d\n", si->si_fd);
if (si->si_band & POLLHUP)
printf(" Hangup on fd %d\n", si->si_fd);
}
void setup_realtime_sigio(int fd, int rtsig)
{
struct sigaction sa;
/* Use SA_SIGINFO to get the extended handler with siginfo_t */
sa.sa_sigaction = rtSigioHandler; /* note: sa_sigaction, not sa_handler */
sa.sa_flags = SA_RESTART | SA_SIGINFO;
sigemptyset(&sa.sa_mask);
if (sigaction(rtsig, &sa, NULL) == -1) {
perror("sigaction"); return;
}
/* Set owner */
fcntl(fd, F_SETOWN, getpid());
/* Specify the realtime signal instead of SIGIO */
fcntl(fd, F_SETSIG, rtsig);
/* Enable async + nonblocking */
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
}
| siginfo_t field | Type | Meaning for I/O signals |
|---|---|---|
si_signo |
int | Signal number (same as first handler argument) |
si_fd |
int | File descriptor that triggered the event |
si_band |
long | Bitmask of events — POLLIN, POLLOUT, POLLERR, POLLHUP (same bits as poll()) |
si_code |
int | POLL_IN, POLL_OUT, POLL_ERR, etc. — coarser event type |
Signal-driven I/O with realtime signals scales better than select() and poll() for very large numbers of file descriptors.
| Mechanism | How Kernel Tracks fds | Performance Scales With | Best For |
|---|---|---|---|
select() |
App passes entire fd set each call | Number of fds monitored | Small fd sets (< 1024) |
poll() |
App passes array each call | Number of fds monitored | Medium fd sets |
| Signal-driven I/O | Kernel remembers fd list — no app re-registration | Number of I/O events that actually occur | Thousands of mostly-idle fds |
epoll |
Kernel remembers fd list | Number of I/O events | Thousands of fds (preferred modern approach) |
select() or poll(), performance degrades linearly as the number of monitored fds grows — even if only 1 fd out of 5000 is ever active. With signal-driven I/O (and epoll), the kernel signals you only when something happens, so idle fds cost essentially nothing.This example sets up signal-driven I/O on two sockets using different realtime signals so each fd’s events are handled independently.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <fcntl.h>
#include <poll.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define SIG_SOCK1 (SIGRTMIN + 1)
#define SIG_SOCK2 (SIGRTMIN + 2)
static int sock1_fd = -1;
static int sock2_fd = -1;
/* Handler receives which fd triggered and which events are ready */
static void ioReadyHandler(int sig, siginfo_t *si, void *ucontext)
{
char buf[256];
ssize_t n;
printf("[signal %d] fd=%d events=0x%lx\n",
sig, si->si_fd, (unsigned long)si->si_band);
if (!(si->si_band & POLLIN))
return; /* Only handle readable events in this example */
n = read(si->si_fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf(" Read %zd bytes from fd %d: %s\n", n, si->si_fd, buf);
}
}
static void install_rt_sigio(int fd, int rtsig)
{
struct sigaction sa;
int flags;
/* Install SA_SIGINFO handler */
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = ioReadyHandler;
sa.sa_flags = SA_RESTART | SA_SIGINFO;
sigemptyset(&sa.sa_mask);
if (sigaction(rtsig, &sa, NULL) == -1) {
perror("sigaction"); exit(1);
}
/* Set owner to this process */
if (fcntl(fd, F_SETOWN, getpid()) == -1) {
perror("F_SETOWN"); exit(1);
}
/* Configure the realtime signal for this fd */
if (fcntl(fd, F_SETSIG, rtsig) == -1) {
perror("F_SETSIG"); exit(1);
}
/* Enable async I/O + nonblocking */
flags = fcntl(fd, F_GETFL);
if (fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK) == -1) {
perror("F_SETFL"); exit(1);
}
printf("fd %d configured with signal %d (SIGRTMIN+%d)\n",
fd, rtsig, rtsig - SIGRTMIN);
}
static int make_udp_socket(int port)
{
int sfd;
struct sockaddr_in addr;
const int one = 1;
sfd = socket(AF_INET, SOCK_DGRAM, 0);
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
return sfd;
}
int main(void)
{
/* Create two UDP sockets on different ports */
sock1_fd = make_udp_socket(9001);
sock2_fd = make_udp_socket(9002);
/* Install different realtime signals for each fd */
install_rt_sigio(sock1_fd, SIG_SOCK1);
install_rt_sigio(sock2_fd, SIG_SOCK2);
printf("Listening on UDP ports 9001 (signal %d) and 9002 (signal %d)\n",
SIG_SOCK1, SIG_SOCK2);
printf("Send: echo 'hello' | nc -u 127.0.0.1 9001\n\n");
/* Main loop: pause waiting for signals */
while (1) {
pause(); /* Wait for any signal */
}
return 0;
}
With this setup, data on port 9001 triggers SIG_SOCK1 and data on port 9002 triggers SIG_SOCK2. The handler knows exactly which fd to read from via si->si_fd, with no scanning needed.
The realtime signal queue has a limited capacity (check with ulimit -i or /proc/sys/kernel/rtsig-max on older kernels). When the queue is full, the kernel falls back to sending SIGIO (the standard signal).
select() or poll() to find which fds are ready — you cannot use the realtime signal queue anymore because it overflowed./* Fallback SIGIO handler — called when realtime queue overflows */
static void sigioFallbackHandler(int sig)
{
/*
* We cannot tell which fd is ready from plain SIGIO.
* Use poll() or select() to discover which fds have data.
* Then process them, and the realtime signal queue
* will drain back to normal on its own.
*/
printf("SIGIO fallback — realtime queue may have overflowed\n");
poll_all_fds(); /* your function to poll and process all registered fds */
}
/* Install the fallback alongside your realtime signal handlers */
void install_fallback(void)
{
struct sigaction sa;
sa.sa_handler = sigioFallbackHandler;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGIO, &sa, NULL);
}
| Feature | Plain SIGIO | Realtime Signal + SA_SIGINFO |
|---|---|---|
| Signal type | Standard signal | Realtime signal (SIGRTMIN to SIGRTMAX) |
| Queued? | No — multiple instances collapse | Yes — each event queued separately |
| fd in handler? | No — must scan all fds | Yes — si_fd tells you exactly which fd |
| Event type in handler? | No | Yes — si_band (POLLIN, POLLOUT, etc.) |
| Setup call | Only O_ASYNC + F_SETOWN |
O_ASYNC + F_SETOWN + F_SETSIG |
| Handler type | sa_handler |
sa_sigaction with SA_SIGINFO |
| Queue overflow? | N/A | Falls back to SIGIO — must handle as fallback |
| Linux-specific? | No — POSIX | Yes — F_SETSIG is Linux-specific |
fcntl(fd, F_SETSIG, SIGRTMIN+N) to assign a realtime signal to the fd instead of SIGIO.2. Install the signal handler with
SA_SIGINFO flag and use sa_sigaction (not sa_handler) — this gives the handler access to siginfo_t containing si_fd (which fd fired) and si_band (which events fired).si_band is a bitmask of I/O events using the same bit definitions as poll(): POLLIN (input data ready), POLLOUT (output buffer has space), POLLERR (error condition), POLLHUP (hangup/peer closed), etc. This lets you determine the exact nature of the I/O event without making any additional system calls.select() or poll(), the application passes the entire list of monitored fds to the kernel on every call, and the kernel scans all of them. Performance degrades linearly with the number of fds, regardless of how many are actually active. With signal-driven I/O, the kernel remembers the fd list and signals the application only when an event actually occurs. Performance scales with the number of events, not the total number of fds being watched. An idle fd costs nothing.poll() or select() to scan all registered fds and process any that are ready. The realtime queue will drain as your handlers run, and normal realtime signal delivery will resume.sa_handler is the traditional signal handler taking only the signal number: void handler(int sig). sa_sigaction is the extended handler taking three arguments: void handler(int sig, siginfo_t *info, void *context). You must use sa_sigaction (and set SA_SIGINFO in sa_flags) whenever you need the extra information in siginfo_t — which includes si_fd and si_band for I/O signals.F_SETSIG to 0 resets the fd back to the default behavior: the kernel sends plain SIGIO when I/O is possible. It is equivalent to not having called F_SETSIG at all. If you also install the signal handler with SA_SIGINFO at the time of delivery, the siginfo_t fields are still populated even for SIGIO.You have now covered the full signal-driven I/O section: setup, fd ownership, trigger conditions per fd type, and high-performance refinement with realtime signals.
