Normally when you want to know if a file descriptor is ready for reading or writing, you call select() or poll() and your program blocks waiting for an event. Signal-driven I/O flips this idea โ instead of your program waiting, the kernel sends you a signal the moment a file descriptor becomes ready.
By default the kernel sends SIGIO for this. But SIGIO has a serious problem: it is a standard non-queuing signal. If two I/O events happen at almost the same time, only one SIGIO is delivered and the second is silently lost.
The solution is F_SETSIG โ a fcntl() command that lets you replace SIGIO with a realtime signal. Realtime signals are queued, so no event is lost.
When you enable signal-driven I/O using fcntl(fd, F_SETFL, O_ASYNC), the kernel notifies you of I/O events via SIGIO. But SIGIO is a standard signal โ it is NOT queued. Here is what happens when two events arrive quickly:
F_SETSIG is a fcntl() command that replaces the default SIGIO signal with any signal you choose โ typically a realtime signal like SIGRTMIN.
โ ๏ธ You must define _GNU_SOURCE before including <fcntl.h> to get the F_SETSIG and F_GETSIG constants.
#define _GNU_SOURCE /* Required for F_SETSIG, F_GETSIG */
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
/* Step 1: Set the owner (this process) to receive the signal */
fcntl(fd, F_SETOWN, getpid());
/* Step 2: Enable async (signal-driven) mode on the fd */
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC);
/* Step 3: Replace SIGIO with a realtime signal */
fcntl(fd, F_SETSIG, SIGRTMIN);
/* To read back which signal is configured */
int sig = fcntl(fd, F_GETSIG, 0);
printf("Configured signal: %d\n", sig);
/* To go back to default SIGIO behavior, pass 0 */
fcntl(fd, F_SETSIG, 0); /* reverts to SIGIO */
Once you set a realtime signal with F_SETSIG, the kernel queues each I/O notification. No event is dropped even if your handler is busy processing a previous one.
There is a second advantage of F_SETSIG beyond queuing: when you install the handler using sigaction() with the SA_SIGINFO flag, the kernel passes a siginfo_t structure to your handler. This tells you:
- Which file descriptor triggered the event (
si_fd) - What type of event it was (
si_code/si_band)
โ ๏ธ Both F_SETSIG AND SA_SIGINFO must be used together for siginfo_t to be populated correctly.
| Field | Meaning |
|---|---|
| si_signo | Signal number that triggered the handler (same as first handler arg) |
| si_fd | The file descriptor on which the I/O event occurred |
| si_code | Type of I/O event (POLL_IN, POLL_OUT, POLL_ERR, etc.) |
| si_band | Bitmask matching poll() revents (POLLIN, POLLOUT, POLLERR, etc.) |
| si_code | si_band value | Meaning |
|---|---|---|
| POLL_IN | POLLIN | POLLRDNORM | Input data available or end-of-file |
| POLL_OUT | POLLOUT | POLLWRNORM | POLLWRBAND | Output buffer has space โ can write |
| POLL_MSG | POLLIN | POLLRDNORM | POLLMSG | Input message available (rarely used) |
| POLL_ERR | POLLERR | I/O error on the file descriptor |
| POLL_PRI | POLLPRI | POLLRDNORM | High-priority input (e.g. TCP OOB data) |
| POLL_HUP | POLLHUP | POLLERR | Hangup โ peer closed connection |
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
/* Signal handler โ receives siginfo_t when SA_SIGINFO is used */
static void io_handler(int sig, siginfo_t *si, void *ucontext)
{
printf("Signal %d received\n", sig);
printf(" fd that triggered event : %d\n", si->si_fd);
/* si_code tells us what happened */
switch (si->si_code) {
case POLL_IN:
printf(" Event: data available to read (POLL_IN)\n");
break;
case POLL_OUT:
printf(" Event: ready to write (POLL_OUT)\n");
break;
case POLL_HUP:
printf(" Event: hangup (POLL_HUP)\n");
break;
case POLL_ERR:
printf(" Event: I/O error (POLL_ERR)\n");
break;
default:
printf(" Event: si_code=%d si_band=%ld\n",
si->si_code, si->si_band);
break;
}
}
int setup_signal_driven_io(int fd)
{
struct sigaction sa;
/* Install handler with SA_SIGINFO so siginfo_t is populated */
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = io_handler; /* use sa_sigaction, not sa_handler */
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGRTMIN, &sa, NULL) == -1) {
perror("sigaction");
return -1;
}
/* This process receives the signal */
if (fcntl(fd, F_SETOWN, getpid()) == -1) {
perror("F_SETOWN");
return -1;
}
/* Enable async / signal-driven mode */
int flags = fcntl(fd, F_GETFL);
if (fcntl(fd, F_SETFL, flags | O_ASYNC) == -1) {
perror("F_SETFL O_ASYNC");
return -1;
}
/* Replace SIGIO with realtime signal SIGRTMIN */
if (fcntl(fd, F_SETSIG, SIGRTMIN) == -1) {
perror("F_SETSIG");
return -1;
}
return 0;
}
int main(void)
{
/* Use STDIN (fd=0) as an example */
if (setup_signal_driven_io(STDIN_FILENO) == -1)
return 1;
printf("Waiting for I/O events on stdin. Type something and press Enter.\n");
printf("Press Ctrl+C to quit.\n");
/* Main loop โ do other work here; I/O events arrive as signals */
for (;;) {
pause(); /* sleep until a signal arrives */
}
return 0;
}
How to compile and test:
gcc -o sig_io sig_io.c
./sig_io
# In another terminal: echo "hello" > /proc/$(pgrep sig_io)/fd/0
# Or just type in the same terminal
Instead of using an async signal handler, you can block the realtime signal and call sigwaitinfo() to synchronously wait for the next queued event. This gives you all the information of siginfo_t without writing an async-signal-safe handler.
#define _GNU_SOURCE
#include <signal.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int fd = STDIN_FILENO;
sigset_t mask;
/* Block SIGRTMIN so it queues instead of being handled */
sigemptyset(&mask);
sigaddset(&mask, SIGRTMIN);
sigprocmask(SIG_BLOCK, &mask, NULL);
/* Setup async I/O on fd */
fcntl(fd, F_SETOWN, getpid());
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC);
fcntl(fd, F_SETSIG, SIGRTMIN);
printf("Synchronously waiting for I/O...\n");
for (;;) {
siginfo_t si;
/* Block here until SIGRTMIN arrives โ returns siginfo_t */
int sig = sigwaitinfo(&mask, &si);
if (sig == -1) {
perror("sigwaitinfo");
break;
}
printf("I/O event on fd %d, si_code=%d\n", si.si_fd, si.si_code);
if (si.si_code == POLL_IN) {
char buf[256];
ssize_t n = read(si.si_fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("Read: %s", buf);
}
}
}
return 0;
}
This approach is cleaner โ no signal handler needed. The program still responds to I/O events efficiently but runs in a predictable sequential flow.
fcntl(fd, F_SETSIG, realtime_signal) to set a realtime signal for I/O notifications. (2) Install the handler using sigaction() with the SA_SIGINFO flag set in sa_flags. Both are required together.si_fd is the file descriptor on which the I/O event occurred. si_code indicates the type of event โ e.g. POLL_IN (data available), POLL_OUT (can write), POLL_HUP (hangup), POLL_ERR (error). This lets one signal handler manage many file descriptors without doing extra select/poll calls._GNU_SOURCE must be defined before including <fcntl.h>. These constants are GNU/Linux extensions not in POSIX.Learn what happens when too many realtime signals pile up and how to handle it safely.
