Full epoll Program
Intermediate
3 of 3
Putting It All Together
In the previous two parts you learned how to use epoll_ctl() to register file descriptors and epoll_wait() to receive events. Now let’s build a real program that monitors multiple FIFOs (named pipes) for input simultaneously — something that would be painful to do with blocking reads but is clean and efficient with epoll.
This program demonstrates the complete epoll workflow: create → register → wait loop → handle events → cleanup.
Call
epoll_create1(0) to get an epoll file descriptorOpen each FIFO path from command-line args, add with
EPOLLINBlock until one or more FIFOs have data. Check for EINTR (signal interrupted) and retry.
If EPOLLIN → read and print data. If HUP/ERR → close fd, decrement counter.
Track open count with
numOpenFds. When it reaches 0, exit the loop./* epoll_multi_fifo.c
* Monitors multiple FIFOs for input using epoll.
* Usage: ./epoll_multi_fifo /tmp/fifo1 /tmp/fifo2 ...
*
* Before running, create FIFOs:
* mkfifo /tmp/fifo1 /tmp/fifo2
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#define MAX_EVENTS 10
#define BUF_SIZE 512
int main(int argc, char *argv[])
{
int epfd, fd, nready, i, numOpenFds;
struct epoll_event ev, evlist[MAX_EVENTS];
char buf[BUF_SIZE];
ssize_t n;
if (argc < 2) {
fprintf(stderr, "Usage: %s fifo...\n", argv[0]);
exit(EXIT_FAILURE);
}
/* ---- Step 1: Create epoll instance ---- */
epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
/* ---- Step 2: Open each FIFO and add to interest list ---- */
numOpenFds = 0;
for (i = 1; i < argc; i++) {
/* O_RDONLY | O_NONBLOCK: don't block waiting for a writer */
fd = open(argv[i], O_RDONLY);
if (fd == -1) {
perror(argv[i]);
continue;
}
/* Register fd: watch for readable data */
ev.events = EPOLLIN;
ev.data.fd = fd; /* store fd so we know it when event fires */
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
perror("epoll_ctl ADD");
close(fd);
continue;
}
printf("Opened and watching: %s (fd=%d)\n", argv[i], fd);
numOpenFds++;
}
if (numOpenFds == 0) {
fprintf(stderr, "No FIFOs could be opened\n");
exit(EXIT_FAILURE);
}
/* ---- Step 3: Event loop ---- */
while (numOpenFds > 0) {
/* Block until at least one fd is ready (timeout=-1: wait forever) */
nready = epoll_wait(epfd, evlist, MAX_EVENTS, -1);
if (nready == -1) {
if (errno == EINTR) {
/*
* EINTR means a signal interrupted us (e.g., SIGSTOP + SIGCONT).
* This is not an error — just restart the call.
*/
printf("epoll_wait interrupted by signal, retrying...\n");
continue;
}
perror("epoll_wait");
exit(EXIT_FAILURE);
}
printf("\n--- epoll_wait() returned %d ready fds ---\n", nready);
/* ---- Step 4: Process each ready fd ---- */
for (i = 0; i < nready; i++) {
fd = evlist[i].data.fd; /* which fd fired? */
printf("fd=%d events: %s%s%s%s\n",
fd,
(evlist[i].events & EPOLLIN) ? "EPOLLIN " : "",
(evlist[i].events & EPOLLHUP) ? "EPOLLHUP " : "",
(evlist[i].events & EPOLLERR) ? "EPOLLERR " : "",
(evlist[i].events & EPOLLRDHUP) ? "EPOLLRDHUP " : "");
if (evlist[i].events & EPOLLIN) {
/* Data is available: read and display it */
n = read(fd, buf, BUF_SIZE - 1);
if (n > 0) {
buf[n] = '\0';
printf(" Data: %s", buf);
} else if (n == 0) {
/* EOF: writer closed the FIFO */
printf(" EOF on fd %d\n", fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
numOpenFds--;
} else {
perror(" read");
}
}
if (evlist[i].events & (EPOLLHUP | EPOLLERR)) {
/*
* EPOLLHUP: other end of FIFO was closed (no writers left)
* EPOLLERR: some I/O error on this fd
* Either way: remove from interest list and close
*/
printf(" HUP or ERR on fd %d — closing\n", fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
numOpenFds--;
}
}
}
/* ---- Step 5: All FIFOs closed, clean up ---- */
printf("\nAll FIFOs closed. Exiting.\n");
close(epfd);
return 0;
}
Compile: gcc epoll_multi_fifo.c -o epoll_multi_fifo
You need two terminal windows. Here is the exact sequence:
$ mkfifo /tmp/fifo1 /tmp/fifo2
# Run the program (will block waiting for writers)
$ ./epoll_multi_fifo /tmp/fifo1 /tmp/fifo2
Opened and watching: /tmp/fifo1 (fd=3)
Opened and watching: /tmp/fifo2 (fd=4)
$ echo “hello from fifo1” > /tmp/fifo1
# Write to fifo2
$ echo “hello from fifo2” > /tmp/fifo2
# Both FIFOs closed, Terminal 1 exits
--- epoll_wait() returned 1 ready fds ---
fd=3 events: EPOLLIN
Data: hello from fifo1
--- epoll_wait() returned 1 ready fds ---
fd=3 events: EPOLLHUP
HUP or ERR on fd 3 — closing
--- epoll_wait() returned 1 ready fds ---
fd=4 events: EPOLLIN
Data: hello from fifo2
...
All FIFOs closed. Exiting.
Why does EPOLLHUP appear after EPOLLIN? When the writer closes the FIFO after writing, the reader gets both an EPOLLIN (there is still data in the buffer) and then EPOLLHUP (the write end is closed).
If a signal is delivered while your program is blocked inside epoll_wait(), the system call is interrupted and returns -1 with errno set to EINTR.
A common scenario: your program is stopped with Ctrl+Z (SIGSTOP) and then resumed with fg (SIGCONT). When SIGCONT is delivered, the sleeping epoll_wait() wakes up with EINTR.
| Feature | select() | poll() | epoll |
|---|---|---|---|
| Max fd limit | FD_SETSIZE (1024) | Unlimited | Unlimited |
| Time complexity per call | O(n) | O(n) | O(ready) |
| Re-register fds each call? | Yes | Yes | No |
| Returns only ready fds? | No (scans all) | No (scans all) | Yes |
| Edge-triggered mode | No | No | Yes (EPOLLET) |
| Portable (POSIX)? | Yes | Yes | Linux only |
The key advantage of epoll at large scale: select and poll scan through ALL monitored fds to find ready ones — O(n) cost. epoll only returns the fds that are actually ready — O(ready) cost. With 10,000 connections but only 5 active at any moment, epoll processes 5 events while select scans 10,000.
epoll supports two modes of notification. Understanding the difference is critical for correctness.
/* Edge-triggered: must read until EAGAIN */
while (1) {
n = read(fd, buf, sizeof(buf));
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
break; /* no more data for now */
perror("read");
break;
}
if (n == 0) {
/* EOF */
break;
}
/* process buf[0..n-1] */
}
Edge-triggered (EPOLLET): epoll_wait() reports the fd only once when NEW data arrives. You must read all available data immediately (loop until EAGAIN). More efficient but harder to use correctly — easy to miss data if you don’t fully drain the fd.
while (numOpenFds > 0). This gives you a clean exit condition — the program automatically stops when all monitored fds are gone, with no polling or idle spinning.