systemd Overview

systemd is the dominant init and service manager on modern Linux. Per systemd(1), it “is a system and service manager for Linux operating systems. When run as first process on boot (as PID 1), it acts as init system that brings up and maintains userspace services. Separate instances are started for logged-in users to start their services.” Where the old sysvinit ran a fixed sequence of shell scripts one after another, systemd models the whole system as a graph of units — services, sockets, targets, mounts, timers, slices, scopes, devices, paths — and brings them up in parallel, constrained only by explicit dependency and ordering declarations. Every service it starts is supervised inside its own cgroup, so the kernel — not fragile PID-file tracking — is the source of truth for which processes belong to a service. Startup is made fast and lazy by activation: a unit can be triggered on demand by an incoming socket connection, a D-Bus message, a device appearing, a timer firing, or a filesystem path changing. This note frames the systemd model and why it won; its components are deep-dived in sibling notes.

This is the orienting note for systemd; it deliberately stays at the model level. The unit abstraction itself is systemd Units, the long-running-process unit is systemd Service Units, the boot synchronization points are systemd Targets and the Boot Sequence, dependency/ordering semantics are systemd Dependencies and Ordering, the cgroup integration is systemd and cgroup Integration, lazy startup is systemd Socket Activation, and logging is The systemd Journal. What makes PID 1 special to the kernel is PID 1 and the init Process; the head-to-head against sysvinit/OpenRC is The Init System Comparison.

Mental Model

Stop thinking of init as a script and start thinking of it as a dependency-resolving graph engine with a supervisor attached. You declare what you want (units), and which units need which others (dependencies) and in what order they may start (ordering) — and systemd computes a valid parallel schedule and executes it, then keeps every started process alive in a cgroup it can monitor and restart. The boot “goal” is itself just a unit: default.target, a node with no behaviour of its own that simply pulls in everything that should be running. Reaching a usable system is reaching a target in the graph.

flowchart TB
  PID1["systemd (PID 1)<br/>graph engine + supervisor"]
  DEF["default.target<br/>(alias: multi-user / graphical)"]
  SI["sysinit.target"]
  BAS["basic.target"]
  MU["multi-user.target"]
  S1["sshd.service<br/>(own cgroup)"]
  S2["nginx.service<br/>(own cgroup)"]
  SK["nginx.socket<br/>(socket activation)"]
  TM["logrotate.timer"]
  PID1 --> DEF
  DEF --> MU
  MU --> BAS
  BAS --> SI
  MU --> S1
  MU --> SK
  SK -. "connection arrives" .-> S2
  BAS --> TM
  TM -. "fires" .-> X["logrotate.service"]

The systemd model as a graph reached at boot. What it shows: PID 1 activates default.target, which depends (transitively, through multi-user → basic → sysinit) on the services that constitute a running system; each service runs in its own cgroup; and some units (nginx.socket, logrotate.timer) exist to activate other units lazily — the socket starts nginx.service only when a connection arrives, the timer starts logrotate.service only when it fires. The insight to take: “booting” is just resolving and reaching a target node in a dependency graph, and much of what runs does so on demand rather than eagerly at boot — which is how parallelism and fast boots are achieved.

What a unit is, and the unit zoo

A unit is systemd’s atom of management — a named, typed object with a .<type> suffix, configured by a declarative INI-style unit file, that systemd can start, stop, and track. systemd(1) enumerates the unit types; each exists because systemd manages a fundamentally different kind of resource:

  • Service (.service) — “start and control daemons and the processes they consist of.” The workhorse: a long-running program, supervised, restartable.
  • Socket (.socket) — “encapsulate local IPC or network sockets… useful for socket-based activation.” systemd holds the listening socket and starts the service on first connection.
  • Target (.target) — “useful to group units, or provide well-known synchronization points during boot-up.” Behaviourless grouping nodes; the replacement for runlevels.
  • Device (.device) — “expose kernel devices in systemd… may be used to implement device-based activation.” Units appear/disappear as udev discovers hardware.
  • Mount (.mount) / Automount (.automount) — “control mount points” and “provide automount capabilities, for on-demand mounting.” systemd owns /etc/fstab mounting and lazy mounting.
  • Timer (.timer) — “useful for triggering activation of other units based on timers.” The cron replacement.
  • Swap (.swap) — “encapsulate memory swap partitions or files.”
  • Path (.path) — “activate other services when file system objects change or are modified.” inotify-driven activation.
  • Slice (.slice) — “group units which manage system processes in a hierarchical tree for resource management.” The cgroup tree’s branch nodes.
  • Scope (.scope) — “manage foreign processes instead of starting them.” Wraps processes systemd did not fork (e.g. a user session, a container) so they still live in a managed cgroup.

