select() vs poll() API Differences & When to Use Each

 

select() vs poll() โ€” Full Comparison
Chapter 63 โ€“ Implementation Details, API Differences & When to Use Each | Linux System Programming
โš–๏ธ
Topic
Comparison
๐Ÿ“„
Part
3 of 3
๐ŸŽฏ
Level
Intermediate

Why Compare select() and poll()?

Both select() and poll() solve the same problem: watching multiple file descriptors for I/O events without blocking. However, they have different APIs, different limits, and different behaviors in certain edge cases.

Understanding their differences helps you write portable code, choose the right tool for the job, and avoid common bugs. This tutorial covers how they work internally, key API differences, and a side-by-side code comparison.

Key Terms

select() poll() fd_set FD_SETSIZE FD_SET FD_ISSET FD_ZERO timeval value-result argument POLLNVAL EBADF kernel poll routines I/O multiplexing

๐Ÿ“Œ Quick Recap: select() API

select() is the older, POSIX-standard I/O multiplexing system call:

#include <sys/select.h>
#include <sys/time.h>

int select(int nfds,
           fd_set *readfds,
           fd_set *writefds,
           fd_set *exceptfds,
           struct timeval *timeout);

/* Returns:
   >0  โ†’ number of ready fds across all sets
    0  โ†’ timeout expired
   -1  โ†’ error (errno set) */

/* struct timeval: */
struct timeval {
    long tv_sec;   /* seconds */
    long tv_usec;  /* microseconds */
};

Helper macros for working with fd_set:

fd_set rfds;

FD_ZERO(&rfds);        /* clear all bits in the set */
FD_SET(fd, &rfds);    /* add fd to the set */
FD_CLR(fd, &rfds);    /* remove fd from the set */
FD_ISSET(fd, &rfds);  /* check if fd is set (returns nonzero if yes) */

The nfds parameter must be set to one more than the highest fd you are monitoring:

/* If you are watching fds 3, 7, and 12: */
int nfds = 12 + 1;  /* = 13 */
select(nfds, &rfds, NULL, NULL, &tv);

๐Ÿ“Œ How select() and poll() Work Inside the Kernel

A common misconception is that select() and poll() are fundamentally different under the hood. They are not. Both use the same set of kernel internal poll routines.

select() system call
poll() system call
Kernel Internal Poll Routines
One routine per fd type (pipe, socket, terminal…)
Returns a bitmask of ready events
Converts bitmask โ†’ r/w/x sets
using POLLIN_SET, POLLOUT_SET, POLLEX_SET
Puts bitmask directly โ†’ revents field
of each struct pollfd

The conversion macros that select() uses internally:

/* How the kernel maps poll bits to select() results */

#define POLLIN_SET  (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
/* These bits โ†’ mark fd as READABLE in select() */

#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
/* These bits โ†’ mark fd as WRITABLE in select() */

#define POLLEX_SET  (POLLPRI)
/* This bit  โ†’ mark fd as EXCEPTIONAL in select() */

Key insight from these macros: POLLERR appears in both POLLIN_SET and POLLOUT_SET. This means when there is an error condition on an fd, select() marks it as both readable AND writable. Your code must still call read() or write() to discover the actual error.

Also notice that POLLHUP maps to the readable set. This is why select() marks a pipe readable when the write-end is closed โ€” even though there is no data and read() will just return 0.

๐Ÿ“Œ API Differences โ€” select() vs poll()

Feature select() poll()
Max fd limit FD_SETSIZE (default 1024 on Linux). Hard to change without recompile. No intrinsic limit. Can monitor any fd number.
Data structure fd_set (a fixed-size bit array) array of struct pollfd (variable size)
Reinitialize in loop? YES โ€” fd_set is value-result, modified on return. Must FD_ZERO + FD_SET again each iteration. NO โ€” events field is preserved. Only revents is overwritten by the kernel.
Timeout type struct timeval (seconds + microseconds) int (milliseconds). Simpler to use.
Timeout modified? On Linux, timeval is updated to show remaining time. Not portable. Timeout value is never modified by the call.
Invalid fd handling Returns -1 with errno = EBADF Sets POLLNVAL in revents for that fd. Other fds can still be reported.
Sparse fd sets Inefficient: kernel must scan all bits up to the highest fd, even if most are 0. Efficient: kernel only processes exactly the fds in your array.
Portability POSIX standard, available everywhere (Windows too) POSIX standard since SUSv3, available on all modern Unix/Linux
Event granularity 3 sets: read, write, except. Coarse-grained. Per-fd events bitmask. Finer control (e.g. POLLPRI, POLLRDHUP).

๐Ÿ“Œ The Value-Result Problem with select()

