Search by

Linux epoll(7) bindings as global PHP functions

Package info

github.com/php-io-extensions/epoll

Language:C

Type:php-ext

Ext name:ext-epoll

pkg:composer/php-io-extensions/epoll

Statistics

Installs: 170

Dependents: 0

Suggesters: 2

Stars: 0

Open Issues: 0

v0.10.6 2026-10-12 03:21 UTC

This package is auto-updated.

Last update: 2026-10-14 02:23:26 UTC


README

ext-epoll

Linux epoll(7) bindings for PHP 8.4+

CI Packagist PHP License Platform OKF PIE

A program that talks to many connections at once needs to answer one question over and over: "which of my file descriptors can I read or write right now without blocking?" PHP's built-in answer is stream_select(). It works, and it's the right answer while you only watch a handful of streams. Every call you hand it the full list, the kernel copies the list in, scans every one, copies the result out. The more fds you watch, the more expensive that becomes.

With 10 connections nothing is taxed. With 10,000, each wakeup scans 10,000 fds to find the 3 that have data. There's also a hard cap, FD_SETSIZE, usually 1024, baked into the C library that PHP inherits. So out of the box, resource waiting doesn't scale.

epoll solves this. You register each fd once with the Linux kernel into an "epoll instance", which also happens to be an fd. Then you wait on it. This is the cool part: the kernel hands back only the fds with activity, each one labelled with the tag you registered it under. PHP never sorts through what's ready; it iterates through the ready ones, reads them, and goes again.

The only problem? PHP never exposes epoll to userland. PHP-FPM uses it internally, and PECL has ev, event and uv, which pick epoll on Linux and hide it behind a portable event loop. Every one of them gives you a loop. This extension exposes epoll directly giving you access to the system calls, one to one, no opinionated event loop: allowing you the freedom to build the loop you need, or watch resources those loop implementations never thought of.

Basic usage

Most ways you will interface with epoll is in two parts -

Start an epoll instance with epoll_create1(), connect to resources and hand their fds to epoll with epoll_ctl().

// Create an epoll instance with the kernel and get back its fd.
$epoll_fd = epoll_create1(EPOLL_CLOEXEC);

$resources = [];
$resource_tags = ['resourceA', 'resourceB', 'resourceC'];

foreach ($resource_tags as $tag => $name) 
{  
    // Open another process on the device
    [$process, $pipes] = $this->startNewProcess();
    // Save it to your list of $resources
    $resources[$tag] = ['name' => $name, 'process' => $process, 'stdin' => $pipes[0], 'stdout' => $pipes[1]];
    
    epoll_ctl($epoll_fd, EPOLL_CTL_ADD, $pipes[1], EPOLLIN, $tag);
    $this->writeToResource($pipes[0])
}

Ping epoll for activity with epoll_wait()and do work. Use epoll to stop listening to resources with epoll_ctl($epoll_fd, EPOLL_CTL_DEL, $resource) before closing the connection to the resource process.