The slice/scope/service split is the cgroup story: services and scopes are leaf cgroups that hold processes, slices are branch cgroups that group and apportion resources. systemd builds a tree with -.slice at the root, then system.slice (system services), user.slice (per-user sessions), and machine.slice (containers/VMs) beneath it — “Processes systemd spawns are placed in individual Linux control groups named after the unit which they belong to in the private systemd hierarchy” (systemd(1)). The mechanics and resource knobs are systemd and cgroup Integration.

How startup actually works

When the kernel hands control to userspace, it execs PID 1 — “systemd is usually not invoked directly by the user, but is installed as the /sbin/init symlink” (init(1)). On startup systemd reads unit files from a search path, then — “On boot systemd activates the target unit default.target whose job is to activate on-boot services and other on-boot units by pulling them in via dependencies” (systemd(1)). default.target is normally a symlink to either multi-user.target (text/server) or graphical.target (desktop). The transitive dependency chain documented in bootup(7) flows local-fs.target → sysinit.target → basic.target → multi-user.target → (graphical.target), with display-manager.service or getty@.service at the leaf producing a login prompt — the target chain is systemd Targets and the Boot Sequence and The systemd Target Ordering sysinit basic multi-user.

The pivotal scheduling rule — the thing that makes systemd parallel — is that requirement and ordering are independent axes. systemd(1): “If only a requirement dependency exists between two units (e.g. foo.service requires bar.service), but no ordering dependency (e.g. foo.service after bar.service) and both are requested to start, they will be started in parallel.” You declare that you need something with Requires=/Wants= and when you need it with After=/Before= — and systemd parallelizes everything not explicitly ordered. The semantics (Requires vs Wants vs Requisite vs BindsTo; After vs Before; Conflicts) are systemd Dependencies and Ordering.

Unit file search path and precedence

Unit files are read from a layered set of directories, highest precedence first: /etc/systemd/system (administrator overrides), /run/systemd/system (runtime-generated), and /usr/lib/systemd/system (distribution/package-shipped) — “User configuration always takes precedence” (systemd(1); the man page also lists /usr/local/lib/systemd/system). A unit present in /etc shadows the same-named unit in /usr/lib, which is how an admin overrides a packaged service without editing package files; drop-in *.conf fragments in a unit.d/ directory layer additive overrides on top.

Activation — lazy, parallel startup

The deepest idea systemd brought from its origin (Lennart Poettering’s “Rethinking PID 1”) is activation: rather than starting every daemon eagerly and serially, create the interfaces daemons talk through and start the daemons only when something uses them. The five activation triggers are all unit types: socket-based (a .socket unit holds the listening socket; the service starts on first connection), bus-based (a D-Bus name request triggers the service), device-based (a .device appears via udev), path-based (a .path watches a filesystem object via inotify), and timer-based (a .timer). Socket activation is the load-bearing one and is its own note, systemd Socket Activation. As Poettering put it, “if we manage to make those sockets available for connection earlier and only actually wait for that instead of the full daemon start-up, then we can speed up the entire boot”, because “the kernel socket buffers help us to maximize parallelization, and the ordering and synchronization is done by the kernel, without any further management from userspace.”

Supervision and logging

Every service systemd starts is a child it wait()s on and tracks via the service’s cgroup, so it knows the moment a service dies and can apply a restart policy. Service output (stdout/stderr) and structured log records are captured by journald (systemd-journald), systemd’s logging daemon — “a journal log record is generated declaring the consumed resources whenever a unit shuts down” (systemd(1)). The journal is structured, indexed, and queryable per-unit (journalctl -u nginx); it is The systemd Journal. PID 1 itself is controllable at runtime over its D-Bus API (org.freedesktop.systemd1) — “The D-Bus API of systemd is described in org.freedesktop.systemd1(5)” — which is what systemctl speaks (see systemctl and Unit Management), and via signals: SIGTERM to PID 1 triggers a daemon-reexec, SIGINT triggers ctrl-alt-del.target, and a family of SIGRTMIN+n signals request specific shutdown/reboot/log-level modes (systemd(1)).

