epoll_ctl() — Managing the Interest List

epoll_ctl() — Managing the Interest List
Linux Chapter 63 · Alternative I/O Models · EmbeddedPathashala
Topic
epoll_ctl()
Level
Intermediate
Part
1 of 3

What is epoll_ctl()?

Once you create an epoll instance using epoll_create(), the next step is telling the kernel which file descriptors you want to monitor and what events you care about. That is exactly what epoll_ctl() does — it manages the interest list of an epoll instance.

Think of the interest list like a watch-list. You add file descriptors to it, and the kernel watches them for you. You can add, modify, or remove entries at any time — even while epoll_wait() is running in another thread.

Key Terms in This Tutorial

epoll_ctl() interest list EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL epoll_event epoll_data_t EPOLLIN max_user_watches

Function Signature
#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);

/* Returns 0 on success, -1 on error */

Parameters explained:

Parameter Type What it means
epfd int The epoll instance file descriptor (from epoll_create)
op int Operation: ADD, MOD, or DEL
fd int The target file descriptor to watch
ev epoll_event* Events to monitor + user data to return later

What file descriptors can you watch?

The fd argument (the file descriptor being monitored) can be:

✅ Allowed
🔵 Pipe
🔵 FIFO
🔵 Socket
🔵 POSIX message queue
🔵 inotify instance
🔵 Terminal / device
🔵 Another epoll descriptor
❌ NOT Allowed
🔴 Regular file
🔴 Directory

Error returned: EPERM

The reason regular files and directories are not supported is that they are always ready for I/O — watching them with epoll makes no sense.

The op Argument — Three Operations

EPOLL_CTL_ADD
Add fd to the interest list. Set which events to monitor via ev. If fd is already in the list → error EEXIST.
EPOLL_CTL_MOD
Modify the events for fd already in the list. Uses new ev settings. If fd is NOT in the list → error ENOENT.
EPOLL_CTL_DEL
Remove fd from the interest list. The ev argument is ignored here. If fd is NOT in the list → error ENOENT.
Also: closing a fd automatically removes it from all epoll interest lists.

The epoll_event Structure

When you call epoll_ctl() with ADD or MOD, you pass a pointer to an epoll_event structure:

struct epoll_event {
    uint32_t     events;    /* bit mask: what events to watch */
    epoll_data_t data;      /* user data: returned when event fires */
};

The data field is a union — you pick one member to use:

typedef union epoll_data {
    void        *ptr;   /* pointer to your own struct */
    int          fd;    /* file descriptor number */
    uint32_t     u32;   /* any 32-bit value */
    uint64_t     u64;   /* any 64-bit value */
} epoll_data_t;

Why is data a union?

When epoll reports a ready event, it gives back this data field. This is the only way to know which file descriptor became ready. The most common use is storing the fd number:

ev.data.fd = fd;   /* store fd so we know it later */

Alternatively, you can store a pointer (ev.data.ptr) to a struct that contains more context — useful in server programs where you track connection state per client.

Code Example: epoll_create + epoll_ctl
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/epoll.h>

int main(void)
{
    int epfd, fd;
    struct epoll_event ev;

    /* Step 1: create epoll instance */
    epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    /* Step 2: open a file (pipe read end, socket, etc.) */
    /* For demo: open stdin (fd=0) */
    fd = 0; /* stdin */

    /* Step 3: set what events to watch and what data to return */
    ev.events  = EPOLLIN;    /* notify when data is ready to read */
    ev.data.fd = fd;         /* we want to know WHICH fd fired */

    /* Step 4: add fd to epoll's interest list */
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
        perror("epoll_ctl ADD");
        exit(EXIT_FAILURE);
    }

    printf("fd %d added to epoll interest list\n", fd);

    /* Modify: change to watch EPOLLIN | EPOLLRDHUP */
    ev.events = EPOLLIN | EPOLLRDHUP;
    if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1) {
        perror("epoll_ctl MOD");
        exit(EXIT_FAILURE);
    }
    printf("fd %d modified in epoll interest list\n", fd);

    /* Remove fd from interest list */
    if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1) {
        perror("epoll_ctl DEL");
        exit(EXIT_FAILURE);
    }
    printf("fd %d removed from epoll interest list\n", fd);

    close(epfd);
    return 0;
}

Compile and run: gcc epoll_ctl_demo.c -o epoll_ctl_demo && ./epoll_ctl_demo

The max_user_watches Limit

Each fd registered in an epoll interest list uses a small amount of non-swappable kernel memory. To prevent abuse, the kernel enforces a per-user limit on the total number of fds across all epoll instances.

Check and change the limit:
# View current limit
cat /proc/sys/fs/epoll/max_user_watches

# Change the limit (root required)
echo 1048576 > /proc/sys/fs/epoll/max_user_watches

The default is calculated based on available RAM. High-performance servers (handling millions of connections) may need to increase this.

Common epoll_ctl() Errors
Error Errno Cause
Adding duplicate fd EEXIST fd already in interest list (use MOD instead)
MOD/DEL on absent fd ENOENT fd not in interest list (ADD it first)
Regular file or directory EPERM epoll does not support regular files / dirs
Invalid epfd or fd EBADF Not a valid open file descriptor

Interview Questions — epoll_ctl()
Q1. What is the epoll interest list?
The interest list is a kernel-managed set of file descriptors that you want epoll to monitor. You register fds into it using epoll_ctl(EPOLL_CTL_ADD). When any of those fds becomes ready for I/O, the kernel reports it via epoll_wait().
Q2. What happens if you try to add a regular file to epoll?
epoll_ctl() returns -1 and sets errno to EPERM. Regular files and directories are always ready for I/O so epoll monitoring is meaningless for them. Use read()/write() directly or use a thread pool for disk I/O.
Q3. Why is the ev.data field a union?
Because when an event fires, you need some way to know which fd caused it. The union gives you flexibility — store just the fd number (ev.data.fd), or a pointer to a full connection context struct (ev.data.ptr). This is important because epoll itself does not tell you which fd triggered the event — only the data you stored does.
Q4. What is max_user_watches and why does it matter?
It is a per-user kernel limit on how many total fds can be registered across all epoll instances. Each registered fd consumes non-swappable kernel memory. In high-connection-count servers (e.g., 100k+ clients), you may need to raise this limit via /proc/sys/fs/epoll/max_user_watches.
Q5. Can one epoll instance monitor another epoll instance?
Yes. You can pass another epoll fd as the fd argument to epoll_ctl(). This lets you build hierarchies of epoll instances, which is useful in multi-threaded designs where each thread has its own epoll and a master epoll watches all of them.
Q6. What error do you get when you try to add a fd that is already in the list?
epoll_ctl() returns -1 with errno set to EEXIST. The correct solution is to use EPOLL_CTL_MOD to change the events for an already-registered fd.
Q7. What happens to an fd’s epoll registration when the fd is closed?
Closing an fd automatically removes it from all epoll interest lists it was registered in. You do not need to call EPOLL_CTL_DEL before closing. However, be careful with duplicated fds (via dup() or fork()) — the registration persists until ALL copies of the fd are closed.

Next: epoll_wait() and Event Bits
Learn how to wait for events and understand all the EPOLL event flags

Part 2 → epoll_wait() EmbeddedPathashala

Leave a Reply

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