poll() — Timeout, Return Values & Real Example

 

poll() — Timeout, Return Values & Real Example
Chapter 63 – Alternative I/O Models | Linux Programming Interface
Topic
poll() Deep Dive
Level
Intermediate
Part
2 of 2

What We Cover in This Part

In Part 1 we learned what poll() is, the struct pollfd structure, and all the event flags. In this part we go deeper:

timeout argument return values explained select() vs poll() return value difference poll_pipes.c example walkthrough server pattern with poll() EINTR handling

Key Terms in This Part

timeout EINTR nonzero revents blocking poll non-blocking poll calloc pipe() ready fd count

The timeout Argument — Three Behaviours

The timeout argument controls how long poll() will wait. The unit is milliseconds (not seconds, not microseconds — milliseconds). This gives you fine-grained control.

timeout values and their effects

timeout = -1
Block forever — wait until at least one fd is ready, or a signal arrives. Use this when you have nothing else to do while waiting.

timeout = 0
Non-blocking check — poll() returns immediately with whatever is ready right now. Returns 0 if nothing is ready. Use for polling in a tight event loop.

timeout > 0
Timed wait — wait up to timeout milliseconds. Returns early if any fd becomes ready or a signal arrives. Ideal for heartbeat/keepalive logic.
/* Example: different timeout values */

/* Block forever — only returns when something is ready */
poll(fds, nfds, -1);

/* Non-blocking — check and return immediately */
poll(fds, nfds, 0);

/* Wait up to 500 milliseconds (half a second) */
poll(fds, nfds, 500);

/* Wait up to 5 seconds */
poll(fds, nfds, 5000);
Note on accuracy: The timeout value is rounded up to the next multiple of the kernel’s timer tick (usually 1-4ms on modern Linux). So timeout = 1 may actually wait 1-4ms. For accurate sub-millisecond timing, use ppoll() which takes a struct timespec.

Return Values — What poll() Tells You

After poll() returns, the return value tells you the overall status. Then you inspect each revents to find out which specific fds are ready.

Decision tree after poll() returns

int ready = poll(fds, nfds, timeout)

−1
Error
Check errno. Common: EINTR (interrupted by signal)

0
Timeout
No fd became ready within the timeout period

N > 0
Ready!
N fds have a nonzero revents. Loop through the array to find them.
int ready = poll(fds, nfds, 5000);

if (ready == -1) {
    if (errno == EINTR) {
        /* A signal interrupted us — this is not a real error.
         * Just call poll() again in your event loop. */
        goto retry;
    }
    perror("poll");
    exit(EXIT_FAILURE);
}

if (ready == 0) {
    printf("Timeout: no fd was ready in 5 seconds\n");
    return;
}

/* ready > 0: some fds have events */
for (int i = 0; i < nfds; i++) {
    if (fds[i].revents == 0)
        continue;   /* nothing on this fd */

    /* Handle the events */
    if (fds[i].revents & POLLIN) { /* data to read */ }
    if (fds[i].revents & POLLOUT) { /* can write    */ }
    if (fds[i].revents & POLLHUP) { /* hangup       */ }
    if (fds[i].revents & POLLERR) { /* error        */ }
}

Important difference from select():

  • select() counts a fd multiple times if it appears in more than one returned set (e.g. readable AND exceptional = counted twice). Its return value can be larger than the actual number of ready fds.
  • poll() counts each fd once, no matter how many bits are set in revents. The return value is exactly the number of fds with a nonzero revents.

Handling EINTR — Signal Interruption

poll() is a blocking system call. If a signal arrives while poll() is waiting, the kernel interrupts it and poll() returns -1 with errno == EINTR. This is not an error in the usual sense — it just means a signal was delivered.

Unlike read() or write(), poll() is never automatically restarted even if you set SA_RESTART on the signal handler. You must handle EINTR yourself.

#define _GNU_SOURCE
#include <poll.h>
#include <errno.h>

