poll() System Call Alternative I/O Models

 

poll() System Call
Chapter 63 โ€“ Alternative I/O Models | Linux System Programming
โš™๏ธ
Topic
poll() API
๐Ÿ“„
Part
1 of 3
๐ŸŽฏ
Level
Intermediate

What is poll()?

When you write a program that talks to many file descriptors at the same time โ€” like a server handling multiple clients โ€” you cannot just call read() on each one and wait. That would block your whole program.

poll() is a system call that lets you watch a list of file descriptors and tells you which ones are ready for reading or writing โ€” without blocking on any single one. It is part of the “Alternative I/O Models” in Linux, alongside select() and epoll.

Think of poll() like a security guard watching many doors at once. Instead of standing at one door and waiting, the guard checks all doors together and tells you which ones have activity.

Key Terms

poll() pollfd POLLIN POLLOUT POLLHUP POLLERR revents events nfds timeout multiplexing non-blocking I/O

๐Ÿ“Œ poll() Function Signature

The poll() function prototype is defined in <poll.h>:

#include <poll.h>

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

/* Returns:
   > 0  โ†’ number of file descriptors that are ready
     0  โ†’ timeout expired, no fd is ready
    -1  โ†’ error (check errno)
*/

Parameters explained:

  • fds[] โ€” array of pollfd structures, one per file descriptor you want to watch
  • nfds โ€” number of items in the array
  • timeout โ€” how long to wait in milliseconds. Use -1 to block forever, 0 to return immediately

๐Ÿ“Œ struct pollfd โ€” The Key Structure

Each entry in the array is a struct pollfd:

struct pollfd {
    int   fd;       /* File descriptor to watch */
    short events;   /* What events we are interested in (INPUT) */
    short revents;  /* What events actually happened (OUTPUT) */
};

You fill in fd and events before calling poll(). After poll() returns, the kernel fills in revents for you.

You Set (Before)
fd = 5
events = POLLIN
revents = 0
โ†’
poll()
kernel checks
all fds
โ†’
Kernel Fills (After)
fd = 5
events = POLLIN
revents = POLLIN โœ“

๐Ÿ“Œ Common Event Flags

These are the flags you use in events (what you want to watch) and that the kernel sets in revents (what happened):

Flag Used in events? Can appear in revents? Meaning
POLLIN Yes Yes Data available for reading
POLLOUT Yes Yes Writing is possible without blocking
POLLPRI Yes Yes Exceptional/out-of-band data (e.g. TCP OOB)
POLLHUP No (ignored) Yes Hang-up: peer closed the connection
POLLERR No (ignored) Yes Error condition on fd
POLLNVAL No (ignored) Yes fd is not open / invalid
POLLRDHUP Yes (Linux only) Yes Peer shut down writing half of stream socket

Note: POLLHUP, POLLERR, and POLLNVAL are always reported in revents even if you did not ask for them in events. You do not need to set them โ€” the kernel sets them automatically when those conditions occur.

๐Ÿ“Œ Real Example: Using poll() with Multiple Pipes

The classic use case from TLPI: create N pipes, write to some random pipes, then use poll() to find out which pipe read-ends have data ready. This is a great exercise to understand how poll() works in practice.

Pipe 0
write_fd[0] โ†’โ†’โ†’ read_fd[0]
Pipe 1
write_fd[1] โ†’โ†’โ†’ read_fd[1]
Pipe 2
write_fd[2] โ†’โ†’โ†’ read_fd[2]
… N pipes
write_fd[N] โ†’โ†’โ†’ read_fd[N]
poll() watches all read ends at once โ†’ tells you which ones have data
/* poll_pipes.c - monitor multiple pipes with poll() */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <time.h>

int main(int argc, char *argv[])
{
    int numPipes, numWrites;
    int **pfds;          /* 2D array: pfds[i][0]=read, pfds[i][1]=write */
    struct pollfd *pollFd;
    int ready, j, randPipe;

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

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

    /* Allocate arrays */
    pfds    = calloc(numPipes, sizeof(int *));
    pollFd  = calloc(numPipes, sizeof(struct pollfd));
    if (!pfds || !pollFd) { perror("calloc"); exit(EXIT_FAILURE); }

    for (j = 0; j < numPipes; j++) {
        pfds[j] = malloc(2 * sizeof(int));
        if (pipe(pfds[j]) == -1) { perror("pipe"); exit(EXIT_FAILURE); }
    }

    /* Write 'a' to random pipes */
    srandom(time(NULL));
    for (j = 0; j < numWrites; j++) {
        randPipe = random() % numPipes;
        printf("Writing to fd: %3d (read fd: %3d)\n",
               pfds[randPipe][1], pfds[randPipe][0]);
        if (write(pfds[randPipe][1], "a", 1) == -1) {
            perror("write"); exit(EXIT_FAILURE);
        }
    }

    /* Build pollfd array: watch read-ends for POLLIN */
    for (j = 0; j < numPipes; j++) {
        pollFd[j].fd     = pfds[j][0];   /* read end */
        pollFd[j].events = POLLIN;        /* want to know if data is ready */
    }

    /* Call poll() โ€” timeout=-1 means block until at least one is ready */
    ready = poll(pollFd, numPipes, -1);
    if (ready == -1) { perror("poll"); exit(EXIT_FAILURE); }

    printf("\npoll() returned: %d ready descriptor(s)\n\n", ready);

    /* Check which pipes have data */
    for (j = 0; j < numPipes; j++) {
        if (pollFd[j].revents & POLLIN)
            printf("Readable: pipe[%d], fd=%3d\n", j, pollFd[j].fd);
    }

    /* Cleanup */
    for (j = 0; j < numPipes; j++) {
        free(pfds[j]);
        close(pfds[j][0]);
        close(pfds[j][1]);
    }
    free(pfds);
    free(pollFd);
    return 0;
}

