When is a File Descriptor Ready?

 

When is a File Descriptor Ready?
Chapter 63 โ€“ Readiness Conditions for select() and poll() | Linux System Programming
๐Ÿ“‹
Topic
FD Readiness
๐Ÿ“„
Part
2 of 3
๐ŸŽฏ
Level
Intermediate

The Core Concept: Ready Does Not Mean Data

This is one of the most important โ€” and most misunderstood โ€” points about select() and poll():

A file descriptor is “ready” if a call to an I/O function would NOT block โ€” regardless of whether data would actually be transferred.

This means a pipe read-end is “ready” even when the write-end has been closed and there is no data โ€” because a read() would return immediately with 0 (end of file) rather than blocking. Similarly, a socket might be “ready to write” even if there is an error โ€” because a write() would return immediately with an error instead of blocking.

Understanding this distinction helps you write correct I/O multiplexing code that handles edge cases properly.

Key Terms

fd readiness non-blocking POLLIN POLLOUT POLLHUP POLLERR POLLPRI POLLRDHUP regular file pipe FIFO terminal pseudoterminal socket end of file PIPE_BUF

๐Ÿ“Œ How select() and poll() Report Readiness

Both system calls use different conventions for indicating readiness:

select() notation
r โ€” fd marked readable (in read set)
w โ€” fd marked writable (in write set)
x โ€” exceptional condition (in except set)
poll() notation
POLLIN โ€” data to read
POLLOUT โ€” writing possible
POLLHUP โ€” hangup
POLLERR โ€” error
POLLPRI โ€” exceptional/out-of-band

Inside the Linux kernel, select() and poll() both use the same internal poll routines. select() converts the kernel’s poll bitmask to its r/w/x format using these internal macros:

/* Internal kernel macros that map poll bits to select() results */
#define POLLIN_SET   (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
                     /* โ†’ marks fd as readable in select() */

#define POLLOUT_SET  (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
                     /* โ†’ marks fd as writable in select() */

#define POLLEX_SET   (POLLPRI)
                     /* โ†’ marks fd as exceptional in select() */

Notice: POLLERR appears in both POLLIN_SET and POLLOUT_SET. This means if there is an error on an fd, select() marks it as both readable AND writable. This is why your code must call read() or write() after select() to find out what actually happened.

๐Ÿ“Œ Regular Files โ€” Always Ready

File descriptors that refer to regular files (files on disk) are always considered ready by both select() and poll(). This is true for both reading and writing.

select() poll() revents Always?
r (readable) POLLIN Yes
w (writable) POLLOUT Yes

Why always ready?

  • A read() on a regular file will always return immediately โ€” with data, end-of-file, or an error. It never blocks waiting for data to “arrive”.
  • A write() to a regular file will always transfer data or fail immediately with an error. It never waits.

Practical implication: Using select() or poll() to watch regular files is mostly pointless โ€” they are always ready. These tools are really designed for sockets, pipes, and terminals where blocking is a real concern.

/* Example: regular files are ALWAYS ready */
#include <stdio.h>
#include <fcntl.h>
#include <poll.h>

int main(void)
{
    struct pollfd pfd;
    int fd = open("/etc/hostname", O_RDONLY);
    if (fd == -1) { perror("open"); return 1; }

    pfd.fd     = fd;
    pfd.events = POLLIN;

    int ret = poll(&pfd, 1, 0);  /* non-blocking poll */
    printf("poll returned: %d\n", ret);
    printf("revents: POLLIN=%s\n",
           (pfd.revents & POLLIN) ? "yes" : "no");
    /* Output: always shows POLLIN=yes for regular files */

    close(fd);
    return 0;
}

๐Ÿ“Œ Terminals and Pseudoterminals

Terminals (like your keyboard/screen connection) and pseudoterminals (used by SSH, xterm, tmux, etc.) have more interesting readiness behavior because data arrival depends on user input or a remote host.

