select() vs poll() — Side-by-Side Comparison

 

select() vs poll() — Side-by-Side Comparison
Chapter 63 · Alternative I/O Models · Linux Programming Interface
📖 Theory
⚡ Performance
🔌 Portability
❓ Interview Q&A

Why Compare select() and poll()?

Both select() and poll() are used for I/O multiplexing — they let one process watch multiple file descriptors at the same time and react when any of them becomes ready for reading or writing. They do the same job, but they differ in API design, timeout precision, error reporting, portability, and performance. Understanding these differences helps you pick the right tool for your application.

Key Terms in This Tutorial

select() poll() nfds revents POLLNVAL EBADF FD_SETSIZE sparse descriptor set timeout precision SUSv3

1. Timeout Precision

This is one small but real difference between the two system calls.

  • select() accepts a struct timeval which gives timeout in seconds and microseconds (millionths of a second).
  • poll() accepts a single integer timeout in milliseconds (thousandths of a second).

So select() has finer timeout granularity on paper. But in practice, both are limited by the kernel’s software clock tick (usually 1ms to 10ms on Linux), so the difference rarely matters in real applications.

Timeout Parameter Comparison
System Call Parameter Type Precision Example
select() struct timeval Microseconds (µs) {tv_sec=1, tv_usec=500000} = 1.5 sec
poll() int timeout Milliseconds (ms) 1500 = 1.5 sec
/* select() timeout example */
struct timeval tv;
tv.tv_sec  = 1;        /* 1 second */
tv.tv_usec = 500000;   /* 500000 microseconds = 0.5 second */
/* total = 1.5 seconds */
select(nfds, &readfds, NULL, NULL, &tv);

/* poll() timeout example */
int timeout = 1500;   /* 1500 milliseconds = 1.5 seconds */
poll(fds, nfds, timeout);

2. Detecting a Closed File Descriptor

What happens if a file descriptor you are monitoring gets closed while you are watching it? The two system calls handle this very differently.

poll() — tells you which one
  • Sets POLLNVAL bit in revents field
  • Each pollfd entry has its own revents
  • You can see exactly which fd was closed
  • Direct and precise error reporting
select() — only tells you “something went wrong”
  • Returns -1 with errno = EBADF
  • No indication of which descriptor failed
  • You must manually check each fd
  • Indirect and less convenient

In practice this difference is not critical because a well-written application already tracks which file descriptors it has opened and closed. But poll() is cleaner here.

/* poll() - detecting closed fd using POLLNVAL */
#include <poll.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    struct pollfd fds[2];

    fds[0].fd     = 3;   /* some open fd */
    fds[0].events = POLLIN;

    fds[1].fd     = 99;  /* this fd is closed / invalid */
    fds[1].events = POLLIN;

    int ret = poll(fds, 2, 1000);

    if (ret > 0) {
        if (fds[1].revents & POLLNVAL) {
            printf("fd 99 is invalid or closed!\n");
        }
    }
    return 0;
}

3. Portability

Historically, select() appeared on UNIX systems much earlier than poll() and was more widely supported. This made select() the safe portable choice for many years.

Today both are standardized under SUSv3 (Single UNIX Specification version 3) and available on all modern Linux, macOS, and UNIX-like systems. So portability is no longer a practical concern when choosing between them.

One caveat: the behavior of poll() still varies slightly across different UNIX implementations in edge cases (e.g., behavior on certain device types). If you are writing code that must run on many older or embedded UNIX variants, test carefully.

Portability Summary
Feature select() poll()
Available since BSD 4.2 (1983) System V (later)
Standardized in SUSv3, POSIX SUSv3, POSIX
Consistent behavior Very consistent Minor variations across OSes
Windows equivalent WSASelect() WSAPoll() (Vista+)

4. Performance — Dense vs Sparse Descriptor Sets

This is the most important practical difference. To understand it, you need to understand how each system call tells the kernel which file descriptors to watch.

How select() passes fd info to kernel

select() takes a parameter called nfds = (highest fd number + 1). The kernel must scan all positions from 0 to nfds-1 in the fd_set bitmask. Even if you are only monitoring fd number 999 and nothing else, the kernel still scans all 1000 positions.

How poll() passes fd info to kernel

poll() takes an array of struct pollfd containing exactly the file descriptors you care about. If you want to watch only fd 999, you pass an array with one entry. The kernel only checks that one entry.

Dense vs Sparse Descriptor Set — Visual

Dense set — monitoring fds 0,1,2,3,4,5 (all consecutive, low numbers)
fd 0 ✓
fd 1 ✓
fd 2 ✓
fd 3 ✓
fd 4 ✓
fd 5 ✓
← select() and poll() both scan only these 6 positions. Similar performance.

Sparse set — monitoring only fd 998 and fd 999 out of range 0..999
fd 0 ✗
fd 1 ✗
… 996 more empty slots …
fd 998 ✓
fd 999 ✓
select(): kernel scans all 1000 positions in fd_set even though only 2 are used
poll(): kernel only checks 2 entries in pollfd array — much faster!