/* Wrapper that automatically retries poll() on EINTR */
int poll_eintr(struct pollfd *fds, nfds_t nfds, int timeout)
{
    int ready;
    do {
        ready = poll(fds, nfds, timeout);
    } while (ready == -1 && errno == EINTR);
    return ready;
}

/* Usage */
int ready = poll_eintr(fds, nfds, -1);
if (ready == -1) {
    perror("poll");   /* real error, not EINTR */
    exit(EXIT_FAILURE);
}
ppoll() alternative: The newer ppoll() system call atomically sets a signal mask while waiting. This eliminates the race condition between a signal arriving and you calling poll() — use ppoll() in production server code for cleaner signal handling.

Full Working Example — Monitoring Multiple Pipes with poll()

This is the poll_pipes.c example from TLPI (Listing 63-2). It creates N pipes, writes to randomly selected pipe write-ends, then calls poll() to find which read-ends have data. This is a clean demonstration of the complete poll() workflow.

What the program does — step by step
1
Read num-pipes and num-writes from command-line arguments.
2
Allocate an array of int[2] for all the pipe fds, and a struct pollfd array of the same size.
3
Call pipe() for each pipe. Read-end goes into pfds[j][0], write-end into pfds[j][1].
4
Write a byte to num-writes randomly chosen pipe write-ends.
5
Fill the pollFd array: each entry gets the pipe’s read-end fd and events = POLLIN.
6
Call poll(), then loop through the array and print which read-ends have data.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <poll.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "Usage: %s num-pipes [num-writes]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    int numPipes  = atoi(argv[1]);
    int numWrites = (argc > 2) ? atoi(argv[2]) : 1;

    /* Allocate array of pipe fd pairs: pfds[j][0]=read, pfds[j][1]=write */
    int (*pfds)[2] = calloc(numPipes, sizeof(int[2]));
    if (!pfds) { perror("calloc"); exit(EXIT_FAILURE); }

    /* Allocate pollfd array — one entry per pipe */
    struct pollfd *pollFd = calloc(numPipes, sizeof(struct pollfd));
    if (!pollFd) { perror("calloc"); exit(EXIT_FAILURE); }

    /* Create all the pipes */
    for (int j = 0; j < numPipes; j++) {
        if (pipe(pfds[j]) == -1) {
            perror("pipe");
            exit(EXIT_FAILURE);
        }
    }

    /* Write a byte to randomly selected pipe write-ends */
    srand(time(NULL));
    for (int j = 0; j < numWrites; j++) {
        int randPipe = rand() % numPipes;
        printf("Writing to fd: %d (read fd: %d)\n",
               pfds[randPipe][1], pfds[randPipe][0]);
        if (write(pfds[randPipe][1], "x", 1) == -1) {
            perror("write");
            exit(EXIT_FAILURE);
        }
    }

    /* Set up the pollfd array — watch the READ end of each pipe */
    for (int j = 0; j < numPipes; j++) {
        pollFd[j].fd     = pfds[j][0];  /* read end */
        pollFd[j].events = POLLIN;       /* we want to know when data arrives */
    }

    /* Call poll() — timeout = -1 means wait forever */
    int ready = poll(pollFd, numPipes, -1);
    if (ready == -1) {
        perror("poll");
        exit(EXIT_FAILURE);
    }

    printf("poll() returned: %d\n", ready);

    /* Find which read-ends have data */
    for (int j = 0; j < numPipes; j++) {
        if (pollFd[j].revents & POLLIN)
            printf("Readable: %d\n", pollFd[j].fd);
    }

    /* Cleanup */
    for (int j = 0; j < numPipes; j++) {
        close(pfds[j][0]);
        close(pfds[j][1]);
    }
    free(pfds);
    free(pollFd);
    return 0;
}
How to compile and run:

gcc -o poll_pipes poll_pipes.c

# Create 10 pipes, write to 3 randomly chosen ones
./poll_pipes 10 3