while (!empty($servers)) 
{
    $activity = epoll_wait(edfd: $epoll_fd, maxevents:16, timeout: -1);
    
    foreach ($activity as ['events' => $events, 'data' => $tag]) 
    {
        $resource = $resources[$tag];
        
        // Read the data out of the resource
        $line = fgets($server['stdout']);
        $valid = ($line === false || $events & EPOLLHUP);
        
        if ($valid) {
            $message = json_decode($line, true);
            echo "{$resource['name']}: ", $message['method'] ?? "reply to #{$message['id']}", "\n";
        }
        else
        {
            // There was nothing to read, so use epoll to close the connection
            epoll_ctl($epfd, EPOLL_CTL_DEL, $server['stdout']);
            proc_close($resource['process']);
            unset($resources[$tag]);
            echo "{$resource['name']}: gone\n";
        }
    }

Requirements

OS Linux. epoll(7) is a Linux syscall family; configure stops on any other kernel.
Kernel Any that has epoll. epoll_pwait2() needs 5.11 or newer; the other six functions don't care.
PHP 8.4 or newer, NTS or ZTS, from php-src or your distribution.
Libraries None beyond libc.
Optional ext/sockets, if you want to register Socket objects. Detected at build, resolved at runtime; the module loads without it.

To build it you need a C compiler, the kernel headers, and the development files of the PHP you intend to load it into:

sudo apt install build-essential libc6-dev php8.4-dev

Installation

With PHP PIE (recommended)

PIE is the PHP Foundation's installer for extensions. It downloads the tagged source from Packagist, builds it against the PHP on your PATH, installs the module and enables it:

sudo pie install php-io-extensions/epoll

Manually (install script)

The repository ships install-debian-trixie.sh for Debian Trixie and compatible systems (Ubuntu 24.04+, Raspberry Pi OS Bookworm and Trixie). It finds the phpize and php-config that match your php, builds, installs the module, writes 30-epoll.ini into every conf.d of that PHP (CLI, FPM, Apache) and leaves the checkout clean:

git clone https://github.com/php-io-extensions/epoll.git
cd epoll
./install-debian-trixie.sh

Manually (phpize)

git clone https://github.com/php-io-extensions/epoll.git
cd epoll
phpize
./configure --enable-epoll
make -j"$(nproc)"
sudo make install

Then enable it: add extension=epoll.so to your php.ini, or drop that line into a file in the scan directory that php --ini reports.

Verify

$ php --ri epoll

epoll

epoll support => enabled
Version => 0.10.x
epoll_pwait2 => libc

epoll_pwait2 reads libc when your C library provides the wrapper, syscall when the extension calls the kernel directly, or unavailable (ENOSYS) on a kernel older than 5.11. The other six functions work on any of the three.

API

Method Description
epoll_create(int $size): int Creates an epoll instance and returns its fd, or -1. $size must be positive; the kernel has ignored its value since Linux 2.6.8.
epoll_create1(int $flags): int Same, with $flags of 0 or EPOLL_CLOEXEC. Prefer this one.
epoll_ctl(int $epfd, int $op, Socket|resource|int $fd, ?int $events = null, ?int $data = null): int Adds, modifies or removes $fd in the instance (EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL). $events is a mask of EPOLL* constants; null sends no event struct, which is what EPOLL_CTL_DEL wants. $data is your 64-bit tag, handed back untouched by the wait functions; null tags the registration with its own fd number. Returns 0 or -1.
epoll_wait(int $epfd, int $maxevents, int $timeout): array|false Blocks up to $timeout milliseconds (-1 forever, 0 poll) and returns up to $maxevents entries of ['events' => int, 'data' => int], empty on timeout, false on error. The fd is not in the result; your tag is.
epoll_pwait(int $epfd, int $maxevents, int $timeout, ?array $sigmask): array|false As epoll_wait(), with $sigmask (a list of signal numbers) installed as the blocked set only for the duration of the wait. null leaves the mask alone; [] unblocks everything, so a pending signal interrupts with EINTR.
epoll_pwait2(int $epfd, int $maxevents, ?int $timeout_ns, ?array $sigmask): array|false As epoll_pwait(), with the timeout in nanoseconds; null blocks forever. Needs Linux 5.11. php --ri epoll shows whether your build reaches it through libc, a raw syscall, or not at all (ENOSYS).
epoll_errno(): int The errno saved by the last call that failed. Compare it against ext/sockets' SOCKET_E* constants; this extension defines none of its own. Bad PHP arguments never reach here: they throw ValueError or TypeError before the syscall.
Constant Description
EPOLLIN, EPOLLOUT, EPOLLPRI Readable, writable, urgent data. The events you ask for.
EPOLLRDHUP Peer closed its write side. Ask for it to learn about a hang-up before the read returns empty.
EPOLLERR, EPOLLHUP Error and hang-up. Always reported, never need asking for.
EPOLLRDNORM, EPOLLRDBAND, EPOLLWRNORM, EPOLLWRBAND, EPOLLMSG The poll(2) aliases the header carries. Bound because the header has them.
EPOLLET Edge-triggered: report once per new arrival instead of on every wait while data remains.
EPOLLONESHOT Report once, then disarm until EPOLL_CTL_MOD re-arms the registration.
EPOLLEXCLUSIVE, EPOLLWAKEUP Wake one of several waiters on the same fd; hold a wakeup lock while the event is pending.
EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL The $op of epoll_ctl().
EPOLL_CLOEXEC The only flag epoll_create1() takes: close the instance across exec.

Every value comes from <sys/epoll.h> at build time. EPOLLET is the header's 1 << 31, kept positive (2147483648) so it passes the uint32 range check on $events.

Extensions you might use with this one

  • ext-posi: POSIX calls as PHP functions. Gives you posix_open(), posix_read(), posix_close() and ioctl() for the raw fds epoll hands back, including the epoll instance itself, which core PHP cannot close.
  • ext/sockets: ships with PHP. Its Socket objects register directly in epoll_ctl(), and its SOCKET_E* constants name the errno values.

AI-assisted development

This codebase was built with LLM assistance under human direction. Prompts, reviews, and architectural decisions were owned by a professional software engineer — the model accelerates drafting and exploration; it does not replace judgment on APIs, dependency direction, or what ships.

Agentic Documentation

This repository carries an Open Knowledge Format bundle at .okf/: Markdown with YAML frontmatter, readable by you and loadable by a coding agent. It records what the README doesn't: how the module is laid out, why each argument check exists, how the stub becomes epoll_arginfo.h, which epoll_pwait2 path a build takes, and a log of every change that altered behaviour.

Start at .okf/index.md. AGENTS.md is the short rule sheet an agent reads before touching the C, the stub, or packaging; each rule points into the bundle for the reason behind it.

Point your agent at the repo and it has the same context the maintainer has. Point yourself at it when the README's answer is "it does X" and you want "because".

Resources

Running the test suite

The suite is Pest v4 and needs no hardware: socket pairs, /proc/self/fdinfo, and a child signal. It runs on Linux only, since the module does. Build the module in the clone, then point php at it:

git clone https://github.com/php-io-extensions/epoll.git
cd epoll
phpize && ./configure --enable-epoll && make -j"$(nproc)"
composer install
php -d extension="$PWD/modules/epoll.so" vendor/bin/pest
Tests:    39 passed (186 assertions)
Duration: 0.15s

Loading the freshly built .so by path means the suite tests the clone, not whatever epoll.so is already installed. If both end up loaded you'll see Module "epoll" is already loaded; add -n to skip your php.ini.

Contributing

Open an issue first for anything beyond a typo. Pull requests change epoll.stub.php and epoll.c together, regenerate epoll_arginfo.h with gen_stub, and add a Pest test for the behaviour. CI must be green on every cell of the matrix. Read AGENTS.md for the rules the code follows.

Security

epoll only reports readiness; it never reads, writes or closes a descriptor. See SECURITY.md for the model and how to report a vulnerability.

License

MIT. See LICENSE.