This is one of the most common bugs when using select() in a loop. The fd_set arguments are “value-result” โ€” the kernel modifies them in place to indicate which fds are ready. After select() returns, only the ready fds remain set; all others are cleared.

This means you must rebuild your fd_sets before every call to select(). poll() does not have this problem.

/* WRONG: select() bug โ€” fd_set not rebuilt each iteration */
void broken_select_loop(int fd1, int fd2)
{
    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(fd1, &rfds);
    FD_SET(fd2, &rfds);

    while (1) {
        /* BUG: After first select() call, rfds only has ready fds.
           fd1 or fd2 may have been cleared! Next iteration
           monitors fewer fds than intended. */
        int ret = select(fd2 + 1, &rfds, NULL, NULL, NULL);
        if (ret > 0) {
            if (FD_ISSET(fd1, &rfds)) { /* handle fd1 */ }
            if (FD_ISSET(fd2, &rfds)) { /* handle fd2 */ }
        }
    }
}

/* CORRECT: rebuild fd_set before each call */
void correct_select_loop(int fd1, int fd2)
{
    fd_set rfds;

    while (1) {
        FD_ZERO(&rfds);        /* clear set */
        FD_SET(fd1, &rfds);   /* re-add fd1 */
        FD_SET(fd2, &rfds);   /* re-add fd2 */

        int nfds = (fd1 > fd2 ? fd1 : fd2) + 1;
        int ret = select(nfds, &rfds, NULL, NULL, NULL);
        if (ret > 0) {
            if (FD_ISSET(fd1, &rfds)) { /* handle fd1 */ }
            if (FD_ISSET(fd2, &rfds)) { /* handle fd2 */ }
        }
    }
}

poll() avoids this entirely because events and revents are separate fields:

/* poll() loop โ€” no rebuilding needed */
void poll_loop(int fd1, int fd2)
{
    struct pollfd fds[2];
    fds[0].fd     = fd1;
    fds[0].events = POLLIN;   /* set ONCE, never touched again */
    fds[1].fd     = fd2;
    fds[1].events = POLLIN;   /* set ONCE, never touched again */

    while (1) {
        /* No rebuild needed! events field is preserved across calls */
        int ret = poll(fds, 2, -1);
        if (ret > 0) {
            if (fds[0].revents & POLLIN) { /* handle fd1 */ }
            if (fds[1].revents & POLLIN) { /* handle fd2 */ }
        }
        /* revents is reset by kernel each call โ€” no manual clearing */
    }
}

๐Ÿ“Œ The FD_SETSIZE Limit in select()

fd_set is a fixed-size data structure. On Linux, by default it can hold file descriptor numbers from 0 to 1023 (FD_SETSIZE = 1024).

fd_set as a bit array:
bit 0
fd=0
bit 1
fd=1
bit 2
fd=2
bit 5 โœ“
fd=5
bit 12 โœ“
fd=12
bit 1023
fd=1023
fd=1024
โŒ LIMIT
#include <stdio.h>
#include <sys/select.h>

int main(void)
{
    printf("FD_SETSIZE = %d\n", FD_SETSIZE);
    /* Output: FD_SETSIZE = 1024 on Linux */

    /* Using fd >= FD_SETSIZE with select() causes undefined behavior! */
    int fd = 1500;  /* hypothetically a very high-numbered fd */

    fd_set rfds;
    FD_ZERO(&rfds);
    /* FD_SET(fd, &rfds); */ /* DANGEROUS: writes past end of fd_set! */

    /* poll() has no such limit */
    struct pollfd pfd;
    pfd.fd     = fd;   /* any fd number works fine */
    pfd.events = POLLIN;
    /* poll(&pfd, 1, -1); */ /* perfectly safe */

    return 0;
}

Modern servers can easily have more than 1024 open connections. This is a real limitation of select() that poll() (and epoll) solve.

๐Ÿ“Œ Invalid File Descriptor Handling

What happens if you accidentally pass a closed or invalid file descriptor?

select() behavior

Returns -1 with errno = EBADF. The entire call fails. You get no information about which fds were valid and ready. You have to hunt for the bad fd yourself.

poll() behavior

Sets POLLNVAL in revents for the bad fd entry. The rest of the fds are still checked and reported normally. You can identify exactly which fd is invalid.