The bottom line: when your application holds many open file descriptors but monitors only a few of them at a time (sparse case), poll() will be faster than select().

Note: Linux 2.6 added optimizations that narrowed this gap compared to Linux 2.4, but the conceptual difference remains.

/* Demonstrating sparse fd scenario with poll() */
#include <poll.h>
#include <stdio.h>

int main(void)
{
    /*
     * Suppose fd values 500 and 999 are the only ones we care about.
     * With poll(), we just list them directly — kernel checks only 2 entries.
     */
    struct pollfd fds[2];

    fds[0].fd     = 500;   /* first fd of interest */
    fds[0].events = POLLIN;
    fds[0].revents = 0;

    fds[1].fd     = 999;   /* second fd of interest */
    fds[1].events = POLLIN;
    fds[1].revents = 0;

    int ret = poll(fds, 2, 5000);  /* wait up to 5 seconds */

    if (ret > 0) {
        if (fds[0].revents & POLLIN)
            printf("fd 500 is ready to read\n");
        if (fds[1].revents & POLLIN)
            printf("fd 999 is ready to read\n");
    } else if (ret == 0) {
        printf("Timeout — no fd became ready\n");
    } else {
        perror("poll");
    }
    return 0;
}

/*
 * With select() for the same scenario, nfds must be 1000
 * and the kernel scans all 1000 positions — wasteful!
 *
 * select() equivalent (less efficient):
 *   fd_set readfds;
 *   FD_ZERO(&readfds);
 *   FD_SET(500, &readfds);
 *   FD_SET(999, &readfds);
 *   select(1000, &readfds, NULL, NULL, &timeout);  // kernel scans 0..999
 */

5. Quick Summary Table
Feature select() poll()
Timeout unit Microseconds Milliseconds
Closed fd detection Returns -1, errno=EBADF (you must find which one) Sets POLLNVAL in revents of the specific fd
Portability Older, very consistent SUSv3, minor variations on old systems
Dense fd set perf. Similar to poll() Similar to select()
Sparse fd set perf. Slower (kernel scans all positions up to nfds) Faster (kernel only checks listed fds)
Max fd limit FD_SETSIZE (usually 1024) No hard limit
fd_set reinit needed Yes — before every call No — just update events field

Interview Questions & Answers
Q1. What is the maximum number of file descriptors you can monitor with select()?select() is limited by the FD_SETSIZE constant, which is typically 1024 on Linux. You cannot use select() to monitor file descriptors with numbers 1024 or higher unless you recompile with a larger FD_SETSIZE. poll() has no such hard limit.

Q2. Why must you reinitialize the fd_set before each call to select(), but not before poll()?select() modifies the fd_set in place — it clears the bits for file descriptors that are NOT ready. So after each call, the fd_set no longer contains the original set of descriptors you wanted to watch. You must rebuild it before the next call. poll() uses a separate events field (what you want to watch) and revents field (what happened), so the events field is never modified by the kernel.

Q3. When does poll() outperform select() in terms of file descriptor scanning?When the descriptor set is sparse — meaning the maximum fd number is large but only a few descriptors are actually being monitored. In this case, poll() wins because the kernel only checks the explicitly listed fds. select() forces the kernel to scan every position from 0 to nfds-1 regardless of how many are actually monitored.

Q4. How does poll() report that a monitored file descriptor was closed?poll() sets the POLLNVAL bit in the revents field of the pollfd structure corresponding to that descriptor. This tells you exactly which fd is invalid. select() only returns -1 with errno set to EBADF, giving no information about which descriptor caused the error.

Q5. Are select() and poll() affected by the kernel’s software clock granularity?Yes. Even though select() provides microsecond-level timeout precision and poll() provides millisecond-level, both are ultimately limited by the kernel’s timer resolution (software clock tick). On a system with a 10ms tick, a timeout of 1ms will not fire exactly at 1ms — it may fire up to one full tick late. So the extra precision of select() may not be practically useful on most Linux systems.

Q6. You are building a proxy server that may hold 50,000 open connections. Would you use select() or poll() for I/O multiplexing? Why?Neither is ideal for 50,000 connections because both have O(n) scaling problems. However between the two, poll() is better because it has no FD_SETSIZE limit and handles sparse descriptor sets more efficiently. For 50,000 connections the correct choice is epoll, which is Linux-specific and scales in O(1) with respect to the number of active events rather than total monitored fds.

Q7. What does nfds mean in the select() call and why is it one greater than the highest fd?nfds tells the kernel how many bit positions to scan in the fd_set bitmask. It must be (highest_fd + 1) because fd numbering starts at 0. If the highest fd you are monitoring is 10, then there are 11 positions (0 through 10) and nfds must be 11. If you pass a smaller value, select() will not check the higher-numbered descriptors even if they are in the fd_set.

Continue Learning

Next: Problems with select() and poll() at scale — why they fail for large numbers of file descriptors and what comes next.

Next: Scaling Problems → EmbeddedPathashala Home

Leave a Reply

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