NAME
Linux::Event::Kernel::Process - Spawn or monitor processes through the event loop
SYNOPSIS
use v5.36;
use Linux::Event::Loop;
use Linux::Event::Kernel::Process;
my $loop = Linux::Event::Loop->new;
my $process = Linux::Event::Kernel::Process->spawn(
loop => $loop,
command => ['/usr/bin/printf', "hello\n"],
stdout => 'pipe',
on_stdout => sub ($self, $bytes) {
print "child said: $bytes";
},
on_exit => sub ($self) {
say "exit code: " . $self->exit_code;
$loop->stop;
},
);
$loop->run;
DESCRIPTION
Linux::Event::Kernel::Process integrates process lifecycle and optional standard I/O with a Linux::Event::Loop.
There are two main ways to use it:
Spawn a new child with
spawn.Observe an existing process with
new(pid => ...).
A spawned Process can also manage asynchronous standard input, standard output, and standard error.
Linux::Event uses Linux pidfds to track the actual process rather than relying only on its numeric PID.
SPAWNING A CHILD
Use spawn when Linux::Event should create the process:
my $process = Linux::Event::Kernel::Process->spawn(
loop => $loop,
command => ['/usr/bin/make', '-j4'],
on_exit => sub ($self) {
...
},
);
command is required.
on_exit is also required unless the class provides an on_exit method.
COMMAND ARGUMENTS
command
command is an array reference containing the executable and its arguments:
command => [
'/usr/bin/git',
'status',
'--short',
]
Linux::Event does not automatically insert a shell.
For example:
command => ['echo', '$HOME']
passes the literal string $HOME as an argument. It does not perform shell variable expansion.
This is intentional and avoids shell quoting and injection surprises.
If shell syntax is actually desired, invoke a shell explicitly:
command => [
'/bin/sh',
'-c',
'printf "%s\n" "$HOME"',
]
WHEN THE CHILD IS ACTUALLY STARTED
A detached spawn call creates a Process specification but does not immediately start the child:
my $process = Linux::Event::Kernel::Process->spawn(
command => ['/usr/bin/sleep', '10'],
on_exit => sub ($self) {
...
},
);
At this point, $process->pid is undefined.
The child is created when the Process is attached:
$loop->add($process);
If loop is supplied to spawn:
my $process = Linux::Event::Kernel::Process->spawn(
loop => $loop,
command => ['/usr/bin/sleep', '10'],
on_exit => sub ($self) {
...
},
);
attachment happens as part of construction, so the child is started before spawn returns successfully.
WORKING DIRECTORY
cwd
Set the child's working directory with the top-level cwd option:
my $process = Linux::Event::Kernel::Process->spawn(
loop => $loop,
command => ['/usr/bin/make'],
cwd => '/srv/project',
on_exit => sub ($self) {
...
},
);
cwd must be a non-empty path.
ENVIRONMENT
env
Supply a complete replacement environment:
my $process = Linux::Event::Kernel::Process->spawn(
loop => $loop,
command => ['/usr/bin/env'],
env => {
PATH => '/usr/bin:/bin',
BUILD_MODE => 'test',
},
on_exit => sub ($self) {
...
},
);
When env is supplied, it replaces the child's complete environment. It is not merged with %ENV automatically.
To inherit the current environment, omit env.
If a mostly inherited environment with a few changes is desired, construct that hash explicitly:
env => {
%ENV,
BUILD_MODE => 'test',
}
STANDARD I/O
The stdin, stdout, and stderr options are top-level spawn options:
my $process = Linux::Event::Kernel::Process->spawn(
loop => $loop,
command => ['/usr/bin/some-program'],
stdin => 'pipe',
stdout => 'pipe',
stderr => 'pipe',
on_stdout => sub ($self, $bytes) {
...
},
on_stderr => sub ($self, $bytes) {
...
},
on_exit => sub ($self) {
...
},
);
Each standard stream defaults to inherit.
inherit
stdout => 'inherit'
The child inherits the corresponding parent standard descriptor.
Linux::Event does not create a Process pipe for it.
pipe
stdout => 'pipe'
Linux::Event creates a pipe and manages the parent end asynchronously.
Use this when application callbacks should receive output or write child input.
null
stdout => 'null'
Connect that child descriptor to /dev/null.
Filehandle
A caller-provided filehandle may be used directly:
stdout => $log_fh
The child receives that descriptor.
The caller retains ownership of its original Perl filehandle.
stderr => stdout
Child stderr may be merged into child stdout:
stdout => 'pipe',
stderr => 'stdout',
Both streams then use the child's stdout destination.
READING CHILD STDOUT
To receive stdout, configure stdout => 'pipe' and provide on_stdout:
my $process = Linux::Event::Kernel::Process->spawn(
loop => $loop,
command => ['/usr/bin/some-program'],
stdout => 'pipe',
on_stdout => sub ($self, $bytes) {
print "stdout: $bytes";
},
on_exit => sub ($self) {
...
},
);
on_stdout
on_stdout => sub ($self, $bytes) {
...
}
$bytes contains the next available chunk of stdout.
It is a byte stream. One callback does not necessarily correspond to one line or one write performed by the child.
If line-oriented parsing is desired, the application must provide that parsing or use an appropriate abstraction above Process.
READING CHILD STDERR
Configure:
stderr => 'pipe'
and provide:
on_stderr => sub ($self, $bytes) {
warn "stderr: $bytes";
}
As with stdout, callback boundaries are read boundaries rather than message or line boundaries.
OUTPUT EOF CALLBACKS
Optional callbacks may detect the end of each output pipe:
on_stdout_eof => sub ($self) {
say "stdout closed";
}
on_stderr_eof => sub ($self) {
say "stderr closed";
}
These callbacks require the corresponding descriptor to be configured as pipe.
WRITING CHILD STDIN
Configure:
stdin => 'pipe'
Then write with:
write_stdin
$process->write_stdin("hello\n");
Linux::Event attempts to write immediately.
If the child cannot currently accept all the bytes, the remainder is queued and written later.
Output order is preserved.
write_stdin may even be used while a spawned Process is still detached:
my $process = Linux::Event::Kernel::Process->spawn(
command => ['/usr/bin/my-program'],
stdin => 'pipe',
on_exit => sub ($self) {
...
},
);
$process->write_stdin("initial input\n");
$loop->add($process);
The queued bytes are written after the child is started and the pipe is attached.
CLOSING CHILD STDIN
close_stdin
$process->close_stdin;
close_stdin is graceful.
It:
Rejects new
write_stdincalls.Allows already accepted bytes to drain.
Closes the child's stdin pipe after those bytes have drained.
Closing the pipe delivers EOF to the child.
For example:
$process->write_stdin($request);
$process->close_stdin;
STDIN BACKPRESSURE
Process stdin has high- and low-watermark flow control.
When queued stdin grows beyond the high watermark, write_stdin begins returning false.
The bytes are still accepted unless the hard pending-input limit would be exceeded.
When the queue later falls to the low watermark, on_stdin_drain fires:
on_stdin_drain => sub ($self) {
# It is reasonable to produce more child input again.
}
pending_stdin_bytes
my $bytes = $process->pending_stdin_bytes;
Return the number of bytes currently waiting to be written to child stdin.
EXIT CALLBACK
on_exit
Every Process requires an effective on_exit callback:
on_exit => sub ($self) {
...
}
on_exit runs after Linux::Event observes the process exit.
For a spawned child with stdout or stderr pipes, Linux::Event first drains bytes that are already available at the current nonblocking boundary and closes its remaining Process-owned stdio handles.
Then on_exit runs.
The owning Loop is still available during the callback:
on_exit => sub ($self) {
$self->loop->stop;
}
After on_exit completes, the Process releases its Loop reference.
EXIT STATUS
For a child whose status Linux::Event reaps, inspect the termination result with the following methods.
exit_code
my $code = $process->exit_code;
Defined when the child exited normally:
on_exit => sub ($self) {
if (defined(my $code = $self->exit_code)) {
say "exited with code $code";
}
}
term_signal
my $signal = $process->term_signal;
Defined when the process was terminated by a signal:
on_exit => sub ($self) {
if (defined(my $signal = $self->term_signal)) {
say "terminated by signal $signal";
}
}
core_dumped
if ($process->core_dumped) {
...
}
Return true when the recorded wait status indicates that termination produced a core dump.
raw_status
my $status = $process->raw_status;
Return the conventional raw wait-status value when Linux::Event owns and obtains that status.
exited
if ($process->exited) {
...
}
Return true after a normal observed process exit.
SENDING A SIGNAL TO THE PROCESS
signal
Send a Linux signal using the Process's pidfd:
use POSIX qw(SIGTERM);
$process->signal(SIGTERM);
signal returns the Process object.
Linux::Event uses pidfd_send_signal rather than merely doing:
kill $signal, $pid;
This matters because a pidfd identifies the specific process instance.
A reused numeric PID cannot accidentally redirect the signal to an unrelated later process.
THERE IS NO GENERIC cancel
Process deliberately does not provide:
$process->cancel;
because several very different actions could be meant by "cancel":
Stop sending stdin.
Stop observing the child.
Ask the child to terminate politely.
Kill the child immediately.
Reap an exited child.
Linux::Event does not guess which policy the application wants.
For example, graceful shutdown might begin with:
$process->signal(SIGTERM);
The application then continues running the Loop until on_exit confirms that the process actually exited.
OBSERVING AN EXISTING PROCESS
Use new rather than spawn when the process already exists:
my $process = Linux::Event::Kernel::Process->new(
loop => $loop,
pid => $pid,
on_exit => sub ($self) {
say "process exited";
},
);
pid is required.
No stdin, stdout, or stderr management is provided in this mode.
The object observes lifecycle through a pidfd.
REAPING AN OBSERVED PROCESS
reap
The default is:
reap => 1
This means Linux::Event owns obtaining the child's wait status.
Use this for a process that is actually a child of the current process and whose wait status should be owned by this Process object:
my $process = Linux::Event::Kernel::Process->new(
loop => $loop,
pid => $child_pid,
reap => 1,
on_exit => sub ($self) {
say $self->exit_code;
},
);
Do not also use an independent wait, waitpid, or SIGCHLD reaper for that same child.
reap => 0
For a process whose wait status belongs elsewhere:
my $process = Linux::Event::Kernel::Process->new(
loop => $loop,
pid => $pid,
reap => 0,
on_exit => sub ($self) {
say "process is gone";
},
);
Linux::Event observes pidfd lifecycle but does not reap the process.
In that mode, decoded wait-status methods such as exit_code, term_signal, and raw_status remain undefined.
This is useful for a non-child process or when another component owns reaping.
PID
pid
my $pid = $process->pid;
Return the numeric process ID.
For an attached observed Process, this is the PID supplied to new.
For a spawned Process, it becomes available after the child has actually been created.
A detached spawn specification therefore has no PID yet.
ATTACHING TO A LOOP
Both spawning and observation may be configured detached.
For a spawn:
my $process = Linux::Event::Kernel::Process->spawn(
command => ['/usr/bin/sleep', '5'],
on_exit => sub ($self) {
...
},
);
$loop->add($process);
For an existing PID:
my $process = Linux::Event::Kernel::Process->new(
pid => $pid,
on_exit => sub ($self) {
...
},
);
$loop->add($process);
Supplying loop => $loop performs that attachment during construction.
CONSTRUCTOR CALLBACKS OR SUBCLASS METHODS
Callbacks can be supplied directly:
my $process = Linux::Event::Kernel::Process->spawn(
command => ['/usr/bin/make'],
stdout => 'pipe',
on_stdout => sub ($self, $bytes) {
print $bytes;
},
on_exit => sub ($self) {
...
},
);
or implemented in a subclass:
package BuildProcess;
use parent 'Linux::Event::Kernel::Process';
sub on_stdout ($self, $bytes) {
print "build: $bytes";
}
sub on_stderr ($self, $bytes) {
warn "build: $bytes";
}
sub on_exit ($self) {
if (defined(my $code = $self->exit_code)) {
say "build exited with $code";
}
else {
say "build received signal " . $self->term_signal;
}
}
A constructor callback overrides a same-named subclass method for that Process.
CALLBACKS
The complete callback set is:
on_exit($self)-
Required. The process exit was observed.
on_stdout($self, $bytes)-
Optional. Requires
stdout => 'pipe'. on_stderr($self, $bytes)-
Optional. Requires
stderr => 'pipe'. on_stdout_eof($self)-
Optional. The child stdout pipe reached EOF.
Requires
stdout => 'pipe'. on_stderr_eof($self)-
Optional. The child stderr pipe reached EOF.
Requires
stderr => 'pipe'. on_stdin_drain($self)-
Optional. Pending stdin fell to the configured low watermark after high-watermark backpressure.
Requires
stdin => 'pipe'. on_error($self, $error)-
Optional. An asynchronous Process or stdio error occurred.
$erroris a Linux::Event::Error.
ERROR HANDLING
on_error
An asynchronous error may be handled with:
on_error => sub ($self, $error) {
warn "child error: $error\n";
}
Linux::Event also retains the most recent error.
last_error
my $error = $process->last_error;
When no on_error callback exists, Linux::Event warns about asynchronous Process errors.
Some synchronous API failures, such as failure of signal, are thrown directly as structured Linux::Event::Error objects.
APPLICATION DATA
data
Attach arbitrary application state:
my $process = Linux::Event::Kernel::Process->spawn(
command => ['/usr/bin/make'],
data => {
build_id => 42,
},
on_exit => sub ($self) {
say "finished build " . $self->data->{build_id};
},
);
Retrieve or replace it with:
my $data = $process->data;
$process->data($new_data);
PROCESS STATE
state
my $state = $process->state;
Common lifecycle states are unattached, running, exited, and failed.
A Process discarded from a managed-fork child uses not_inherited as its terminal state there.
is_running
if ($process->is_running) {
...
}
Return true while the Process is actively being monitored.
is_terminal
if ($process->is_terminal) {
...
}
Return true after normal exit, terminal failure, or a managed-fork disposition that makes the object unusable in the current process.
LOOP OWNERSHIP
The Loop retains a running Process.
Dropping the application's reference does not silently stop process monitoring.
Conversely, destroying the Loop does not choose a signal and kill the child for you.
Process shutdown policy belongs to the application.
If your program owns children that must be reaped, keep the Loop running until their on_exit callbacks confirm completion.
PROCESS I/O TUNING
Process stdout/stderr and stdin queue policy have their own Process-specific settings.
They are not placed inside a nested tuning hash.
Reusable defaults belong in a Process subclass through process_options:
package BuildProcess;
use parent 'Linux::Event::Kernel::Process';
sub process_options ($class) {
return (
read_size => 131_072,
max_reads_per_tick => 32,
max_pending_stdin => 8_388_608,
);
}
For one spawned Process, the same option names may instead be supplied directly as top-level spawn options:
my $process = BuildProcess->spawn(
command => ['/usr/bin/make'],
stdout => 'pipe',
stdin => 'pipe',
read_size => 32_768,
max_reads_per_tick => 16,
max_pending_stdin => 2_097_152,
on_exit => sub ($self) {
...
},
);
Direct spawn values override the subclass defaults for that object.
read_size
Default: 65_536.
Maximum number of bytes in one stdout or stderr read callback payload.
max_reads_per_tick
Default: 64.
Maximum number of successful reads from each active child output pipe during one readiness dispatch.
This is a fairness control so one noisy child cannot monopolize the Loop indefinitely.
stdin_high_watermark
Default: 1_048_576.
Pending-stdin level above which write_stdin starts returning false to signal backpressure.
The bytes are still accepted unless the hard limit would be exceeded.
stdin_low_watermark
Default: 262_144.
After high-watermark backpressure has occurred, on_stdin_drain fires when pending stdin falls to or below this value.
It may not exceed stdin_high_watermark.
max_pending_stdin
Default: 0.
Hard limit on queued stdin bytes.
Zero means no hard queue limit.
If a nonzero hard limit would be exceeded, the unsent bytes are rejected, Process stdin is closed according to the Process error contract, and an output_limit error is reported.
SPAWN FAILURE SAFETY
Linux::Event spawning is designed so arbitrary Perl application code does not run in a post-fork child setup path.
Native spawning establishes the configured descriptors, environment, and working directory.
If the child has been created but Linux::Event cannot complete its Process setup, Linux::Event kills and reaps that exact child before propagating the setup failure.
Partially created Process-owned descriptors are also cleaned up.
This avoids leaving an accidentally unmanaged child behind after a failed attachment.
PIDFD PROCESS IDENTITY
A numeric PID can eventually be reused by Linux.
Process therefore opens or receives a pidfd and uses that kernel process identity for lifecycle notification.
signal also uses the pidfd.
This avoids the following race:
Process A exits.
Its numeric PID is reused by process B.
Application code signals the old numeric PID.
Process B receives the signal accidentally.
The pidfd continues to refer to the intended process instance.
LOOP-AWARE FORKING
Process currently supports only the default parent-only behavior during Linux::Event::Loop managed fork.
It does not currently support share, clone, or move.
An active Process should therefore be omitted from those disposition lists:
my $pid = $loop->fork(
clone => [$timer],
);
The Process remains active in the parent.
Its inherited child-side Loop registrations, pidfd, Process-owned stdio handles, callback state, and queued stdin are discarded during child reconstruction.
The child copy becomes terminal with state not_inherited.
This does not kill the real process being monitored by the parent.
PLATFORM
Process uses Linux pidfds.
The lifecycle and status path targets Linux 5.4 or newer.
The distribution also requires the build and libc support used by its native process-spawn implementation.
There is intentionally no fallback that runs arbitrary Perl child setup code after a traditional fork merely to support older process facilities.
IMPLEMENTATION MODEL
One Process object owns the pieces associated with one process:
Pidfd lifecycle notification.
Optional stdin pipe.
Optional stdout pipe.
Optional stderr pipe.
Pending stdin queue state.
Decoded exit status.
Process callbacks.
Application
data.
The child pipe descriptors are implementation details of the Process resource; they are not exposed as separate public Linux::Event::IO::Pipe objects.
Use Linux::Event::IO::Pipe directly when the application independently owns an unrelated pipe.
PERFORMANCE MODEL
Stdout and stderr mechanical read draining is handled below the semantic Perl callback boundary.
Each successful read still reaches the configured application callback as a byte string, while read_size and max_reads_per_tick preserve application chunking and fairness policy.
Callback methods and process_options are resolved and cached by concrete subclass. Constructor callbacks override them for one Process without adding per-event method lookup.
These details normally require no application action.
SEE ALSO
Linux::Event, Linux::Event::Loop, Linux::Event::IO::Pipe, Linux::Event::Kernel::Signal, Linux::Event::Error, docs/PROCESS-DESIGN.md.