NAME
Linux::Event::Kernel::Signal - Handle Unix signals through the event loop
SYNOPSIS
use v5.36;
use POSIX qw(SIGINT SIGTERM);
use Linux::Event::Loop;
use Linux::Event::Kernel::Signal;
my $loop = Linux::Event::Loop->new;
my $shutdown = Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => [SIGINT, SIGTERM],
on_signal => sub ($self, $number, $count) {
say "Received signal $number";
$loop->stop;
},
);
$loop->run;
DESCRIPTION
Linux::Event::Kernel::Signal lets a Linux::Event application respond to Unix signals as normal event-loop callbacks.
For example, a server can respond to SIGINT and SIGTERM without putting application Perl code inside an asynchronous %SIG handler:
my $shutdown = Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => [SIGINT, SIGTERM],
on_signal => sub ($self, $number, $count) {
$listener->close;
$loop->stop;
},
);
Linux::Event uses Linux signalfd internally.
The important application-level difference is that on_signal runs as ordinary Loop work.
It does not interrupt arbitrary Perl code in the middle of execution.
WHY USE A SIGNAL OBJECT?
Traditional Perl signal handling often looks like:
$SIG{TERM} = sub {
...
};
That callback is associated with asynchronous process-signal delivery.
With Linux::Event, the signal is instead consumed through the event loop:
my $signal = Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => SIGTERM,
on_signal => sub ($self, $number, $count) {
...
},
);
The application callback therefore runs from normal Linux::Event dispatch.
This is generally much easier to reason about when the callback needs to:
close a Listener
write to a Stream
cancel a Timer
modify application state
stop the Loop
CHOOSING SIGNALS
signals
signals is required.
It may contain one numeric signal:
signals => SIGTERM
or an array reference:
signals => [SIGINT, SIGTERM]
Signal constants are commonly imported from POSIX:
use POSIX qw(SIGINT SIGTERM SIGHUP);
For example:
my $signal = Linux::Event::Kernel::Signal->new(
signals => [SIGHUP, SIGTERM],
on_signal => sub ($self, $number, $count) {
...
},
);
Duplicate numbers are removed automatically.
Unsupported signals
SIGKILL and SIGSTOP cannot be handled through signalfd and are rejected.
Signal numbers must be positive integers supported by the platform.
THE CALLBACK
on_signal
The callback receives three arguments:
on_signal => sub ($self, $number, $count) {
...
}
They are:
$self-
The Linux::Event::Kernel::Signal object.
$number-
The numeric signal that was received.
$count-
The number of
signalfdrecords observed for that signal during the current complete drain.
For example:
my $shutdown = Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => [SIGINT, SIGTERM],
on_signal => sub ($self, $number, $count) {
say "Signal $number arrived";
$self->loop->stop;
},
);
WHAT COUNT MEANS
For most ordinary signals, $count will usually be one.
However, Linux::Event drains all currently available signalfd records before performing semantic callback delivery and aggregates records for the same signal number.
For real-time signals, several queued records may therefore produce:
$count > 1
Ordinary Unix signals may already have been coalesced by the kernel before signalfd observes them.
Therefore $count means:
records Linux::Event actually observed
not:
number of kill() calls attempted by other code
ONE OBJECT CAN WATCH SEVERAL SIGNALS
A single Signal object can subscribe to several numbers:
my $signal = Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => [SIGINT, SIGTERM, SIGHUP],
on_signal => sub ($self, $number, $count) {
if ($number == SIGHUP) {
reload_configuration();
return;
}
$loop->stop;
},
);
The callback's $number tells you which subscribed signal was received.
SEVERAL OBJECTS CAN WATCH THE SAME SIGNAL
Several Signal objects on the same Loop may subscribe to the same signal.
For example:
my $logger = Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => SIGTERM,
on_signal => sub ($self, $number, $count) {
log_shutdown_request();
},
);
my $shutdown = Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => SIGTERM,
on_signal => sub ($self, $number, $count) {
$loop->stop;
},
);
Both subscribers receive the observed signal.
Subscribers for one signal are called in attachment order.
A callback may safely cancel itself or another Signal object.
If a later subscriber is cancelled before its turn, it is skipped.
ONE LOOP OWNS A SIGNAL NUMBER
A particular signal number may be owned by only one Linux::Event Loop in a process.
This is because reading a signalfd consumes its notifications.
Therefore this is supported:
Loop A
Signal object 1 -> SIGTERM
Signal object 2 -> SIGTERM
but Linux::Event does not allow:
Loop A -> SIGTERM
Loop B -> SIGTERM
at the same time in the same process.
Multiple subscribers should use the same owning Loop.
CONSTRUCTOR CALLBACKS OR SUBCLASS METHODS
A Signal can use a constructor callback:
my $signal = Linux::Event::Kernel::Signal->new(
signals => SIGTERM,
on_signal => sub ($self, $number, $count) {
...
},
);
or a subclass:
package ShutdownSignal;
use parent 'Linux::Event::Kernel::Signal';
sub on_signal ($self, $number, $count) {
$self->data->{listener}->close;
$self->loop->stop;
}
package main;
my $signal = ShutdownSignal->new(
loop => $loop,
signals => [SIGINT, SIGTERM],
data => {
listener => $listener,
},
);
A constructor on_signal callback overrides the subclass method for that particular object.
Constructor callbacks are usually simplest when the subscription needs lexical application state.
Subclassing is useful for reusable signal policy.
APPLICATION DATA
data
Arbitrary application state can be stored with the Signal:
my $signal = Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => SIGTERM,
data => {
listener => $listener,
},
on_signal => sub ($self, $number, $count) {
$self->data->{listener}->close;
},
);
Retrieve it with:
my $data = $signal->data;
While the Signal is nonterminal, it may also be replaced:
$signal->data($new_data);
Cancellation releases retained application data.
A cancelled Signal cannot retain new data.
ATTACHING TO A LOOP
A Signal may be attached during construction:
my $signal = Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => SIGTERM,
on_signal => sub ($self, $number, $count) {
...
},
);
or constructed detached:
my $signal = Linux::Event::Kernel::Signal->new(
signals => SIGTERM,
on_signal => sub ($self, $number, $count) {
...
},
);
and added later:
$loop->add($signal);
Once active, the Signal belongs to that Loop.
LOOP OWNERSHIP
An active Signal subscription is retained by its Loop.
This means an application does not need to retain an extra reference merely to keep an active signal subscription alive.
For example:
Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => SIGTERM,
on_signal => sub ($self, $number, $count) {
$loop->stop;
},
);
remains active because the Loop owns the subscription.
Cancellation removes it from the Loop's Signal service.
CANCELLING A SIGNAL SUBSCRIPTION
cancel
$signal->cancel;
Stop receiving the subscribed signals for this object.
Cancellation is terminal.
Calling cancel more than once is harmless.
A Signal may cancel itself:
on_signal => sub ($self, $number, $count) {
do_once();
$self->cancel;
}
or another Signal:
on_signal => sub ($self, $number, $count) {
$other_signal->cancel;
}
Cancellation during dispatch is safe.
LIFECYCLE
A Signal has three public states:
unattached
active
cancelled
state
my $state = $signal->state;
Return the current lifecycle state.
is_active
if ($signal->is_active) {
...
}
Return true while the subscription is active.
is_terminal
if ($signal->is_terminal) {
...
}
Return true after cancellation.
A cancelled Signal cannot be reattached.
INSPECTING SUBSCRIBED SIGNALS
signals
my $numbers = $signal->signals;
Return an array reference containing the numeric signals subscribed by this object.
For example:
for my $number (@{ $signal->signals }) {
say "Watching signal $number";
}
LOOP
loop
my $loop = $signal->loop;
Return the owning Linux::Event::Loop while attached.
SIGNAL MASKS
This section matters when combining Linux::Event with lower-level signal or thread code.
Linux signalfd receives signals by having those signals blocked in the consuming thread's signal mask.
Linux::Event manages that blocking automatically for signals it owns.
When the first Linux::Event subscription for a signal is attached, Linux::Event records whether that signal was already blocked.
When the last subscription is removed, Linux::Event restores only the mask state that Linux::Event itself changed.
For example, if your application had already blocked SIGTERM before creating the Signal object, Linux::Event does not later decide that it should become unblocked.
This preserves application-owned signal-mask policy.
DO NOT ALSO EXPECT %SIG TO HANDLE THE SAME SIGNAL
A Signal subscription is not an additional observer layered on top of an ordinary Perl %SIG handler.
For example, do not design code around both:
$SIG{TERM} = sub {
...
};
and:
Linux::Event::Kernel::Signal->new(
loop => $loop,
signals => SIGTERM,
...
);
receiving the same notification.
The signal is blocked for signalfd consumption while Linux::Event owns it.
Use the Linux::Event Signal callback as the application's handler for that signal.
THREADS
Signal masks are per-thread.
If an application creates its own worker threads, the easiest model is to establish the Linux::Event Signal subscriptions before creating those threads.
New threads then inherit the blocked signal mask.
If threads already exist, the application is responsible for ensuring that signals intended for signalfd are blocked consistently in threads that could otherwise receive them.
Linux::Event's own resolver workers arrange their signal masks so they do not accidentally consume application Signal traffic.
Perl ithreads are not required to use Linux::Event::Kernel::Signal.
LOOP-AWARE FORKING
Signal currently supports only the default parent-only behavior during Linux::Event::Loop managed fork.
It does not currently support:
share
clone
move
A Signal should therefore be omitted from those disposition lists.
For example:
my $pid = $loop->fork(
clone => [$timer],
);
An active Signal that is not listed remains active in the parent.
The inherited child Signal service is discarded during child Loop reconstruction.
Linux::Event also restores child-side signal-mask entries that it had blocked for the discarded service.
The inherited Signal object is not active in the child.
An ordinary CORE::fork does not make an inherited Linux::Event Signal service safe to reuse.
Use the managed Loop fork contract when forking a process that already owns Linux::Event resources.
SHARED SIGNALFD SERVICE
Applications normally do not need to know how many signalfd descriptors are used.
All Signal objects attached to one Loop share one private nonblocking signalfd service.
For example:
100 Signal objects
do not imply:
100 signalfd descriptors
The shared service keeps subscriber lists for each signal number and fans an observed signal out to the appropriate Signal objects.
Several native records may also be drained and aggregated before entering Perl callbacks.
DELIVERY ORDER
When one signalfd drain contains several different signal numbers, Linux::Event delivers them in numeric signal order.
For one signal number, subscribers are called in attachment order.
This gives dispatch deterministic ordering without requiring applications to depend on the order in which separate kernel records happened to be read.
PERFORMANCE MODEL
Each Loop uses one shared native Signal service rather than one signalfd per Signal object.
A readiness event enters the native service once, drains available signalfd_siginfo records, aggregates them by signal number, and then invokes the semantic Perl callbacks.
Constructor callbacks are retained for their objects and subclass methods are cached rather than looked up repeatedly for every signal record.
These details normally require no application action.
SEE ALSO
Linux::Event, Linux::Event::Loop, Linux::Event::Kernel::Timer, Linux::Event::Kernel::Process, docs/SIGNAL-DESIGN.md.