Overview
When you enable signal-driven I/O on a file descriptor, the kernel sends SIGIO (or a configured realtime signal) when “I/O is possible” on that fd. But exactly when does the kernel decide to send that signal? The answer depends on the type of file descriptor.
This page covers each fd type in detail: terminals, pseudoterminals, pipes, FIFOs, datagram sockets, stream sockets, and inotify file descriptors.
| fd Type | Signal on Read Ready? | Signal on Write Ready? | Other Events Signaled? |
|---|---|---|---|
| Terminal | Yes — on new input or EOF | No | Disconnect NOT signaled |
| Pseudoterminal (slave) | Yes — on new input | Yes (kernel 2.4.19+) | EOF NOT signaled |
| Pipe / FIFO (read end) | Yes — when data written to pipe | N/A | Write end closed |
| Pipe / FIFO (write end) | N/A | Yes — when free space ≥ PIPE_BUF | Read end closed |
| Datagram socket | Yes — each new datagram | Not mentioned | Async error on socket |
| Stream socket | Yes — new data or new connection | Yes — send buffer space | TCP connect complete, peer close, async error |
| inotify fd | Yes — when fd becomes readable | N/A | — |
Real Terminals (e.g., /dev/tty0, serial ports):
- SIGIO is sent whenever new input becomes available, even if previous input has not been read yet.
- SIGIO is also sent when an end-of-file (EOF) condition occurs on a terminal (e.g., user presses Ctrl+D).
- There is no “output possible” signaling for real terminals.
- A terminal disconnect is not signaled.
Pseudoterminals (PTY — slave side):
- Signal is generated when new input becomes available on the slave side.
- Starting with Linux kernel 2.4.19, “output possible” is also signaled on the slave side — this happens when input is consumed on the master side (freeing buffer space).
- EOF is not signaled on a pseudoterminal (unlike real terminals).
Example: monitoring stdin (a terminal) with signal-driven I/O:
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
static volatile sig_atomic_t inputReady = 0;
static void sigioHandler(int sig) { inputReady = 1; }
void setup_terminal_sigio(void)
{
struct sigaction sa;
int flags;
/* Handler first */
sa.sa_handler = sigioHandler;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGIO, &sa, NULL);
/* Set owner and enable async */
fcntl(STDIN_FILENO, F_SETOWN, getpid());
flags = fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
}
Signal-driven I/O works for both the read end and the write end of a pipe or FIFO, but the triggering conditions are different for each end.
Signal is generated when:
- Data is written to the pipe (even if previous unread data exists)
- The write end is closed
Signal is generated when:
- A
read()on the pipe increases free space ≥ PIPE_BUF bytes - The read end is closed
Example: Signal-driven I/O on the read end of a pipe:
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
static volatile sig_atomic_t pipeDataReady = 0;
static void sigioHandler(int sig) { pipeDataReady = 1; }
int main(void)
{
int pfd[2];
struct sigaction sa;
int flags;
char buf[256];
ssize_t n;
/* Create pipe */
if (pipe(pfd) == -1) { perror("pipe"); return 1; }
/* Set up SIGIO handler */
sa.sa_handler = sigioHandler;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGIO, &sa, NULL);
/* Make read end the owner and enable async + nonblocking */
fcntl(pfd[0], F_SETOWN, getpid());
flags = fcntl(pfd[0], F_GETFL);
fcntl(pfd[0], F_SETFL, flags | O_ASYNC | O_NONBLOCK);
/* Fork: child writes to pipe */
if (fork() == 0) {
close(pfd[0]);
/* Give parent time to set up */
sleep(1);
write(pfd[1], "Hello SIGIO\n", 12);
/* Writing to pipe triggers SIGIO on parent's read end */
close(pfd[1]);
/* Closing write end also triggers SIGIO on read end */
_exit(0);
}
close(pfd[1]); /* parent closes write end */
/* Main loop */
while (1) {
if (pipeDataReady) {
while ((n = read(pfd[0], buf, sizeof(buf)-1)) > 0) {
buf[n] = '\0';
printf("Read from pipe: %s", buf);
}
if (n == 0) { printf("Write end closed\n"); break; }
if (errno == EAGAIN) pipeDataReady = 0;
}
/* ... do other work ... */
}
return 0;
}
Note: When the write end is closed, the read end receives SIGIO. Reading from the pipe then returns 0 (EOF), telling you the pipe is done.
Signal-driven I/O works for both UNIX domain and Internet domain sockets.
Datagram Sockets (UDP, UNIX SOCK_DGRAM):
- An input datagram arrives on the socket (even if there are already unread datagrams queued)
- An asynchronous error occurs on the socket
Stream Sockets (TCP, UNIX SOCK_STREAM):
- A new connection arrives on a listening socket (server: accept() can now be called)
- A
connect()completes — TCP connection reaches ESTABLISHED state (active side). Note: not signaled for UNIX domain sockets in the same situation - New input arrives on the connected socket (even if previous data not yet read)
- The peer closes its write half via
shutdown()or closes the socket withclose() - Output space becomes available in the socket send buffer
- An asynchronous error occurs on the socket
| Socket Event | Datagram | TCP Stream | UNIX Stream |
|---|---|---|---|
| New data/datagram arrived | ✔ | ✔ | ✔ |
| New connection on listening socket | — | ✔ | ✔ |
| connect() completed (ESTABLISHED) | — | ✔ | ❌ |
| Peer closes (shutdown/close) | — | ✔ | ✔ |
| Output space available (send buffer) | — | ✔ | ✔ |
| Async error on socket | ✔ | ✔ | ✔ |
Example: Signal-driven I/O on a UDP server socket:
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>
static volatile sig_atomic_t udpDataReady = 0;
static void sigioHandler(int sig) { udpDataReady = 1; }
int create_udp_server(int port)
{
int sfd;
struct sockaddr_in addr;
struct sigaction sa;
int flags;
sfd = socket(AF_INET, SOCK_DGRAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
/* Install SIGIO handler */
sa.sa_handler = sigioHandler;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGIO, &sa, NULL);
/* Set owner and enable async + nonblocking */
fcntl(sfd, F_SETOWN, getpid());
flags = fcntl(sfd, F_GETFL);
fcntl(sfd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
return sfd;
}
void server_loop(int sfd)
{
char buf[512];
struct sockaddr_in client;
socklen_t clen;
ssize_t n;
while (1) {
/* Do other work ... */
if (udpDataReady) {
/* Drain all datagrams */
clen = sizeof(client);
while ((n = recvfrom(sfd, buf, sizeof(buf)-1, 0,
(struct sockaddr *)&client, &clen)) > 0) {
buf[n] = '\0';
printf("Received: %s\n", buf);
}
/* n == -1 with errno == EAGAIN means no more datagrams */
udpDataReady = 0;
}
}
}
An inotify file descriptor monitors filesystem events (file creation, modification, deletion, etc.) on files or directories you register with it. Signal-driven I/O can be used with inotify as well.
A SIGIO is generated when the inotify fd becomes readable — meaning at least one event is available to be read from it.
#include <sys/inotify.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>
static volatile sig_atomic_t inotifyEvent = 0;
static void sigioHandler(int sig) { inotifyEvent = 1; }
int main(void)
{
int ifd, wd;
struct sigaction sa;
int flags;
char evbuf[1024];
ssize_t n;
/* Create inotify instance */
ifd = inotify_init();
/* Watch /tmp for file creation and deletion */
wd = inotify_add_watch(ifd, "/tmp", IN_CREATE | IN_DELETE);
/* Install SIGIO handler */
sa.sa_handler = sigioHandler;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGIO, &sa, NULL);
/* Set owner and enable async + nonblocking on inotify fd */
fcntl(ifd, F_SETOWN, getpid());
flags = fcntl(ifd, F_GETFL);
fcntl(ifd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
printf("Watching /tmp for file create/delete events...\n");
while (1) {
/* Do other work ... */
if (inotifyEvent) {
/* Read all available inotify events */
while ((n = read(ifd, evbuf, sizeof(evbuf))) > 0) {
struct inotify_event *ev = (struct inotify_event *)evbuf;
if (ev->mask & IN_CREATE)
printf("File created: %s\n", ev->name);
if (ev->mask & IN_DELETE)
printf("File deleted: %s\n", ev->name);
}
inotifyEvent = 0;
}
}
return 0;
}
Using signal-driven I/O with inotify means your program is never blocked waiting for filesystem events — you get notified only when something actually changes.
while loop inside the handler (or in the main loop after seeing the flag).accept() can now be called without blocking.while loop with recvfrom() until it returns -1 with EAGAIN (the socket must be O_NONBLOCK). Then reset the flag. Reason: multiple datagrams may arrive between signal deliveries, and the kernel may not send a separate SIGIO for each one.Learn about F_SETSIG, SA_SIGINFO, realtime signals, and how to handle thousands of fds efficiently.