A minimal unit file

# /etc/systemd/system/myapp.service
[Unit]
Description=My example service
After=network-online.target          # ordering: don't start until the network is up
Wants=network-online.target          # requirement (weak): pull the target in, but
                                     #   don't fail if it can't be reached
[Service]
Type=notify                          # the daemon calls sd_notify(READY=1) when ready;
                                     #   systemd waits for that before considering it started
ExecStart=/usr/bin/myapp --serve
Restart=on-failure                   # supervision: relaunch if it exits non-zero
RestartSec=2s
MemoryMax=512M                       # cgroup resource limit, enforced by the kernel
[Install]
WantedBy=multi-user.target           # `systemctl enable` makes multi-user.target Want this

Line by line: the [Unit] section declares relationships — After= is ordering only (start after the network target is reached) while Wants= is a weak requirement (try to bring the target up, but don’t abort if it fails); the two together are the common idiom for “I need the network, but I won’t die if it’s flaky.” [Service] defines the process: Type=notify means systemd holds the unit in “activating” until the daemon explicitly signals readiness via the sd_notify protocol (so dependents ordered After=myapp.service start only once it is truly ready, not merely forked); Restart=on-failure is the supervision policy; MemoryMax=512M becomes a cgroup memory.max limit the kernel enforces. [Install] is not read at runtime — it is consulted by systemctl enable, which creates the symlink that makes multi-user.target Want this unit, i.e. start it at boot. Service-unit specifics (Type=, Exec*=, restart policy) are systemd Service Units.

Why systemd replaced sysvinit

The displacement was not fashion; it was four concrete deficiencies of the SysV model, each of which systemd answers structurally. The canonical argument is “Rethinking PID 1”:

  1. Serial startup was slow. sysvinit ran /etc/rc?.d scripts essentially one after another. systemd parallelizes everything not explicitly ordered, and defers much of it via activation.
  2. Shell scripts were wasteful. Poettering measured a typical boot invoking “grep at least 77 times. awk is called 92 times, cut 23 and sed 74” — each a fork+exec+library-load — observing “No other language but shell would do something like that.” Declarative unit files replace the scripts with a few key=value lines parsed once.
  3. Process supervision was unreliable. A double-forking daemon escapes PID-file tracking; init could not reliably tell whether a service was alive. systemd puts every service in a cgroup, and “cgroup membership is securely inherited by child processes, they cannot escape” — so the kernel authoritatively reports the service’s whole process tree, enabling correct restart and clean shutdown.
  4. No real dependency model. SysV encoded order crudely via S20/K80 filename-number prefixes and fixed runlevels. systemd has an explicit dependency graph and replaces the fixed runlevels with targets (it maps the legacy numbers for compatibility — “2, 3, and 4 are equivalent to systemd.unit=multi-user.target; and 5 is equivalent to systemd.unit=graphical.target”, per systemd(1)). The comparison with sysvinit, OpenRC, runit, and s6 is The Init System Comparison.

The cgroup-v2-only direction (dated)

systemd has steadily forced the move to the cgroup v2 unified hierarchy and away from the legacy v1 (and “hybrid”) layout. The legacy and hybrid hierarchies were deprecated in systemd v256, and v258 (released 2025-09-17) removed them outright: per the v258 NEWS, “Support for cgroup v1 (‘legacy’ and ‘hybrid’ hierarchies) has been removed. cgroup v2 (‘unified’ hierarchy) will always be mounted during system bootup and systemd-nspawn container initialization.” As of this writing the current release is systemd v262 (2026-06-19), per the main NEWS. The practical consequence is that on a contemporary systemd system, every resource-control knob a service, container, or Kubernetes node uses lives under the single unified /sys/fs/cgroup/ tree — the same primitive containers use. The cgroup integration is systemd and cgroup Integration.

Uncertain

