5.3 The Pre-Exec Sequence
Everything between "peinit decides to start service X" and "X's binary is running". The service is in Starting throughout.
5.3.1 Step 1: Arm the start timeout #
StartTimeout covers the entire remaining sequence — pre-hooks, fork,
exec, and the readiness wait. A single deadline, measured from the
operation's creation rather than from this point, bounds all of it.
Expiry aborts the start and kills the service's cgroup tree. The cause
recorded depends on where the deadline landed: a timeout during
pre-hooks is PreHookFailure; one during the readiness wait or the main
process's execution is ReadinessTimeout.
5.3.2 Step 2: Provision runtime directories #
If the definition lists RuntimeDirectories, peinit creates each one
under /run and applies its descriptor (§3.3) before anything else in
the launch. Failure classifies as ParentSetupFailure.
5.3.3 Step 3: Run pre-exec hooks #
Each ExecStartPre command runs in sequence, forked into hooks/. A
token is materialised for each at the point of use, from HookIdentity
if set and Identity otherwise (§4.1); a failure there fails the hook
and the service with PreHookFailure.
Any hook exiting non-zero kills the entire service cgroup tree —
cleaning up whatever grandchildren the hook left — and fails the service
with PreHookFailure.
When every hook has succeeded, peinit kills the hooks/ sub-cgroup
before the main process starts, so a hook that forked something into the
background does not become part of the service.
5.3.4 Step 4: Materialise the service token #
The main process's token, per §4.1. A failure means no child exists and
the service fails with ParentSetupFailure.
If a later parent-side step fails before the fork, peinit closes the token descriptor exactly once. A failure to close is retained as cleanup evidence and does not change how the start is classified — the classification describes what went wrong with the start, and a failed close is a separate fact about the same failure.
5.3.5 Step 5: Create the cgroups and the error pipe #
The main/ and health/ sub-cgroups are created, and a
pipe2(O_CLOEXEC) error pipe. The parent keeps the read end,
non-blocking; the child will hold the write end.
The pipe is how the child reports a failure it hits after fork but
before exec. If exec succeeds, the write end closes automatically —
that is what O_CLOEXEC is for — and the parent reads EOF, which means
setup succeeded. If setup fails, the child writes a structured error
first.
The payload is exactly eight bytes, written with one write(2):
| Bytes | Content |
|---|---|
| 0–3 | little-endian u32, the child setup step identifier |
| 4–7 | little-endian i32, a positive Linux errno |
The child writes at most one payload — the first reportable failure —
and then exits. The parent treats a non-EOF payload that is not exactly
eight bytes, or carries an unknown step identifier, or an errno of zero
or less, as malformed evidence and fails closed with PreExecFailure.
The step identifiers:
| Id | Step |
|---|---|
| 1 | Close the read end of the error pipe |
| 2 | Set the standard streams |
| 3 | Reset the signal environment |
| 4 | Install the service token |
| 5 | Set the resource limits |
| 6 | Set oom_score_adj |
| 7 | Set the working directory |
| 8 | reserved |
| 9 | Confirm NOTIFY_SOCKET |
| 10 | Inject stored descriptors |
| 11 | Exec the binary |
| 12 | Create a session |
| 13 | Acquire the controlling terminal |
Identifier 8 is reserved and never emitted: the environment is built in
the parent and applied by execve, so there is no step in the child
that could fail. Identifiers 12 and 13 are the two terminal steps, which
occur only for a service with a TTYPath.
If pipe2 fails, no child exists and the service fails with
ParentSetupFailure.
5.3.6 Step 6: Fork #
clone3(CLONE_PIDFD | CLONE_INTO_CGROUP), targeting main/. This does
two things atomically: it returns a pidfd for the child, and it places
the child directly into main/ at creation. There is no window in which
the child exists without a pidfd, and none in which it runs or execs in
peinit's own cgroup.
Placing the child at creation also avoids the alternative, which would
be having the child write its own PID into cgroup.procs — something
its post-installation token could not do.
A clone3 failure means no child exists: ParentSetupFailure, with
EMFILE/ENFILE, EAGAIN and ENOMEM the usual causes.
Immediately after clone3 returns, in the parent and on either outcome,
peinit closes the token descriptor and the main/ cgroup descriptor,
exactly once each. The child relies on close-on-exec and its own exit
instead. A failure to close is cleanup evidence and does not change how
the start is classified.
5.3.7 Step 7: The parent after the fork #
- Close the write end of the error pipe.
- Register the read end with the event loop. peinit does not block on it — signals, control traffic, timers and log pipes stay serviceable while the child's setup is pending.
- When the read end becomes readable or hangs up, read it
non-blocking:
- Would block: leave the source registered, the job stays in pending setup.
- EOF: exec succeeded. Record the pidfd on the job, emit
job.started, and only then apply readiness side effects such as Simple/Alive activation. - Data: setup failed. Parse the step and errno, log the specific
failure, fail the service with
PreExecFailure.
While setup is pending the job is not Running, and no dependent that waits on this service's readiness is released.
5.3.8 Steps 8 and 9 #
The child's own path is §5.4. What follows exec is the readiness wait and the post-readiness work:
- Simple with
Readiness=Notify: wait forREADY=1, then Active. - Simple with
Readiness=Alive: Active as soon as exec succeeds. - Oneshot: wait for the exit. Success is Completed; without
RemainAfterExitit then goes Inactive once dependents are released. A non-zero exit is Failed.
On readiness or successful exit, peinit runs ExecStartPost in sequence
into hooks/, releases the service's dependents, kills hooks/, and —
for a Simple service only — arms the watchdog if WatchdogTimeout is
non-zero and the health check timer if HealthCheck is set.
A post-hook that fails is logged and does not fail the service. The state transition to Active or Completed happens before the post-hooks run, so a service is already Active while its post-hooks are executing.
5.3.9 Failures before the fork #
Steps 2, 4, 5 and 6 can all fail with no child in existence. peinit
handles all four entirely in the parent: clean up whatever cgroups were
created, fail the service with ParentSetupFailure, and return the
error to the caller. These are system-level resource problems — file
descriptor limits, PID limits, memory, cgroup filesystem errors — rather
than anything about the service.