NAME

Linux::Event::Loop - Linux-native event loop (epoll + timerfd + signalfd + eventfd + pidfd)

SYNOPSIS

use v5.36;
use Linux::Event;

my $loop = Linux::Event->new;   # epoll backend, monotonic clock

# I/O watcher (read/write) with user data stored on the watcher:
my $conn = My::Conn->new(...);

my $w = $loop->watch($fh,
  read  => \&My::Conn::on_read,
  write => \&My::Conn::on_write,
  error => \&My::Conn::on_error,   # optional
  data  => $conn,                   # optional (avoid closure captures)

  edge_triggered => 0,              # optional, default false
  oneshot        => 0,              # optional, default false
);

$w->disable_write;

# Timers (monotonic)
my $id = $loop->after(0.250, sub ($loop) {
  say "250ms later";
});

# Signals (signalfd): strict 4-arg callback
my $sub = $loop->signal('INT', sub ($loop, $sig, $count, $data) {
  say "SIG$sig ($count)";
  $loop->stop;
});

# Wakeups (eventfd): create once, then use to wake a blocking run()
my $waker = $loop->waker;

# In another thread/process (or any place you need to wake the loop):
#   $waker->signal;
#
# After calling $loop->waker, the loop installs an internal watcher that drains
# the wakeup fd automatically. The wakeup fd is reserved for loop wakeups.

# Pidfds (pidfd): one-shot exit notification
my $pid = fork() // die "fork: $!";
if ($pid == 0) { exit 42 }

my $psub = $loop->pid($pid, sub ($loop, $pid, $status, $data) {
  require POSIX;
  if (POSIX::WIFEXITED($status)) {
    say "child $pid exited: " . POSIX::WEXITSTATUS($status);
  }
});

$loop->run;

DESCRIPTION

Linux::Event::Loop is a minimal, Linux-native event loop that exposes Linux FD primitives cleanly and predictably. It is built around:

  • epoll(7) for I/O readiness

  • timerfd(2) for timers

  • signalfd(2) for signal delivery

  • eventfd(2) for explicit wakeups

  • pidfd_open(2) (via Linux::FD::Pid) for process lifecycle notifications

Linux::Event is intentionally not a networking framework, protocol layer, retry/backoff engine, process supervisor, or socket abstraction. Ownership is explicit; there is no implicit close, and teardown operations are idempotent.

CONSTRUCTION

new(%opts) -> $loop

my $loop = Linux::Event->new(
  backend => 'epoll',   # default
  clock   => $clock,    # optional; must provide tick/now_ns/etc.
  timer   => $timer,    # optional; must provide after/disarm/read_ticks/fh
);

Options:

  • backend

    Either the string 'epoll' (default) or a backend object that implements watch, unwatch, and run_once.

  • clock

    An object implementing the clock interface used by the scheduler. By default, a monotonic clock is used.

  • timer

    An object implementing the timerfd interface used by the loop. By default, Linux::Event::Timer is used.

RUNNING THE LOOP

run() / run_once($timeout_seconds) / stop()

run() enters the dispatch loop and continues until stop() is called.

run_once($timeout_seconds) runs at most one backend wait/dispatch cycle. The timeout is in seconds; fractions are allowed.

If you need stop() to reliably wake a blocking backend wait, call $loop->waker once during initialization. When present, stop() signals the waker to return promptly from a blocking wait.

WATCHERS

watch($fh, %spec) -> Linux::Event::Watcher

Create (or replace) a watcher for a filehandle.

Watchers are keyed internally by file descriptor (fd). Calling watch() again for the same fd replaces the existing watcher atomically.

Supported keys in %spec:

  • read - coderef (optional)

  • write - coderef (optional)

  • error - coderef (optional). Called on EPOLLERR.

  • data - user data (optional). Stored on the watcher to avoid closure captures.

  • edge_triggered - boolean (optional, advanced). Defaults to false.

  • oneshot - boolean (optional, advanced). Defaults to false.

    If true, the watcher uses EPOLLONESHOT-style semantics: after an event is delivered, the fd is disabled inside the kernel until it is re-armed. Re-arming is typically done by forcing an epoll MOD (for example by toggling disable_read/enable_read or disable_write/enable_write).