/* Demonstrating invalid fd handling */
#include <stdio.h>
#include <poll.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    int pfd[2];
    pipe(pfd);

    /* Close one end, but try to poll it anyway */
    close(pfd[0]);  /* close the read end */

    struct pollfd fds[2];

    /* fd 0 (read end) is closed โ€” invalid */
    fds[0].fd     = pfd[0];
    fds[0].events = POLLIN;

    /* fd 1 (write end) is still valid */
    fds[1].fd     = pfd[1];
    fds[1].events = POLLOUT;

    int ret = poll(fds, 2, 0);
    printf("poll() returned: %d\n", ret);

    /* Check what happened for each fd */
    for (int i = 0; i < 2; i++) {
        printf("fds[%d]: POLLIN=%s POLLOUT=%s POLLNVAL=%s POLLERR=%s\n", i,
            (fds[i].revents & POLLIN)   ? "yes" : "no",
            (fds[i].revents & POLLOUT)  ? "yes" : "no",
            (fds[i].revents & POLLNVAL) ? "yes" : "no",
            (fds[i].revents & POLLERR)  ? "yes" : "no");
    }
    /* fds[0]: POLLNVAL=yes  (was closed)
       fds[1]: POLLOUT=yes   (still valid, writable) */

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

๐Ÿ“Œ Same Task: select() vs poll() Side by Side

Monitor two pipes for readability. One pipe has data written into it. Both implementations should detect the same result.

/* ============================================================
   TASK: monitor 2 pipes, detect which has data ready to read
   ============================================================ */

/* ---- Using select() ---- */
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>

void demo_select(int fd1, int fd2)
{
    fd_set rfds;
    struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };

    /* Must build fd_set fresh every time */
    FD_ZERO(&rfds);
    FD_SET(fd1, &rfds);
    FD_SET(fd2, &rfds);

    int nfds = (fd1 > fd2 ? fd1 : fd2) + 1;
    int ret  = select(nfds, &rfds, NULL, NULL, &tv);

    if (ret > 0) {
        if (FD_ISSET(fd1, &rfds)) printf("[select] fd1 (%d) is readable\n", fd1);
        if (FD_ISSET(fd2, &rfds)) printf("[select] fd2 (%d) is readable\n", fd2);
    } else if (ret == 0) {
        printf("[select] timeout\n");
    } else {
        perror("[select] error");
    }
}

/* ---- Using poll() ---- */
#include <poll.h>

void demo_poll(int fd1, int fd2)
{
    struct pollfd fds[2];
    fds[0].fd     = fd1;
    fds[0].events = POLLIN;
    fds[1].fd     = fd2;
    fds[1].events = POLLIN;

    /* No need to rebuild; events field is preserved */
    int ret = poll(fds, 2, 5000);  /* 5000ms timeout */

    if (ret > 0) {
        if (fds[0].revents & POLLIN) printf("[poll]   fd1 (%d) is readable\n", fd1);
        if (fds[1].revents & POLLIN) printf("[poll]   fd2 (%d) is readable\n", fd2);
    } else if (ret == 0) {
        printf("[poll] timeout\n");
    } else {
        perror("[poll] error");
    }
}

int main(void)
{
    int p1[2], p2[2];
    pipe(p1);
    pipe(p2);

    /* Write data only to pipe 1 */
    write(p1[1], "hello", 5);

    printf("--- select() result ---\n");
    demo_select(p1[0], p2[0]);

    printf("\n--- poll() result ---\n");
    demo_poll(p1[0], p2[0]);

    close(p1[0]); close(p1[1]);
    close(p2[0]); close(p2[1]);
    return 0;
}

Expected output:

--- select() result ---
[select] fd1 (3) is readable

--- poll() result ---
[poll]   fd1 (3) is readable

๐Ÿ“Œ Timeout Precision Comparison
select() โ€” struct timeval
struct timeval tv;
tv.tv_sec  = 2;       /* 2 seconds */
tv.tv_usec = 500000;  /* + 500ms */
/* Total: 2.5 seconds */
select(nfds, &rfds, NULL, NULL, &tv);
/* On Linux, tv is decremented
   to show remaining time after
   select() returns. On other
   systems, tv may be undefined
   after the call. */
poll() โ€” int milliseconds
int timeout = 2500;  /* 2500ms = 2.5s */
poll(fds, nfds, timeout);
/* timeout value is NEVER
   modified by poll().
   Same value usable across
   repeated calls. */
/* -1 = block forever
    0 = return immediately */

poll() timeout is simpler to use and more predictable. select()’s timeval modification is Linux-specific and makes it awkward to use in tight loops.

๐Ÿ“Œ When to Use select() vs poll()

Use select() when:
  • You need maximum portability (including older systems and Windows via Winsock)
  • You are monitoring a small, fixed set of file descriptors all below 1024
  • You are working on legacy code that already uses select()
  • Your codebase needs to compile on very old Unix systems where poll() may not be available
Use poll() when:
  • You need to monitor file descriptors with numbers above 1023
  • You use the same fd set repeatedly in a loop (no rebuild overhead)
  • You want finer event control (POLLRDHUP, POLLPRI per fd)
  • You want more helpful handling of invalid fds (POLLNVAL instead of EBADF)
  • You are writing new Linux code and portability to Windows is not required
