14.4 Resource Exhaustion
peinit is single-threaded PID 1, so most resource pressure reaches it as a failure at a syscall rather than as slowness.
14.4.1 Descriptors #
peinit holds a descriptor per supervised process (a pidfd), two per
supervised process for output pipes, one per armed timer, one per
control connection, two per jobs connection (the socket and the peer's
pidfd), one per submitted job's output sink, plus the sockets, the
epoll instance and the signalfd.
Control connections are bounded
in all by MaxControlConnections (256), which is set for descriptors
rather than for fairness: what keeps one caller from taking them is
MaxControlConnectionsPerUser (§10.1). A submission also holds its
prepared token and attached descriptors from acceptance until the launch
takes them, which is bounded by MaxJobsPerSubmitter times the
descriptor cap per message.
EMFILE or ENFILE from pipe2 or clone3 fails the start with
ParentSetupFailure — a restart-eligible cause, so a service that
failed because the system was momentarily out of descriptors gets
another go.
The pre-start check helper's result descriptor and pidfd are both unregistered from the event loop and closed, so a service using filesystem conditions costs no descriptors beyond the start itself.
14.4.2 Processes #
EAGAIN from clone3 — the PID limit — is also ParentSetupFailure
and restart-eligible.
The one path that forks outside the launch machinery is the timer
last-run write, one child per firing of a persistent timer (§9.2).
The children _exit as soon as the write returns and peinit matches
each one's exit status back to its write, so a failure is reported
rather than discarded — but the fork itself is real and happens on every
firing.
It is a fork of PID 1, so the child briefly inherits everything peinit holds: every pidfd, the epoll instance, both sockets, every service's pipe read end. It uses none of them and exits immediately. Driving the write through an asynchronous LCS path instead would remove the fork entirely; there is no such path today.
14.4.3 Memory #
ENOMEM from clone3 behaves like the others.
peinit's own memory is bounded by design in the places that could otherwise grow without limit: the pre-eventd buffer has a fixed size and drops its oldest, there is no outbound queue for log delivery, terminal jobs and operations are dropped rather than retained, and neither has a history structure.
Nor do the two structures that once did. A graph execution context and
its operation associations are retired on the maintenance turn after
every member reaches a terminal state (§7.3), so a boot or an on-demand
start costs nothing lasting.
And an OnFailure chain
entry is cleared once its handler has held for a RestartWindow (§6.3),
so a resident handler does not permanently occupy a slot of that
failure's depth budget.
14.4.4 Disk #
A full root filesystem shows up in three places. The boot attempt counter cannot be written, which peinit treats as a counter of zero and continues — a failure to record an attempt is not itself worth escalating. The random seed cannot be saved at shutdown, which is recorded and does not block the shutdown. And registryd cannot write, which is registryd's problem and reaches peinit as a Critical service failing.
14.4.5 The OOM killer #
An ErrorControl=Critical service is marked OOM-immune, with
oom_score_adj at -1000; everything else is left at the default
(§5.4). A Critical service is one whose loss reboots the machine, so
letting the OOM killer pick it would turn memory pressure into a reboot.
peinit itself is PID 1 and the kernel will not choose it.