epoll_ctl()
Intermediate
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.
#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 |
The fd argument (the file descriptor being monitored) can be:
🔵 FIFO
🔵 Socket
🔵 POSIX message queue
🔵 inotify instance
🔵 Terminal / device
🔵 Another epoll descriptor
🔴 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.
fd to the interest list. Set which events to monitor via ev. If fd is already in the list → error EEXIST.fd already in the list. Uses new ev settings. If fd is NOT in the list → error ENOENT.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.
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;
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.
#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
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.
# 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.
| 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 |
epoll_ctl(EPOLL_CTL_ADD). When any of those fds becomes ready for I/O, the kernel reports it via epoll_wait().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.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./proc/sys/fs/epoll/max_user_watches.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.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.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.