Minimal Embedded Init Processes: Reap Child Exits Without Losing Service Failure Status

A minimal init or supervisor must retain each child’s termination result while removing the kernel’s waitable record. Treat SIGCHLD as a reason to inspect children, not as a one-signal-per-exit ledger. Assign one reaping owner, drain available statuses, and record the service generation before deciding whether to restart it.

This guide uses Linux process semantics documented in Linux man-pages 6.12. It addresses a custom init or supervisor’s child accounting, not installation of startup scripts. An executed fixture used an ordinary x86-64 parent on Linux 6.18.44; it did not run as PID 1, change subreaper state or exercise a production service.

Separate process ownership from service identity

Keep a record for each supervised launch: logical service, launch generation, child PID, start state, expected shutdown state and restart eligibility. A service name is not a unique process lifetime. If generation 12 exits after generation 13 has been prepared, its result must remain attached to generation 12.

Define which processes the supervisor can actually wait for. A normal parent can collect its children; it cannot collect every process visible in a process listing. Orphan adoption depends on the hierarchy. The PR_SET_CHILD_SUBREAPER documentation describes adoption by the nearest living ancestor subreaper. That attribute is not inherited by forked children, although it survives exec. A supervisor outside this arrangement should not promise to reap arbitrary daemon grandchildren.

Prefer foreground services when that fits the product. A launcher’s successful exit may otherwise mean only that it forked a background process. Document whether the supervised identity is the launcher, actual worker or an adopted descendant, and how readiness is established separately from launch success.

A coalesced child-exit notification wakes one reaper, which drains and classifies waitpid results before matching service generations and applying recovery policy.
Original supervisor model. The signal count is not the number of child termination records.

Decode the wait result before decoding status

The wait and waitpid contract distinguishes a returned child PID, zero under WNOHANG, and minus one with an error. The status integer is meaningful only when a child result was returned. Do not reuse the previous iteration’s status after a zero or error return.

Observation Required interpretation
waitpid returns a positive PID Match the child record and classify its status before continuing the drain.
Zero with WNOHANG No matching child is currently waitable. Existing children may still be running.
Minus one, ECHILD No eligible unreaped child remains; also investigate unexpected competing reapers or discarded-status policy.
Minus one, EINTR Apply the retry/control policy without reading an invalid status.
Another error Record a supervisor defect or unsupported invocation; do not classify it as service success.

For a normal exit, first test WIFEXITED, then obtain WEXITSTATUS. Only the low eight bits of the exit argument are retained. For signal termination, test WIFSIGNALED before WTERMSIG. Keep those categories distinct instead of manufacturing a universal shell-style number such as 128 plus signal. Such an encoding may be an interface convention, but it is not the raw wait status.

If stopped or continued states are requested, handle WIFSTOPPED and WIFCONTINUED without treating them as termination. A traced child’s stop can also be reported under the documented conditions. WCOREDUMP is conditional on platform support and a signaled result; it should not be used as evidence that a retrievable core file exists at a particular path.

Drain all available children under one owner

A practical event-loop design blocks SIGCHLD before launching children and uses a synchronous signal-consumption mechanism to wake the reaper. On notification, repeatedly call waitpid with selector -1 and WNOHANG until there is no immediately available result. This is a proposed design, not complete init code. Its signal-mask setup, shutdown path and interactions with libraries still require review.

Standard signal semantics allow repeated instances of a pending standard signal to coalesce. Three child exits therefore need not produce three distinguishable SIGCHLD deliveries. The waitable child records are the authoritative results. A single wait after each notification can leave zombies indefinitely when no new notification arrives.

Do not let a generic library thread, asynchronous handler and service manager all call waitpid(-1) independently. Any one can consume a result expected by another. Centralize reaping and dispatch decoded records to interested components, or partition wait ownership with an explicitly verified scheme. Register a newly launched child before the reaper can process its result; a fast-exiting child is a normal case.

Preserve results instead of suppressing zombies blindly

Explicitly selecting SIG_IGN for SIGCHLD or using SA_NOCLDWAIT can prevent zombies, but also removes the normal wait-status path. This differs from merely leaving SIGCHLD at its default disposition, despite the default action being described as ignore. The distinction is documented in wait(2) and matters when service failure codes drive recovery.

Persist or enqueue the decoded termination record before deleting the supervisor’s child entry. Include whether shutdown had been requested, because an expected orderly stop and an unexpected crash may require different policies. Bound the result queue; silently dropping exit records to avoid blocking replaces a zombie problem with missing failure evidence.

Qualify bursts, restart races and adoption

An executed Python fixture created exactly three owned children with exit arguments 0, 7 and 23. The parent collected all three with waitpid, verified WIFEXITED and each decoded value, and observed ECHILD after the final reap. This checks ordinary host status extraction and cleanup only. It did not test signal termination, coalescing, orphan adoption or init-specific behavior.

For the target supervisor, add a bounded burst of short-lived children, one delayed child, an exit during startup registration, and shutdown while a restart timer is pending. Verify that every launch has exactly one terminal record and that a stale restart decision cannot launch an obsolete generation. Exercise a deliberately double-forking test service only in an isolated environment with the intended init/subreaper hierarchy.

Finally, distinguish reaping from successful service operation. Exit zero does not establish that a device transaction was committed; a restart does not restore application state automatically. The release evidence should contain the child-ownership map, decoded-status ledger, bounded restart policy and proof that no unreaped results remain after the test. A quiet process table alone cannot show whether failure information was preserved.

Similar Posts