Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Service Managers Compared — Exit Statuses, Signals, and Restarts

Four managers, four supervision philosophies. systemd policies restarts with an exit-code vocabulary; OpenRC makes supervision an opt-in module (supervise-daemon) bolted onto a runlevel engine; runit and s6 always restart and push interpretation of a death into your ./finish script (s6 adding one documented exception: finish exit 125). The per-manager details live in systemd.md, openrc.md, runit.md, and s6.md; the broader init-system comparison is ../../init-systems/comparison.md. Behavior marked (config) is set per service; (design) is fixed by the supervisor’s model.

The one table that matters (SM-012)

DimensionsystemdOpenRC (+ supervise-daemon)runits6
Supervision modelcgroup per unit; one manager process; main PID trackeds-s-d: none (start/stop only) (config); supervise-daemon: parent-child fork of a foreground daemontree: runit(PID 1) → runsvdir → runsv per service dirtree: s6-svscan → s6-supervise per service dir
Restart policy modelpolicy matrix Restart= (default no) (config); success/failure/abnormal/watchdog classesrespawn always on unexpected exit (design); unsupervised services never restartrestart always, regardless of exit code or signal (design)restart always, unless ./finish exits 125 (permanent failure) or -O (design)
Clean vs abnormalexit 0 + SIGHUP/INT/TERM/PIPE (non-oneshot) + SuccessExitStatus= = clean; other signals = abnormal; wait-status split exited/killed/dumpednone — death is binary; logged as “exited with return code N” vs “terminated by signal N”not classified; ./finish gets $1 exit code (or -1), $2 = signal number or 0not classified by default; ./finish gets $1 exit code (or 256), $2 = signal number
Exit-code-based restart conditionsSuccessExitStatus=, RestartPreventExitStatus=, RestartForceExitStatus= (codes, status names, signal names)nonenonefinish exit 125 only
Stop signal configKillSignal= (SIGTERM), RestartKillSignal=, SendSIGHUP=; SIGCONT always follows (config)stopsig= (SIGTERM) (config); stopgroup= for process groupfixed TERM+CONT on d/x; control chars for others; custom control/t hooksdown-signal file (SIGTERM default) + SIGCONT (config)
Stop timeout configTimeoutStopSec= (default DefaultTimeoutStopSec= 90s) (config)retry= schedule, e.g. SIGTERM/5 (config)none — sv waits 7 s by default, that’s allnone by default — never force-killed unless timeout-kill is set
Escalation to SIGKILLFinalKillSignal= (SIGKILL) after timeout, unless SendSIGKILL=no (config)only if your retry schedule includes SIGKILL (default SIGTERM/5 does not)only explicit sv kill / control kSIGKILL after timeout-kill ms (pid or group via flag-timeout-killpg), or explicit s6-svc -k
Restart limits / loop brakeStartLimitIntervalSec= (10s) + StartLimitBurst= (5) → refuse + StartLimitAction= (reboot…); systemctl reset-failed flushesrespawn_delay=0, respawn_max=10 (0=unlimited), respawn_period=12s; give-up logged to syslognone — 1 s anti-busyloop delay only (design)none — 1 s minimum respawn delay; max-death-tally records deaths for external throttling
Restart timingRestartSec= 100 ms; opt-in backoff RestartSteps=/RestartMaxDelaySec= (v254+)respawn_delay + per-retry step 128 ms capped 30 sfixed 1 s if the service exits immediatelyfixed 1 s if not ready when it dies
Failure reportingfailed state; systemctl status/show; Result= / $SERVICE_RESULT vocabulary; $EXIT_CODE/$EXIT_STATUS to hooksrc-service status / rc-status; supervise-daemon syslog lines; no failed-state machinesv status state word (run/down/finish), flags, sv exit codess6-svstat (incl. last exitcode/signal), s6-svdt death tally, ./event fifodir
Readiness notificationType=notify (sd_notify, READY=1), watchdognotify=fd:N or socket:ready (READY=1 datagram)./check program for sv availability (exit 0)notification-fd + s6-notifyoncheck; s6-svstat -o up,ready
Zombie reapingPID 1 manager reaps all unit processessupervise-daemon reaps its direct childrunsv reaps ./run/./finish; orphans re-parent to PID 1explicit: s6-svscan reaper waits every child (“especially important when run as process 1”); s6-supervise reaps run/finish
Logging modeljournald (manager-integrated)files / output_logger= / --stdout-loggerlog/ subservice → svlogd rotationlog/ subservice → s6-log pipeline, log-loss-averse

Reading the table — three mental models

  1. Policy-driven (systemd). The manager owns a vocabulary of exit codes and signals and a policy engine (Restart= × rate limiter × escalation chain). Power: per-condition behavior (RestartPreventExitStatus=75 250 SIGKILL). Cost: defaults matter — Restart=no, 90 s stop timeout, control-group teardown.
  2. Opt-in supervision (OpenRC). The base system never restarts anything; supervise-daemon adds runit-style always-restart with a respawn window, and health checks. Exit codes influence nothing except your own scripts.
  3. Always-restart supervision (runit, s6). The manager never interprets a death — it just re-runs ./run, bounded only by a 1 s anti-busyloop delay. All interpretation is delegated to ./finish (and in s6, to the single documented 125 permanent-failure contract). No restart limits by design: braking loops is your script’s job or an external watchdog’s.

Which failure signals are observable where

QuestionsystemdOpenRCrunits6
“What was the last exit code?”systemctl show -p ExecMainStatus,Resultsupervise-daemon sysloginfer via ./finish loggings6-svstat -o exitcode
“Was it killed by which signal?”`Result=signalcore-dump+$EXIT_STATUS`supervise-daemon syslog./finish $2
“How many times has it died recently?”journal + start-limit statesupervise-daemon logsnot recordeds6-svdt (tally up to 4096)
“Why did it stop restarting?”Result=start-limit-hitsyslog “respawned too many times”never stops restartingfinish exit 125 / -O (wantedup=false)

Further reading in this part

References