# Sample output:
# Writing to fd: 4  (read fd: 3)
# Writing to fd: 14 (read fd: 13)
# Writing to fd: 14 (read fd: 13)
# poll() returned: 2
# Readable: 3
# Readable: 13

Notice: even though fd 14 was written to twice, fd 13 is listed only once — poll() counts each fd once, not once per write.

Real-World Pattern — TCP Server Event Loop with poll()

This is how poll() is actually used in a TCP server. The idea: keep the listening socket and all connected client sockets in the pollFd array. When poll() returns, serve whoever is ready.

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAX_CLIENTS  64
#define PORT         8080
#define BUF_SIZE     1024

int main(void)
{
    /* Create and bind the listening socket (setup code omitted for brevity) */
    int listenFd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr = {
        .sin_family      = AF_INET,
        .sin_port        = htons(PORT),
        .sin_addr.s_addr = INADDR_ANY,
    };
    bind(listenFd, (struct sockaddr *)&addr, sizeof(addr));
    listen(listenFd, 10);

    printf("Listening on port %d...\n", PORT);

    /* pollFd[0] = the listening socket
     * pollFd[1..MAX_CLIENTS] = connected client sockets */
    struct pollfd fds[MAX_CLIENTS + 1];
    int nfds = 1;

    fds[0].fd     = listenFd;
    fds[0].events = POLLIN;   /* watch for new connections */

    /* Initialize client slots as unused */
    for (int i = 1; i <= MAX_CLIENTS; i++) {
        fds[i].fd = -1;       /* -1 = slot is free */
    }

    char buf[BUF_SIZE];

    for (;;) {                /* event loop */
        int ready = poll(fds, nfds, -1);
        if (ready == -1) {
            perror("poll");
            break;
        }

        /* Check the listening socket — new connection? */
        if (fds[0].revents & POLLIN) {
            int clientFd = accept(listenFd, NULL, NULL);
            if (clientFd != -1) {
                /* Find a free slot */
                int slot = -1;
                for (int i = 1; i <= MAX_CLIENTS; i++) {
                    if (fds[i].fd == -1) { slot = i; break; }
                }
                if (slot == -1) {
                    /* No room — reject */
                    close(clientFd);
                } else {
                    fds[slot].fd     = clientFd;
                    fds[slot].events = POLLIN | POLLRDHUP;
                    if (slot >= nfds) nfds = slot + 1;
                    printf("New client on fd %d (slot %d)\n", clientFd, slot);
                }
            }
        }

        /* Check all client slots */
        for (int i = 1; i < nfds; i++) {
            if (fds[i].fd == -1)
                continue;

            if (fds[i].revents == 0)
                continue;

            /* Client disconnected? */
            if (fds[i].revents & (POLLHUP | POLLRDHUP | POLLERR)) {
                printf("Client fd %d disconnected\n", fds[i].fd);
                close(fds[i].fd);
                fds[i].fd = -1;   /* free the slot */
                continue;
            }

            /* Data ready to read? */
            if (fds[i].revents & POLLIN) {
                int n = read(fds[i].fd, buf, sizeof(buf) - 1);
                if (n <= 0) {
                    /* 0 = client closed, -1 = error */
                    close(fds[i].fd);
                    fds[i].fd = -1;
                } else {
                    buf[n] = '\0';
                    printf("From fd %d: %s\n", fds[i].fd, buf);
                    /* Echo back */
                    write(fds[i].fd, buf, n);
                }
            }
        }
    }

    close(listenFd);
    return 0;
}
Key design points in the pattern above:

  • The listening socket is always at fds[0].
  • Client slots use fd = -1 to mark them as free (poll() ignores negative fds).
  • We watch POLLRDHUP to detect clean client disconnects immediately.
  • We always check POLLERR | POLLHUP even though we didn’t request them in events.
  • After closing a client fd, we set fds[i].fd = -1 so the slot can be reused.

