AOSP Expert & Production Engineering
3 min read

Epoll

epoll_create, epoll_ctl, epoll_wait

epoll is the Linux kernel's highly scalable I/O event notification facility. It is designed to replace older APIs like select and poll, which degrade in performance linearly as the number of monitored file descriptors increases. epoll scales at O(1) in the number of monitored descriptors.

The API consists of three main system calls:

  1. epoll_create1: Creates an epoll instance and returns a file descriptor pointing to it.
  2. epoll_ctl: Adds, modifies, or removes file descriptors from the epoll instance's interest list.
  3. epoll_wait: Blocks the calling thread until an event occurs on any of the monitored file descriptors.
#include <sys/epoll.h>
#include <unistd.h>
#include <stdio.h>

void epoll_example(int target_fd) {
    int epfd = epoll_create1(0);
    
    struct epoll_event ev;
    ev.events = EPOLLIN; // Monitor for input availability
    ev.data.fd = target_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, target_fd, &ev);

    struct epoll_event events[MAX_EVENTS];
    int num_events = epoll_wait(epfd, events, MAX_EVENTS, -1); // Block indefinitely
    
    for (int i = 0; i < num_events; i++) {
        if (events[i].data.fd == target_fd) {
            printf("Data is ready to read!\n");
        }
    }
    close(epfd);
}

Edge-Triggered vs Level-Triggered

epoll supports two modes of operation:

  • Level-Triggered (LT): This is the default. epoll_wait will return the file descriptor as ready as long as the condition holds (e.g., there is still unread data in the buffer). It behaves similarly to poll.
  • Edge-Triggered (ET): Enabled via the EPOLLET flag. epoll_wait returns only when the state of the file descriptor changes (e.g., transitions from not-ready to ready). If you do not read all available data, epoll_wait will not notify you again until new data arrives. ET requires the use of non-blocking file descriptors and reading until EAGAIN is returned.

Looper.cpp: Android's epoll Wrapper

In AOSP, the foundation of the event-driven architecture is the Looper (implemented in C++ in system/core/libutils/Looper.cpp). The Looper is an object associated with a specific thread that processes a queue of messages.

Under the hood, Looper.cpp relies exclusively on epoll. It monitors an internal file descriptor (often an eventfd or pipe) used for waking up the thread when a new message is enqueued, alongside any other user-registered file descriptors.

When a thread calls Looper::pollOnce(), the Looper invokes epoll_wait(). The thread sleeps efficiently in the kernel until a file descriptor signals readiness.

Use in InputDispatcher and MessageQueue

The Android graphics and input stack are deeply dependent on epoll.

InputDispatcher

The InputDispatcher component (part of SurfaceFlinger/SystemServer) receives raw touch and key events from the kernel via evdev nodes. It uses an epoll instance to monitor multiple input device file descriptors simultaneously. When a user touches the screen, epoll_wait wakes the dispatcher thread instantly to process the event.

MessageQueue

Android's Java-level MessageQueue and Handler system are backed by the native Looper. The Java MessageQueue.nativePollOnce() method calls down into the C++ Looper::pollOnce(). This allows a single Android application thread (like the main UI thread) to block efficiently while waiting for UI events, Binder transactions, or timed messages, all orchestrated by a single epoll instance.