Signal Queue Overflow in Signal-Driven I/O

Signal Queue Overflow in Signal-Driven I/O
Chapter 63 โ€“ Alternative I/O Models | Part 2 of 5
โš ๏ธ Topic: Queue Overflow
๐Ÿ”‘ Key: SIGIO fallback
๐ŸŽฏ Level: Intermediate

Why Overflow Happens

In Part 1 we saw that using F_SETSIG with a realtime signal solves the signal-loss problem โ€” realtime signals are queued. But the queue is not infinite. The Linux kernel limits how many realtime signals can be pending for a process at one time.

When that limit is hit and a new I/O event arrives, the kernel cannot queue another realtime signal. Instead it falls back to delivering the default SIGIO. This is the overflow notification โ€” it tells your program “something happened but I lost track of exactly what.”

If your application does not handle this case, it will silently miss I/O events under high load.

Keywords in this tutorial:

Signal Queue Limit SIGIO Fallback Queue Overflow /proc/sys/kernel/rtsig-max sigwaitinfo() RLIMIT_SIGPENDING select() poll()

๐Ÿšจ What Happens When the Queue Overflows

There is a per-user limit on the number of queued realtime signals. You can check it at:

# Check the realtime signal queue limit for your user
cat /proc/sys/kernel/rtsig-max     # older kernels
ulimit -i                          # RLIMIT_SIGPENDING (current user)

# Check current pending count (Linux specific)
cat /proc/$(pgrep your_process)/status | grep SigQ

When the limit is reached:

๐Ÿ“Š Signal Queue Overflow โ€” What the Kernel Does
I/O event occurs โ†’ kernel tries to queue SIGRTMIN
โฌ‡
Queue is FULL โ†’ cannot add another realtime signal
โฌ‡
Kernel falls back โ†’ delivers old SIGIO instead
โฌ‡
SIGIO handler fires โ€” but has NO siginfo_t, NO si_fd info!
We know overflow happened but don’t know which FD or event type

This is a serious problem: the SIGIO fallback handler cannot determine which file descriptors have pending I/O. The only option is to drain the realtime signal queue and then use select() or poll() to figure out what is ready.

๐Ÿ›ก๏ธ Correctly Handling Signal Queue Overflow

A correctly written application using F_SETSIG must install a separate SIGIO handler as a fallback. The strategy is:

  1. Drain all queued realtime signals using sigwaitinfo() (non-blocking loop)
  2. Temporarily use select() or poll() to get a complete picture
  3. Process all ready FDs, then resume normal realtime-signal monitoring

๐Ÿ“Š Overflow Recovery Strategy
1๏ธโƒฃ
SIGIO arrives
Queue overflow detected
2๏ธโƒฃ
Drain queue
sigwaitinfo() loop with WNOHANG-style
3๏ธโƒฃ
Use poll()
Find all ready FDs
4๏ธโƒฃ
Resume
Back to realtime signal mode
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include <errno.h>

#define MAX_FDS 64

/* Global list of monitored file descriptors */
static int watched_fds[MAX_FDS];
static int nfds = 0;

/* Called when the realtime signal queue overflows and SIGIO is delivered */
static void sigio_overflow_handler(int sig)
{
    sigset_t rt_mask;
    siginfo_t si;
    struct timespec zero = {0, 0};

    printf("WARNING: realtime signal queue overflowed! Falling back to poll()\n");

    /* Step 1: Drain whatever is left in the realtime signal queue */
    sigemptyset(&rt_mask);
    sigaddset(&rt_mask, SIGRTMIN);
    while (sigtimedwait(&rt_mask, &si, &zero) > 0)
        ; /* discard โ€” we will use poll() to get fresh info */

    /* Step 2: Use poll() to find ALL ready file descriptors */
    struct pollfd pfds[MAX_FDS];
    for (int i = 0; i < nfds; i++) {
        pfds[i].fd      = watched_fds[i];
        pfds[i].events  = POLLIN | POLLOUT;
        pfds[i].revents = 0;
    }

    int ready = poll(pfds, nfds, 0);  /* timeout=0: non-blocking */
    if (ready <= 0)
        return;

    /* Step 3: Process each ready descriptor */
    for (int i = 0; i < nfds; i++) {
        if (pfds[i].revents & POLLIN)
            printf("  fd %d is readable\n", pfds[i].fd);
        if (pfds[i].revents & POLLOUT)
            printf("  fd %d is writable\n", pfds[i].fd);
        if (pfds[i].revents & (POLLERR | POLLHUP))
            printf("  fd %d has error/hangup\n", pfds[i].fd);
    }
}

