Refining Signal-Driven I/O F_SETSIG · SA_SIGINFO · Realtime Signals

 

Refining Signal-Driven I/O
F_SETSIG · SA_SIGINFO · Realtime Signals · High-Performance Servers | Chapter 63 · TLPI
🔧 F_SETSIG
🔷 F_GETSIG
⚡ SA_SIGINFO
📈 Scalable I/O

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.

⚠️ Problem with Plain SIGIO on Multiple fds

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.

🔧 Two Steps to Refined Signal-Driven I/O

To take full advantage of signal-driven I/O with fd-specific notification, do two things:

Step 1
Use F_SETSIG
Use 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.
Step 2
Use SA_SIGINFO
Use SA_SIGINFO flag in sigaction() so the handler receives a siginfo_t struct containing the fd number and event type.
fd gets F_SETSIG = SIGRTMIN+1
I/O event occurs
Kernel queues SIGRTMIN+1 with si_fd
SA_SIGINFO handler reads si_fd

🔷 F_SETSIG — Changing the Signal from SIGIO to a Realtime Signal

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
💡 Why a Realtime Signal? Realtime signals (SIGRTMIN to SIGRTMAX) are queued — if 10 fds become ready simultaneously, 10 separate signal instances are queued and delivered one by one. Standard signals like SIGIO are not queued — multiple pending instances collapse into one delivery.

⚡ SA_SIGINFO — Getting fd Info in the Signal Handler

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

📈 Performance Advantage at Scale

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)
Key insight: With 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.

📄 Complete Example — Multiple fds with Realtime Signals

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.

⚠️ Handling Realtime Signal Queue Overflow

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

Best practice: Always install a SIGIO handler as a fallback even when using F_SETSIG with realtime signals. In the SIGIO fallback handler, fall back to using 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);
}

⚖️ SIGIO vs Realtime Signal — Side-by-Side
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

Key Terms

F_SETSIG F_GETSIG SA_SIGINFO sa_sigaction siginfo_t si_fd si_band SIGRTMIN realtime signal signal queue overflow POLLIN POLLOUT scalable I/O

🏫 Interview Questions — Refining Signal-Driven I/O
Q1. What is the problem with using plain SIGIO when monitoring multiple file descriptors?
With plain SIGIO you cannot tell which fd triggered the signal — the handler only receives the signal number. If 100 fds are monitored and SIGIO fires, you must scan all 100 with poll/read to find the active ones. Also, SIGIO is not queued — if multiple fds become ready simultaneously, multiple SIGIO deliveries may collapse into one.
Q2. What two things must you do to get per-fd notification in signal-driven I/O?
1. Use 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).
Q3. What does si_band contain in the siginfo_t for I/O signals?
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.
Q4. Why do realtime signals offer better performance than standard signals for I/O notification?
Realtime signals are queued — each occurrence is individually queued even if the same signal fires multiple times before it is handled. Standard signals like SIGIO are not queued — if SIGIO fires 50 times before your handler runs, you still only see one delivery. With realtime signals, each I/O event on each fd results in exactly one queued signal, ensuring no events are missed.
Q5. Why does signal-driven I/O scale better than select() or poll() for thousands of fds?
With 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.
Q6. What happens when the realtime signal queue overflows? How do you handle it?
When the kernel’s realtime signal queue is full, it falls back to sending a plain SIGIO. This means some events that should have been individually queued may be collapsed. To handle this correctly: always install a plain SIGIO handler as a fallback. When this fallback fires, use 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.
Q7. What is the difference between sa_handler and sa_sigaction? When must you use sa_sigaction?
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.
Q8. If you set F_SETSIG to 0, what signal is delivered?
Setting 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.

Chapter 63 Signal-Driven I/O — Complete

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.

EmbeddedPathashala Home Back to Part 7 →

Leave a Reply

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