Consider epoll when:
  • You are building a high-performance server handling thousands of connections
  • Both select() and poll() scale poorly โ€” they scan all monitored fds on every call
  • epoll only reports fds that actually have events, making it O(active events) not O(total fds)
  • Linux-only: use when portability is not a concern and performance is critical

๐Ÿ“Œ Quick Reference Summary
Aspect select() poll()
Max fd number 1023 (FD_SETSIZE – 1) No limit
Rebuild set each call? YES (mandatory) No
Timeout units struct timeval (ยตs precision) int milliseconds
Timeout modified? Yes (Linux only) Never
Invalid fd โ†’ result -1, errno=EBADF POLLNVAL in revents
Kernel implementation Same internal poll routines Same internal poll routines
Portability Widest (incl. Windows) All modern Unix/Linux
Scalability Poor (scans up to nfds bits) Fair (scans nfds entries)
POLLRDHUP support Not available Yes (Linux 2.6.17+)

๐ŸŽฏ Interview Questions โ€” select() vs poll() Comparison
Q1: What is the fundamental limitation of select() that poll() does not have?

A: select() uses the fd_set data type which has a fixed size determined by FD_SETSIZE (1024 on Linux). It cannot monitor file descriptors with numbers 1024 or above without recompiling the application. poll() places no such limit on the fd numbers that can be monitored.

Q2: Why must you reinitialize fd_set before each call to select() in a loop?

A: Because fd_set is a “value-result” argument. select() modifies the fd_sets in place on return, clearing all bits for file descriptors that were not ready. If you do not reinitialize, the next call will only monitor the fds that happened to be ready in the previous call, missing fds that were not ready yet.

Q3: What does poll() do differently from select() when one fd in the list is invalid (already closed)?

A: select() fails entirely with -1 and errno set to EBADF, giving no information about the other fds. poll() sets POLLNVAL in the revents field of just that invalid fd’s entry, and continues to report the status of all other valid fds in the array. This makes it much easier to identify and handle the problem.

Q4: Do select() and poll() use different kernel code internally?

A: No. Both use the same internal kernel poll routines. These routines return a bitmask of ready events for each fd. poll() puts this bitmask directly into revents. select() uses conversion macros (POLLIN_SET, POLLOUT_SET, POLLEX_SET) to translate the bitmask into its r/w/x fd_set representation.

Q5: Why does select() mark a file descriptor as both readable AND writable when there is an error?

A: Because of the internal macros POLLIN_SET and POLLOUT_SET. POLLERR appears in both macros. When the kernel’s internal poll routine returns POLLERR for an fd, select() places that fd in both the read set and the write set. The application must then call read() or write() to discover the actual error.

Q6: What is the timeout unit in poll() vs select()?

A: poll() uses a single int parameter in milliseconds. select() uses a struct timeval with seconds and microseconds fields. poll()’s timeout is simpler. Also, on Linux, select() modifies the timeval to show remaining time on return (not portable), while poll() never modifies its timeout parameter.

Q7: What is the nfds parameter in select() and why is it important?

A: nfds must be set to one more than the highest fd number being monitored. The kernel scans the fd_set bit array from bit 0 up to bit (nfds-1). If you set nfds too low, some fds will be missed. If you set it too high unnecessarily, the kernel wastes time scanning empty bits. poll() does not need this โ€” it simply processes exactly the N entries in your array.

Q8: Why do both select() and poll() scale poorly for very large numbers of connections? What is the alternative?

A: Both have to scan through all monitored fds on every call โ€” select() scans up to nfds bits, poll() iterates through all N pollfd entries โ€” even if only a few fds have events. For 10,000 connections with only 5 active, this is wasteful. The alternative is epoll (Linux-specific), which uses an event notification mechanism that only returns fds that actually have events, scaling as O(active events) rather than O(total monitored fds).

Q9: On which platform is select() available that poll() typically is not?

A: Windows. The Winsock API on Windows supports select() (with some differences). poll() is not natively available in the Windows Winsock API (though WSAPoll was added later in Windows Vista). If you need to write cross-platform socket code that works on Windows, select() is the safer portable choice.

Q10: What is FD_SETSIZE and can you change it?

A: FD_SETSIZE is a compile-time constant that determines the size of fd_set. On Linux it defaults to 1024. You can change it by defining FD_SETSIZE to a larger value before including sys/select.h, but you must recompile your entire application. Also, the system limit on open fds per process (set by ulimit) must also be raised. This is another reason many applications prefer poll() or epoll over select().

Chapter 63 Complete!

You have covered poll(), fd readiness conditions, and the full select() vs poll() comparison.

โ† Part 1: poll() API โ† Part 2: FD Readiness EmbeddedPathashala Home

Leave a Reply

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