Handlers are invoked as:

read  => sub ($loop, $fh, $watcher) { ... }
write => sub ($loop, $fh, $watcher) { ... }
error => sub ($loop, $fh, $watcher) { ... }

unwatch($fh) -> bool

Remove the watcher for $fh. Returns true if a watcher was removed, false if $fh was not watched (or had no fd). Calling unwatch() multiple times is safe.

Dispatch contract

When the backend reports events for a file descriptor, the loop dispatches callbacks in this order (when applicable):

1. error
2. read
3. write

This order is frozen.

SIGNALS

signal($sig_or_list, $cb, %opt) -> Linux::Event::Signal::Subscription

Register a signal handler using Linux signalfd.

$sig_or_list may be a signal number (e.g. 2), a signal name ('INT' or 'SIGINT'), or an arrayref of those values.

Callback ABI (strict): the callback is always invoked with 4 arguments:

sub ($loop, $sig, $count, $data) { ... }

Only one handler is stored per signal; calling signal() again for the same signal replaces the previous handler.

Options:

  • data - arbitrary user value passed as the final callback argument.

Returns a subscription handle with an idempotent cancel method.

See Linux::Event::Signal.

WAKEUPS

waker() -> Linux::Event::Wakeup

Returns the loop's singleton waker object (an eventfd(2) handle) used to wake the loop from another thread or process.

The waker is created lazily on first use and is never destroyed for the lifetime of the loop.

Important: when the waker is created via $loop->waker, the loop also installs an internal read watcher that drains the wakeup fd. This guarantees that $waker->signal (and $loop->stop after waker creation) can reliably wake a blocking backend wait (for example, epoll_wait).

Because watchers are keyed by file descriptor and calling watch() replaces the existing watcher for that fd, user code MUST NOT call $loop->watch($waker->fh, ...). The wakeup fd is reserved for loop wakeups and is managed internally.

See Linux::Event::Wakeup.

PIDFDS

pid($pid, $cb, %opts) -> Linux::Event::Pid::Subscription

Registers a pidfd watcher for $pid.

Callback ABI (strict): the callback is always invoked with 4 arguments:

sub ($loop, $pid, $status, $data) { ... }

If reap => 1 (default), the loop attempts a non-blocking reap of the PID and passes a wait-status compatible value in $status. If reap => 0, no reap is attempted and $status is undef.

This is a one-shot subscription: after a defined status is observed and the callback is invoked, the subscription is automatically canceled.

Replacement semantics apply per PID: calling pid() again for the same $pid replaces the existing subscription.

See Linux::Event::Pid for full semantics and caveats (child-only reaping).

TIMERS

Timers use a monotonic clock.

after($seconds, $cb) -> $id

Schedule $cb to run after $seconds. Fractions are allowed.

Timer callbacks are invoked as:

sub ($loop) { ... }

at($deadline_seconds, $cb) -> $id

Schedule $cb at an absolute monotonic deadline in seconds (same timebase as the clock used by this loop). Fractions are allowed.

cancel($id) -> bool

Cancel a scheduled timer. Returns true if a timer was removed.

NOTES

Threading and forking helpers

Linux::Event intentionally does not provide $loop->thread or $loop->fork helpers. Concurrency helpers are policy-layer constructs and belong in separate distributions. The core provides primitives (waker, pid) that make such helpers straightforward to implement in user code.

VERSION

This document describes Linux::Event::Loop version 0.007.

AUTHOR

Joshua S. Day

LICENSE

Same terms as Perl itself.

STABILITY

As of version 0.007, the public API and the following contracts are frozen:

  • I/O watcher callback ABI and dispatch order (error, then read, then write)

  • Timer callback ABI (($loop))

  • Signal callback ABI (($loop, $sig, $count, $data)) and replacement semantics per signal

  • Wakeup (waker) single-instance contract and reliable stop() wake guarantee

  • Pid subscription callback ABI (($loop, $pid, $status, $data)) and replacement semantics per PID

Future releases will be additive and will not change existing behavior.