Verify against the target distro: which systemd version a given 2026 distribution actually ships (Debian/Ubuntu/Fedora/RHEL track different points), and therefore whether cgroup v1 is merely deprecated or fully removed there. Reason: upstream removal (v258) precedes distro adoption; LTS distros lag. To resolve: check the distro’s shipped systemctl --version and release notes. The freedesktop man pages were 403 during this research, so the systemd(1)/init(1)/bootup(7) quotations were taken from man7.org mirrors — verify wording against freedesktop.org/software/systemd/man/latest/ when reachable. uncertain

Failure Modes and Common Misunderstandings

After= makes a unit a dependency.” It does not. After=/Before= are pure ordering — they say nothing about whether the other unit is started at all. You need Requires=/Wants=/BindsTo= for the requirement, and almost always both an ordering and a requirement together. Declaring only After=foo.service and expecting foo to be pulled in is the single most common unit-file bug. (systemd Dependencies and Ordering.)

Forgetting enable vs start. systemctl start runs a unit now; systemctl enable writes the [Install] symlinks so it starts at boot. They are independent — a started-but-not-enabled service vanishes on reboot, an enabled-but-not-started one does nothing until reboot.

Type=forking readiness races. A daemon that double-forks and exits the parent (the old idiom) needs Type=forking (and ideally a PIDFile=), or systemd misreads the parent’s exit as the service finishing. Modern services should use Type=notify (sd_notify) or Type=simple so readiness is unambiguous. This is exactly the supervision problem cgroups were brought in to solve.

Thinking systemd “is just an init.” It is an init and a service supervisor, a cgroup resource manager, a socket/timer/path activator, a mount manager, and the hub of an ecosystem of helper daemons (below). Treating it as a drop-in /etc/rc.local runner misses the model entirely.

The ancillary ecosystem (framed, not deep-dived)

“systemd” as shipped is a suite. PID 1 (the manager) is the core, but alongside it run a set of optional daemons that each own a slice of the platform — and which belong to other notes/MOCs, mentioned here only so the reader knows where the boundaries are: systemd-logind (login session and seat management — the user.slice/scope machinery), systemd-udevd (device-node management, feeding .device units), systemd-networkd (network configuration), systemd-resolved (DNS/nss), systemd-timesyncd (SNTP clock sync), and systemd-journald (logging, the one tightly bound to the manager — The systemd Journal). These are independently togglable; a minimal system runs PID 1 and journald and little else. The point of this note is the manager and its model; each helper is a sibling concern.

Alternatives and When to Choose Them

systemd is the default on Debian, Ubuntu, Fedora, RHEL, SUSE, and Arch — effectively the mainstream Linux desktop/server default. The alternatives persist in niches: sysvinit (legacy, still on some minimal systems), OpenRC (Gentoo/Alpine default; dependency-based but script-driven, lighter), runit and s6 (tiny, process-supervision-focused, popular in containers and minimalist distros like Void), and busybox init (embedded). The honest trade-off: systemd buys parallelism, robust supervision, socket activation, and a uniform management surface at the cost of size, complexity, and a large attack/feature surface as PID 1 — which is why minimal/embedded/container contexts often prefer the supervisors. The structured comparison is The Init System Comparison. Note that inside a container the relevant question is usually not “systemd vs sysvinit” but “do I even need an init?” — see Reaping Orphans and the subreaper for why a tiny reaping init (tini) is often the right PID 1 in a container rather than a full systemd.

Production Notes

systemd’s reach is the reason it is both loved and criticized in production. The upside is operational uniformity: systemctl status, journalctl -u, cgroup-enforced MemoryMax=/CPUQuota=, systemd-analyze blame/critical-chain for boot profiling (systemd-analyze and Boot Profiling), and socket activation that lets a host bring services up lazily — all the same regardless of the service. The cost is that PID 1 is now a large, central, fast-moving component; a systemd bug or misconfiguration is a system-wide event, and the cgroup-v2-only direction (v258+) has forced container runtimes and Kubernetes to follow (older v1-only tooling breaks). Container orchestrators that run systemd inside a container do so to get its supervision/cgroup model, but most container images instead run the app directly with a tiny reaping init — the two worlds meet at the cgroup substrate systemd and container runtimes both stand on (systemd and cgroup Integration, Linux Containers and Isolation MOC).

See Also