poll() vs select() — When to Use Which
Feature select() poll()
Max file descriptors FD_SETSIZE (usually 1024) Unlimited (heap array)
Fd description method 3 bit-sets Array of structs
Timeout unit struct timeval (microseconds) int (milliseconds)
After returning Must rebuild fd sets each call Array reused; only revents changes
Return value counts Can count same fd multiple times Each fd counted once
Portability All POSIX systems All modern POSIX systems
Disable one fd without rebuilding No — must FD_CLR and recalculate max Yes — set events=0 or fd=-fd
Peer disconnect detection Not direct POLLRDHUP (Linux)
Rule of thumb: For new code on Linux, prefer epoll() for servers with many connections (it scales to tens of thousands of fds efficiently). Use poll() for moderate numbers of fds or when portability to non-Linux POSIX systems matters. Avoid select() in new code.

Interview Questions — poll() Timeout & Behaviour
Q1. What happens when you pass timeout = -1 to poll()?

poll() blocks indefinitely until at least one of the monitored file descriptors becomes ready, or a signal is delivered to the process. It will never return 0 (timeout) in this mode.

Q2. poll() returned -1 and errno is EINTR. Is this a real error? What should you do?

No, it is not a real error. It means a signal handler was invoked while poll() was waiting. poll() is never automatically restarted even with SA_RESTART. The correct action is to check for errno == EINTR and call poll() again. Wrap it in a do-while loop or write a small wrapper function that handles EINTR transparently.

Q3. poll() returned 3. How do you find which file descriptors are ready?

You must loop through all elements in your pollFd array and check fds[i].revents != 0. The return value (3) only tells you how many fds have a nonzero revents — it doesn’t tell you which ones or where they are in the array. You have to check every entry. The loop may visit entries where revents is 0 and skip them cheaply.

Q4. What is the difference in the positive return value between select() and poll()?

In select(), a fd that appears in multiple returned sets (e.g. readable AND in the error set) is counted multiple times, so the return value can exceed the actual number of ready fds. In poll(), each fd in the array is counted at most once regardless of how many bits are set in its revents. The return value from poll() exactly equals the number of entries with a nonzero revents field.

Q5. You have a server with 10,000 connections. Should you use select(), poll(), or epoll()? Why?

Use epoll(). Both select() and poll() require the kernel to scan all monitored fds on every call — their performance is O(N) in the number of fds, which is slow for 10,000 connections. Also, select() has a hard FD_SETSIZE limit of 1024 on many systems. epoll() maintains a kernel-side data structure and only returns the fds that are actually ready, giving O(1) or O(ready) performance regardless of total fd count.

Q6. How does POLLNVAL differ from POLLERR?

POLLNVAL means the fd field in the pollfd structure is not a valid open file descriptor at the time of the poll() call — you may have already closed it or it was never opened. POLLERR means the fd is valid and open, but an error condition has occurred on it (e.g. a write to a broken pipe, or a socket error). POLLNVAL indicates a programming bug (monitoring a closed fd). POLLERR indicates a runtime I/O error.

Q7. In the poll_pipes example, 3 random writes go to 2 distinct pipes. What does poll() return?

poll() returns 2 — the number of distinct pipe read-ends that have data available. Even if one pipe had 2 writes (and thus 2 bytes in its kernel buffer), poll() still counts that pipe’s read-end only once because it has a single pollfd entry and the count measures entries with nonzero revents, not the number of bytes or writes.

Q8. Why must you always check for POLLHUP and POLLERR even if you didn’t set them in events?

Because POLLHUP and POLLERR are status flags that the kernel sets in revents whenever those conditions exist, regardless of what you put in events. If you only check POLLIN in revents and ignore the rest, you may call read() on an fd that has a hangup or error and get unexpected behavior. Always check all relevant bits in revents after every poll() call.

Series Complete — poll() System Call

You now know how poll() works, its event flags, timeout modes, return values, and real-world usage patterns.

← Part 1: pollfd & Events All Tutorials

Leave a Reply

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