Compile and run:

gcc -o poll_pipes poll_pipes.c
./poll_pipes 5 3

Sample output:

Writing to fd:   5 (read fd:   4)
Writing to fd:   9 (read fd:   8)
Writing to fd:   5 (read fd:   4)

poll() returned: 2 ready descriptor(s)

Readable: pipe[0], fd=  4
Readable: pipe[2], fd=  8

๐Ÿ“Œ Step-by-Step: How the Example Works

1
Create N pipes using pipe(). Each pipe gives two fds: [0] for reading, [1] for writing.
2
Write data to random pipes using write(pfds[randPipe][1], "a", 1). Some pipes get data, others remain empty.
3
Fill pollFd[] โ€” set fd to each pipe’s read-end and events = POLLIN.
4
Call poll(pollFd, numPipes, -1) โ€” blocks until at least one pipe read-end has data. Returns the count of ready fds.
5
Check revents โ€” loop through pollFd[], and check if revents & POLLIN is true for each fd.

๐Ÿ“Œ Common poll() Usage Patterns

Pattern 1: poll() in a loop (typical server)

/* Typical server loop with poll() */
#include <poll.h>
#include <stdio.h>

#define MAX_FDS 100

void server_loop(int listen_fd)
{
    struct pollfd fds[MAX_FDS];
    int nfds = 0;
    int i, ready;

    /* Add listen socket */
    fds[0].fd     = listen_fd;
    fds[0].events = POLLIN;
    nfds = 1;

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

        for (i = 0; i < nfds; i++) {
            if (fds[i].revents == 0)
                continue;  /* nothing on this fd */

            if (fds[i].revents & POLLIN) {
                if (fds[i].fd == listen_fd) {
                    /* New incoming connection */
                    int conn_fd = accept(listen_fd, NULL, NULL);
                    if (nfds < MAX_FDS) {
                        fds[nfds].fd     = conn_fd;
                        fds[nfds].events = POLLIN;
                        nfds++;
                    }
                } else {
                    /* Data from existing client */
                    char buf[256];
                    int n = read(fds[i].fd, buf, sizeof(buf));
                    if (n == 0) {
                        /* Client disconnected */
                        close(fds[i].fd);
                        fds[i].fd = -1; /* mark as unused */
                    } else {
                        /* process data */
                    }
                }
            }

            if (fds[i].revents & POLLHUP) {
                printf("Hangup on fd %d\n", fds[i].fd);
                close(fds[i].fd);
                fds[i].fd = -1;
            }
        }
    }
}

Pattern 2: Setting fd = -1 to skip a slot

/* poll() ignores entries where fd is set to -1 */
/* This is the clean way to remove an fd from monitoring */

pollFd[i].fd = -1;   /* tells poll() to skip this slot */

/* Later when you add a new fd, reuse this slot */
for (j = 0; j < nfds; j++) {
    if (pollFd[j].fd == -1) {
        pollFd[j].fd     = new_fd;
        pollFd[j].events = POLLIN;
        break;
    }
}

Pattern 3: Timeout usage

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

if (ret == 0) {
    printf("Timeout: no fd became ready in 5 seconds\n");
} else if (ret > 0) {
    printf("%d fd(s) ready\n", ret);
} else {
    perror("poll");
}

๐ŸŽฏ Interview Questions โ€” poll() System Call
Q1: What is the difference between events and revents in struct pollfd?

A: events is an input field โ€” you set it before calling poll() to tell the kernel what events you are interested in (e.g., POLLIN for readable). revents is an output field โ€” the kernel fills it after poll() returns to tell you what actually happened on that fd. You never set revents yourself; only the kernel does.

Q2: What does poll() return when the timeout expires?

A: poll() returns 0 when the timeout expires without any fd becoming ready. It returns a positive number equal to the count of ready fds when at least one fd has an event, and -1 on error.

Q3: How do you remove a file descriptor from a poll() monitoring list?

A: Set the fd field of that pollfd entry to -1. poll() automatically ignores entries where fd is -1. This avoids having to compact the array on every removal.

Q4: Do you need to add POLLHUP or POLLERR to events to detect hangup or error?

A: No. POLLHUP, POLLERR, and POLLNVAL are always checked by the kernel and reported in revents regardless of what you put in events. You should always check for these in your revents handling loop.

Q5: What is the purpose of the timeout parameter in poll()? Give values and their meanings.

A:

  • -1 โ€” block indefinitely until at least one fd is ready
  • 0 โ€” return immediately (non-blocking check)
  • >0 โ€” wait up to that many milliseconds, then return 0 if still no fd is ready
Q6: Can poll() be used in a loop? What precaution does it NOT need (unlike select)?

A: Yes, poll() is commonly used in a server loop. Unlike select(), you do NOT need to reinitialize the fd set before each call. poll() uses separate events and revents fields โ€” events stays unchanged between calls, and the kernel just overwrites revents each time. With select(), the fd_set is modified on return, so you must rebuild it every iteration.

Q7: What is POLLNVAL and when does the kernel set it?

A: POLLNVAL is set in revents when the fd in that pollfd entry is not a valid open file descriptor at the time of the poll() call. This is different from POLLERR, which indicates an error on a valid fd. POLLNVAL means the fd itself is invalid (already closed or never opened).

Q8: What header file is needed for poll()?

A: #include <poll.h>

Continue Learning

Next: When is a File Descriptor Ready? (select & poll readiness rules)

Next: FD Readiness โ†’ EmbeddedPathashala Home

Leave a Reply

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