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
Both system calls use different conventions for indicating readiness:
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.
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 (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 |
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 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.
| Data in pipe? | Write-end open? | select() | poll() revents |
|---|---|---|---|
| No | No (closed) | r | POLLHUP |
| Yes | Yes | r | POLLIN |
| Yes | No (closed) | r | POLLIN | POLLHUP |
/* 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;
}
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.
| 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.
/* 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 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.
/* 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);
}
There are two ways to detect that a remote peer has stopped sending data:
if (pfd.revents & POLLIN) {
int n = read(fd, buf, sz);
if (n == 0) {
/* EOF: remote closed */
}
}
/* Need extra read() call
just to detect shutdown */
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.
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.
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.
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.
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.
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.
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.
A: poll() sets POLLIN, and select() marks the fd as readable. This is the signal that accept() can be called without blocking.
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.
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