Condition / Event select() poll() revents
Input available (user typed something) r POLLIN
Output possible (can write to terminal) w POLLOUT
Pseudoterminal peer closed (other end closed) rw POLLHUP (at minimum)
Pseudoterminal master in packet mode: slave state changed x POLLPRI
Portability warning: When one side of a pseudoterminal pair is closed, the flags set in revents for the other side vary by OS and even by whether it is the master or slave side being monitored. On Linux, at least POLLHUP is set. Portable code should check POLLHUP, POLLERR, and POLLIN together to handle this case safely.
/* Watching stdin (terminal) for input */
#include <stdio.h>
#include <poll.h>
#include <unistd.h>

int main(void)
{
    struct pollfd pfd;
    pfd.fd     = STDIN_FILENO;  /* fd 0 = terminal input */
    pfd.events = POLLIN;

    printf("Waiting 5 seconds for you to type something...\n");
    int ret = poll(&pfd, 1, 5000);  /* 5 second timeout */

    if (ret == 0) {
        printf("Timeout: no input in 5 seconds\n");
    } else if (ret > 0) {
        if (pfd.revents & POLLIN) {
            char buf[256];
            int n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
            buf[n] = '\0';
            printf("You typed: %s", buf);
        }
        if (pfd.revents & POLLHUP) {
            printf("Terminal closed!\n");
        }
    } else {
        perror("poll");
    }
    return 0;
}

๐Ÿ“Œ Pipes and FIFOs โ€” Read End

Pipes are the classic use case for poll(). The readiness of a pipe’s read-end depends on two things: whether there is data in the pipe, and whether the write-end is still open.

Pipe Read-End States:
No data + Write-end open
โ†’
BLOCKS โ€” poll() will not mark as ready
Data available + Write-end open
โ†’
select(): r | poll(): POLLIN
Data available + Write-end closed
โ†’
select(): r | poll(): POLLIN | POLLHUP
No data + Write-end closed (EOF)
โ†’
select(): r | poll(): POLLHUP
Data in pipe? Write-end open? select() poll() revents
No No (closed) r POLLHUP
Yes Yes r POLLIN
Yes No (closed) r POLLIN | POLLHUP
Portability note: On some Unix systems (not Linux), when the write-end closes, poll() sets POLLIN instead of POLLHUP. Portable code should check both POLLIN and POLLHUP to detect that the write-end was closed.
/* Demonstrating pipe read-end readiness */
#include <stdio.h>
#include <unistd.h>
#include <poll.h>

void check_pipe_readiness(int read_fd, const char *label)
{
    struct pollfd pfd = { .fd = read_fd, .events = POLLIN };
    int ret = poll(&pfd, 1, 0);  /* non-blocking check */

    printf("\n[%s]\n", label);
    printf("  poll() returned: %d\n", ret);
    printf("  POLLIN  = %s\n", (pfd.revents & POLLIN)  ? "YES" : "no");
    printf("  POLLHUP = %s\n", (pfd.revents & POLLHUP) ? "YES" : "no");
}

int main(void)
{
    int pfd[2];
    if (pipe(pfd) == -1) { perror("pipe"); return 1; }

    /* State 1: No data, write-end open โ†’ should NOT be ready */
    check_pipe_readiness(pfd[0], "No data, write-end open");

    /* State 2: Data available, write-end open โ†’ POLLIN */
    write(pfd[1], "hello", 5);
    check_pipe_readiness(pfd[0], "Data available, write-end open");

    /* State 3: Data available, write-end closed โ†’ POLLIN | POLLHUP */
    close(pfd[1]);
    check_pipe_readiness(pfd[0], "Data available, write-end closed");

    /* Read the data */
    char buf[10];
    read(pfd[0], buf, 5);

    /* State 4: No data, write-end closed โ†’ POLLHUP (EOF) */
    check_pipe_readiness(pfd[0], "No data, write-end closed (EOF)");

    close(pfd[0]);
    return 0;
}

๐Ÿ“Œ Pipes and FIFOs โ€” Write End

