When Is “I/O Possible” Signaled? Terminals · Pipes & FIFOs · Sockets · inotify

 

When Is “I/O Possible” Signaled?
Terminals · Pipes & FIFOs · Sockets · inotify | Chapter 63 · TLPI
📺 Terminals
🔄 Pipes & FIFOs
🔌 Sockets
👁 inotify

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.

📈 Quick Summary — Signal Trigger Conditions by fd Type
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

📺 Terminals and Pseudoterminals

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.
Tip: Even if you have not read previous keystrokes, every new keystroke still generates a SIGIO. Your handler may be called multiple times before you drain all the data.

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);
}

🔄 Pipes and FIFOs

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.

🔒 Read End of Pipe/FIFO

Signal is generated when:

  • Data is written to the pipe (even if previous unread data exists)
  • The write end is closed
🖊 Write End of Pipe/FIFO

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.

🔌 Sockets — Datagram and Stream

Signal-driven I/O works for both UNIX domain and Internet domain sockets.

Datagram Sockets (UDP, UNIX SOCK_DGRAM):

Signal is generated when:

  • 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):

Signal is generated when:

  • 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 with close()
  • 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;
        }
    }
}

👁 inotify File Descriptors

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.

Key Terms

SIGIO trigger conditions terminal input pseudoterminal pipe read end pipe write end PIPE_BUF datagram socket stream socket TCP ESTABLISHED listening socket inotify_init inotify_add_watch

🏫 Interview Questions — When Is I/O Possible Signaled?
Q1. For a terminal fd, does SIGIO fire when the previous input has not been read yet?
Yes. For both terminals and pseudoterminals, SIGIO is generated whenever new input becomes available, regardless of whether previous input has been consumed. This means your handler can be called multiple times with unread data still pending. This is why you must drain all data using a while loop inside the handler (or in the main loop after seeing the flag).
Q2. Does a terminal EOF (Ctrl+D) generate SIGIO? What about terminal disconnect?
Yes, pressing Ctrl+D (EOF) on a real terminal generates SIGIO. However, a terminal disconnect (e.g., the serial cable being unplugged) is not signaled. Note: EOF is also not signaled for pseudoterminals (only for real terminals).
Q3. What triggers SIGIO on the write end of a pipe?
Two events trigger SIGIO on the write end: (1) a read from the pipe increases the available space so that at least PIPE_BUF bytes can now be written without blocking, and (2) the read end of the pipe is closed (which means any future write will generate SIGPIPE).
Q4. For a TCP listening socket, when exactly does SIGIO fire?
SIGIO fires when a new connection arrives on the listening socket — i.e., a client has completed the three-way handshake and the connection is in the accept queue. This means accept() can now be called without blocking.
Q5. Why is TCP connect() completion signaled but not UNIX domain socket connect() completion?
This is a Linux/BSD implementation detail. TCP connect() involves an asynchronous network-level three-way handshake that takes time to complete — signaling the completion makes sense for non-blocking TCP connects. UNIX domain socket connections are local and complete synchronously, so there is no equivalent asynchronous event to signal.
Q6. What condition triggers SIGIO for an inotify file descriptor?
SIGIO is generated when the inotify fd becomes readable — meaning at least one filesystem event (file create, delete, modify, etc.) has been generated for a watched file or directory. After receiving SIGIO, you read from the inotify fd to extract the event structures.
Q7. You are writing a server that monitors multiple UDP sockets. After receiving SIGIO, what is the correct approach?
When SIGIO fires, drain all available datagrams from the socket using a 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.

Next: Refining Signal-Driven I/O

Learn about F_SETSIG, SA_SIGINFO, realtime signals, and how to handle thousands of fds efficiently.

Next Topic → ← Previous

Leave a Reply

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