/* Normal realtime signal handler for I/O events */
static void rt_io_handler(int sig, siginfo_t *si, void *ctx)
{
    printf("RT signal: fd=%d  si_code=%d\n", si->si_fd, si->si_code);
}

int main(void)
{
    struct sigaction sa;

    /* Install SIGIO handler for overflow recovery */
    sa.sa_handler = sigio_overflow_handler;
    sa.sa_flags   = 0;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGIO, &sa, NULL);

    /* Install realtime signal handler with SA_SIGINFO */
    sa.sa_sigaction = rt_io_handler;
    sa.sa_flags     = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGRTMIN, &sa, NULL);

    /* Watch stdin */
    watched_fds[0] = STDIN_FILENO;
    nfds = 1;

    /* Setup async I/O */
    fcntl(STDIN_FILENO, F_SETOWN, getpid());
    int fl = fcntl(STDIN_FILENO, F_GETFL);
    fcntl(STDIN_FILENO, F_SETFL, fl | O_ASYNC);
    fcntl(STDIN_FILENO, F_SETSIG, SIGRTMIN);

    printf("Monitoring stdin. Type to trigger I/O events.\n");
    for (;;)
        pause();

    return 0;
}

๐Ÿ”ง Increasing the Realtime Signal Queue Limit

You can reduce the chance of overflow by raising the limit, but you cannot eliminate the need for overflow handling โ€” the limit always exists.

# Check current limit (signals pending allowed per user)
ulimit -i

# Temporarily raise to 8192 for this shell session
ulimit -i 8192

# Permanently raise via /etc/security/limits.conf
# Add this line:
#   yourusername  soft  sigpending  8192
#   yourusername  hard  sigpending  8192

# Check via /proc for a running process
cat /proc/$(pgrep myapp)/status | grep SigQ
# Output example:
# SigQ: 23/7637    (23 pending out of max 7637)

โš ๏ธ Raising the limit is useful but not enough on its own. Always write the SIGIO fallback handler. Under extreme load (many monitored FDs with bursts of events), overflow can still happen.

๐ŸŽฏ Interview Questions
Q1. What happens when the realtime signal queue fills up during signal-driven I/O?
The kernel cannot queue another realtime signal, so it falls back to delivering a plain SIGIO signal. This tells the process a queue overflow occurred. The SIGIO delivery does NOT include siginfo_t details (no si_fd, no si_code), so the process loses track of which file descriptors have pending I/O events.
Q2. How should an application recover from a signal queue overflow?
Install a SIGIO handler as a fallback. When SIGIO arrives (indicating overflow), drain the remaining queued realtime signals using sigtimedwait() with a zero timeout, then call select() or poll() with a zero timeout to get a fresh complete snapshot of all ready file descriptors. Process all ready FDs, then resume normal operation.
Q3. Why is it not enough to just increase the realtime signal queue limit to solve overflow?
The limit is per-user and bounded by system resources. Under heavy load with many monitored file descriptors generating bursts of events, even a large queue can fill up. Applications must always handle the SIGIO fallback for correctness.
Q4. Why does the SIGIO overflow handler call sigtimedwait() before calling poll()?
To drain any remaining realtime signals from the queue. If we don’t drain first, those queued signals would fire after we finish poll(), causing duplicate or confusing processing. Draining ensures we get a clean starting state before the authoritative poll() scan.
Q5. Which Linux command or file lets you check how many signals are currently queued for a process?
/proc/PID/status contains a SigQ field showing “current_queued/max_allowed” โ€” for example “23/7637”. You can also check the limit with ulimit -i.

Next: Signal-Driven I/O in Multithreaded Apps โ†’

Learn F_SETOWN_EX and how to direct I/O signals to a specific thread.

Part 3: Multithreaded Signal-Driven I/O โ† Part 1

Leave a Reply

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