The write-end readiness of a pipe depends on two things: whether there is enough space in the pipe buffer, and whether the read-end is still open.

Pipe Write-End States:
No space (buffer full) + Read-end open
โ†’
BLOCKS โ€” poll() will NOT mark as ready
Space โ‰ฅ PIPE_BUF bytes + Read-end open
โ†’
select(): w | poll(): POLLOUT
Space โ‰ฅ PIPE_BUF bytes + Read-end closed
โ†’
select(): w | poll(): POLLOUT | POLLERR
No space + Read-end closed (broken pipe)
โ†’
select(): w | poll(): POLLERR
Space for PIPE_BUF bytes? Read-end open? select() poll() revents
No No (closed) w POLLERR
Yes Yes w POLLOUT
Yes No (closed) w POLLOUT | POLLERR

What is PIPE_BUF? Linux uses PIPE_BUF (4096 bytes by default) as the threshold for atomic writes. poll() considers a pipe writable only when there is at least PIPE_BUF bytes of free space. This guarantees that a write of up to PIPE_BUF bytes will not block.

Portability note: On some Unix systems, when the read-end closes, poll() returns POLLOUT or POLLHUP instead of POLLERR. Portable code should check POLLOUT, POLLHUP, and POLLERR together to detect a broken pipe situation.
/* Check what PIPE_BUF is on your system */
#include <stdio.h>
#include <limits.h>
#include <unistd.h>

int main(void)
{
    printf("PIPE_BUF = %d bytes\n", PIPE_BUF);
    /* On Linux: typically 4096 bytes */

    /* Actual pipe capacity is larger (65536 bytes since Linux 2.6.11) */
    /* but poll() uses PIPE_BUF as the "safe atomic write" threshold */

    long pipe_cap = fpathconf(1, _PC_PIPE_BUF);
    printf("_PC_PIPE_BUF = %ld\n", pipe_cap);
    return 0;
}

๐Ÿ“Œ Sockets โ€” Readiness Conditions

Sockets have the richest and most important set of readiness conditions because they cover both TCP stream and UDP datagram sockets, listening sockets, and various shutdown scenarios.

Condition / Event select() poll() revents
Data available to read r POLLIN
Writing is possible (send buffer has space) w POLLOUT
New connection ready on listening socket r POLLIN
Out-of-band (urgent) data received (TCP only) x POLLPRI
Peer closed connection or called shutdown(SHUT_WR) rw POLLIN | POLLOUT | POLLRDHUP

POLLRDHUP (Linux-specific, available since kernel 2.6.17) deserves special attention:

  • It is set when the remote peer has shut down the writing half of the connection โ€” i.e., the remote called shutdown(fd, SHUT_WR) or closed the socket entirely.
  • Without POLLRDHUP, you would have to notice that POLLIN is set, then call read(), and check if it returns 0 (which means EOF / remote shutdown).
  • With POLLRDHUP, you can detect the remote shutdown directly in your revents check, writing simpler code.
Linux UNIX domain socket note: When a UNIX domain socket peer calls close(), Linux also sets POLLHUP in revents, in addition to other flags shown in the table above.
/* Socket poll() example: TCP server detecting client disconnect */
#include <stdio.h>
#include <poll.h>
#include <unistd.h>
#include <sys/socket.h>

void handle_client(int client_fd)
{
    struct pollfd pfd;
    pfd.fd     = client_fd;
    pfd.events = POLLIN | POLLRDHUP;  /* watch for data AND remote close */

    while (1) {
        int ret = poll(&pfd, 1, -1);
        if (ret == -1) { perror("poll"); break; }

        /* Check for remote shutdown first */
        if (pfd.revents & POLLRDHUP) {
            printf("Remote peer shut down writing half\n");
            break;
        }

        /* Check for data to read */
        if (pfd.revents & POLLIN) {
            char buf[1024];
            int n = read(client_fd, buf, sizeof(buf));
            if (n == 0) {
                printf("EOF: client disconnected\n");
                break;
            } else if (n > 0) {
                printf("Received %d bytes\n", n);
                /* process data here */
            } else {
                perror("read");
                break;
            }
        }

        /* Check for error */
        if (pfd.revents & POLLERR) {
            printf("Error on socket\n");
            break;
        }

        /* Check for hangup */
        if (pfd.revents & POLLHUP) {
            printf("Hangup on socket\n");
            break;
        }
    }

    close(client_fd);
}

๐Ÿ“Œ POLLRDHUP vs Checking read() Return Value

There are two ways to detect that a remote peer has stopped sending data:

Old way (without POLLRDHUP)
if (pfd.revents & POLLIN) {
    int n = read(fd, buf, sz);
    if (n == 0) {
        /* EOF: remote closed */
    }
}
/* Need extra read() call
   just to detect shutdown */
Better way (with POLLRDHUP)
if (pfd.revents & POLLRDHUP) {
    /* remote shutdown detected
       directly in revents โ€”
       no extra read() needed */
}
/* Simpler, especially useful
   with edge-triggered epoll */

POLLRDHUP is especially valuable when using the edge-triggered mode of epoll (another I/O model covered in Section 63.4), where getting events right without extra system calls matters a lot for performance.

๐ŸŽฏ Interview Questions โ€” File Descriptor Readiness
Q1: What does “ready” mean for a file descriptor in poll() or select()?

A: A file descriptor is “ready” if a call to an I/O function on it would not block โ€” regardless of whether data is actually transferred. For example, if a pipe’s write-end is closed, the read-end is “ready” even though reading it returns 0 (EOF) rather than actual data.

Q2: Are regular files always ready in select() and poll()?

A: Yes. Regular files are always marked readable and writable because read() and write() on them always return immediately (with data, EOF, or an error). This makes select()/poll() mostly useless for monitoring regular disk files.

Q3: What revents does poll() return when a pipe’s write-end is closed and there is no data?

A: poll() sets POLLHUP in revents. The read-end is also marked readable by select() because a read() call would return 0 immediately (EOF) without blocking.

Q4: What is the significance of PIPE_BUF in poll() pipe write-end readiness?

A: Linux considers a pipe’s write-end “writable” (sets POLLOUT) only when there is at least PIPE_BUF bytes of free space in the pipe buffer. This guarantees that a write of up to PIPE_BUF bytes will succeed atomically without blocking.

Q5: What does poll() return for a socket when the remote peer closes the connection?

A: poll() sets POLLIN | POLLOUT | POLLRDHUP in revents. select() marks the fd as both readable (r) and writable (w). The POLLRDHUP flag (Linux 2.6.17+) directly indicates the remote shutdown of the write half.

Q6: What is POLLRDHUP and when should you use it?

A: POLLRDHUP is a Linux-specific flag (since kernel 2.6.17) set in revents when the remote end of a stream socket connection shuts down its write half. Without it, you detect this by noticing POLLIN is set and then calling read() which returns 0. With POLLRDHUP, you can detect the remote shutdown directly without an extra read() call. It is especially useful with edge-triggered epoll.

Q7: For a listening socket, what does poll() return when a new connection arrives?

A: poll() sets POLLIN, and select() marks the fd as readable. This is the signal that accept() can be called without blocking.

Q8: What is TCP out-of-band data and how does poll() signal it?

A: Out-of-band (urgent) data in TCP is a mechanism to send high-priority data that bypasses normal data flow. When a TCP socket receives OOB data, poll() sets POLLPRI in revents and select() marks the fd as having an exceptional condition (x). It is received using recv() with the MSG_OOB flag.

Q9: On Linux, what extra flag does poll() return for UNIX domain sockets when the peer closes?

A: On Linux, poll() also sets POLLHUP for UNIX domain sockets when the peer calls close(), in addition to the flags shown for general sockets. This behavior differs from other Unix implementations.

Continue Learning

Next: select() vs poll() โ€” Detailed Comparison, API Differences, and When to Use Each

Next: select vs poll โ†’ โ† Back: poll() API

Leave a Reply

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