07 - systemd¶
Backlink: 00 - Track Overview
Scope: the unit model (service/socket/target/slice/scope/timer/mount/path/automount/device/swap), dependency + ordering semantics and the transaction/job engine, cgroup integration and resource control, journald internals, socket/path activation,
systemd-analyze, drop-ins/overrides and the load path, user vs system managers, and the supervision mechanism that made systemd win. This is a mechanism module: we care about the D-Bus API surface, the syscalls PID 1 makes, the cgroupfs writes, the fd-passing ABI, and the on-disk journal format, not the "how do I enable a service" surface.
Concept deep-dive¶
What systemd actually is, and why it won¶
systemd is not "an init system." It is a manager of a dependency graph of typed objects (units), backed by cgroups, driven over a D-Bus API, that happens to also be PID 1. The init part is the least interesting part. Three design decisions won the argument against SysV init and Upstart, and you should be able to defend each at the mechanism level:
-
Aggressive parallelization via socket activation removes ordering as a boot bottleneck. SysV boot was a topologically-sorted shell-script pipeline: service B could not start until service A's script returned, because B connected to A's socket and A had to be listening first. systemd inverts this. It creates all listening sockets up front (in PID 1, before any daemon runs), then starts every service in parallel. If B connects to A's socket before A is ready, the connection just sits in the kernel's socket buffer (the accept queue), and the kernel blocks the reader until A drains it. The kernel's socket buffer becomes the synchronization primitive, so explicit ordering between A and B is unnecessary. This is the same trick launchd used on macOS, and it is the single biggest reason boot went from sequential to parallel. (See socket activation below.)
-
cgroups give reliable process tracking that PID-based supervision never had. A classic SysV daemon double-forks and reparents to PID 1 to "daemonize," which severs the parent's knowledge of it. PID files lie (stale PIDs, recycled PIDs). systemd puts every service in its own cgroup, so every process a service spawns, no matter how many times it forks or how it tries to escape, stays in that cgroup.
systemctl stopkills the cgroup, not a PID. This is why systemd always knows the exact process set of a service, and whyKillMode=,MemoryMax=, andsystemctl statusshowing the full process tree all just work. Process tracking and resource control are the same mechanism. -
A declarative dependency graph with a transaction engine replaces imperative ordering. Instead of numbered symlinks (
S20foo,K80bar), units declare relationships (Wants=,After=), and systemd computes a transaction (a consistent set of jobs) and executes it respecting ordering while maximizing parallelism. Ordering and requirement are orthogonal axes, which is the thing mid-level admins get wrong most often.
PID 1 responsibilities in the kernel sense: it is the ancestor that reaps all orphaned zombies (wait() on reparented children), it receives SIGCHLD for the whole orphan pool, it must never crash (a PID 1 crash panics the kernel), and it is the D-Bus endpoint org.freedesktop.systemd1 on the system bus. systemctl is just a D-Bus client. busctl lets you talk to that API raw.
org.freedesktop.systemd1 (D-Bus system bus)
▲
systemctl / busctl ─────┘
│ (StartUnit, StopUnit, GetUnit, SetUnitProperties…)
▼
┌─────────────────────────────────────────────────────────┐
│ systemd (PID 1) │
│ ┌──────────────┐ ┌───────────────┐ ┌────────────┐ │
│ │ Unit table │ │ Job / Trans- │ │ Event loop │ │
│ │ (typed objs) │──▶│ action engine │──▶│ sd-event │ │
│ └──────────────┘ └───────────────┘ └─────┬──────┘ │
│ writes cgroupfs, opens sockets, fork/exec │
└────────────────────────────────────────────────┼─────────┘
▼
/sys/fs/cgroup/… listening sockets (fd 3+) child procs
The unit model: typed objects, not scripts¶
A unit is a named, typed state machine. The type suffix selects the C implementation and the set of legal directives. The eleven types you must know:
| Type | Purpose | Contains procs? | Key mechanism |
|---|---|---|---|
.service |
A daemon or one-shot job systemd forks | yes (leaf cgroup) | ExecStart=, Type=, readiness protocol |
.socket |
A listening socket systemd owns on behalf of a service | no | socket activation, fd passing |
.target |
A synchronization / grouping point (no exec) | no | replaces SysV runlevels; pure dependency anchor |
.slice |
Inner node of the cgroup tree for resource partitioning | no (v2 rule) | -.slice→system.slice/user.slice/machine.slice |
.scope |
Externally-forked procs adopted into a cgroup (transient only) | yes (leaf) | created via D-Bus by session/container managers |
.timer |
Time- or calendar-based activation | no | monotonic + realtime (OnCalendar=), persistence |
.mount |
A mount point (mirrors an fstab entry) | no | generated from /etc/fstab by a generator |
.automount |
Lazy autofs-backed mount | no | kernel autofs, mounts on first access |
.path |
Activation on filesystem events | no | inotify-driven |
.device |
A udev device exposed as a unit | no | udev tags (SYSTEMD_WANTS=) create ordering edges |
.swap |
A swap area | no | generated from fstab |
.scope vs .service is the crucial distinction: a service encapsulates processes systemd itself fork/exec'd; a scope encapsulates processes some other process forked and then handed to systemd via the D-Bus StartTransientUnit call (this is how systemd-logind wraps every login session as session-42.scope, and how machined/container runtimes wrap payloads). Scopes are always transient (never on-disk unit files). This is the mechanism answer to "what is a container, in systemd terms": a scope (or a delegated service) whose cgroup has Delegate=yes.
Targets are worth internalizing: multi-user.target, graphical.target, basic.target, sysinit.target, network-online.target are not "runlevels with names." They are units with no ExecStart; they exist purely to be depended-upon. graphical.target Requires=multi-user.target and After=multi-user.target; the display manager WantedBy=graphical.target. The "default target" (systemctl get-default, a symlink /etc/systemd/system/default.target) is the root of the boot transaction.
The load path and drop-in precedence (where a unit actually comes from)¶
When you reference foo.service, systemd searches a fixed, layered set of directories, highest priority first, and the first full unit file found wins for the main file. This is straight out of systemd.unit(5):
/etc/systemd/system/ ← admin, highest priority (survives package updates)
/run/systemd/system/ ← runtime, volatile (generators, transient units)
/usr/lib/systemd/system/ ← package-shipped (RPM-owned; do NOT edit here)
Two mechanisms override without editing the package-owned file, and this maps directly onto the CLAUDE.md "never modify package-owned files" rule:
- Full replacement: a
foo.servicein/etc/systemd/system/shadows the/usr/libone entirely. - Drop-ins (the right answer): a directory
foo.service.d/containing*.conffragments. systemd loads the main unit, then applies every drop-in.confalphabetically on top. Drop-ins can be in/etc,/run, or/usr/lib, and they stack by directory priority then filename.systemctl edit foo.servicecreates/etc/systemd/system/foo.service.d/override.conf. This is the canonical, update-safe override, equivalent to a config drop-in directory for any RPM-owned file.
Precedence gotcha that bites people: for list-valued directives (ExecStart=, After=, Environment=), a drop-in appends. To replace a list you must first clear it with an empty assignment (ExecStart= with nothing) then set the new value. For scalar directives the last assignment wins. systemctl cat foo.service shows the fully-merged effective unit with source-file comments; systemd-delta shows every override/masked/extended relationship system-wide. mask is the nuclear option: symlink the unit to /dev/null so it can never be started, even as a dependency.
Generators (systemd.generator(7)) run before unit loading, very early in boot, and synthesize units into /run/systemd/generator*. systemd-fstab-generator turns /etc/fstab into .mount/.swap units; systemd-gpt-auto-generator discovers root/ESP by GPT partition type UUID; systemd-getty-generator spawns gettys. This is why there is no .mount file on disk for your root filesystem: it is generated every boot.
Dependency vs ordering: the two orthogonal axes¶
This is the highest-frequency conceptual error. Requirement dependencies decide which units get pulled into the transaction. Ordering dependencies decide when they run relative to each other. Neither implies the other. If you write Requires=b.service with no After=b.service, systemd will start both, in parallel, in undefined order. That is almost never what you meant.
Requirement / pull-in directives:
| Directive | Semantics |
|---|---|
Wants= |
Pull in the target; if it fails, we don't care. The soft, preferred form. |
Requires= |
Pull in the target; if it fails to start, we fail too. Does not by itself react to later runtime failure. |
Requisite= |
Like Requires, but does not start the target; requires it to be already active, else fail immediately. |
BindsTo= |
Like Requires, but also tracks runtime state: if the bound unit stops for any reason (even a crash or a device unplug), we stop too. The strong coupling. |
PartOf= |
One-directional propagation of stop/restart only: stopping/restarting the parent propagates to us, but not vice versa. |
Conflicts= |
Negative dependency: starting us stops the other, and vice versa (mutual exclusion, e.g. emergency.target vs everything). |
Upholds= |
Continuously restart the target as long as we're active (stronger than Wants, added later). |
Ordering directives: Before= / After= (mirror images), plus Requires=+After= is the common "start it and wait for it" combo. The relationships expressed in [Install] (WantedBy=, RequiredBy=) are not live dependencies; they are instructions for what systemctl enable should create as symlinks in the reverse dependency's .wants/ directory. Enabling is just symlink management.
The transaction / job engine. When you systemctl start graphical.target, systemd does not walk and exec. It:
- Builds a transaction: the set of jobs (a job = "bring unit U to state S", e.g.
start,stop,restart,verify-active) implied by the requested job plus all its requirement dependencies, recursively. - Merges jobs: if two paths both request
start dbus.service, they collapse to one job. Conflicting jobs in one transaction (start X and stop X) are resolved or the transaction is refused as inconsistent. - Orders jobs using the
After/Beforeedges, and detects ordering cycles. On a cycle, systemd tries to break it by dropping a job that is not strictly required (loggingFound ordering cycle… breaking cycle by deleting job X). If it can't, the whole transaction is refused. A cycle involving aRequiresedge can leave the system unbootable, dropped to emergency mode. - Executes jobs, running non-ordered jobs concurrently, gated only by the ordering edges. Ordering also implicitly controls shutdown: stop ordering is the reverse of start ordering, computed from the same
After/Beforegraph, which is why you almost never write explicit stop ordering.
Two default dependencies you must remember (DefaultDependencies=yes, on unless disabled): normal service units get Requires=sysinit.target After=sysinit.target and After=basic.target, plus Conflicts=shutdown.target Before=shutdown.target so they're cleanly torn down. Setting DefaultDependencies=no (common for early-boot units) removes these; get it wrong and you create a cycle with sysinit.target.
Service types and the readiness protocol (the supervision core)¶
Type= tells systemd when a service is considered "started" (the "up" edge that satisfies After= for later units). Getting this wrong is the classic "my service reports active but the thing depending on it starts too early" bug.
Type= |
"Started" means… | Notes |
|---|---|---|
simple |
the moment fork()+execve() returns (immediately) |
default if ExecStart set and no Type/BusName. systemd does not know when the daemon is actually ready. Downstream After= fires too early. |
exec |
execve() has succeeded in the child |
slightly stronger than simple: catches exec failures synchronously. Good default for modern units. |
forking |
the parent exits after fork'ing the real daemon | the classic SysV double-fork daemon. Needs PIDFile= to track the child. Fragile; avoid for new code. |
oneshot |
the process exits successfully | for one-shot setup jobs. Combine with RemainAfterExit=yes so the unit stays "active" after the process is gone. |
dbus |
the service acquires its BusName= on the bus |
readiness = D-Bus name ownership. |
notify / notify-reload |
the daemon sends READY=1 via sd_notify() |
the correct readiness contract. Downstream units start only after the daemon says it's ready. notify-reload adds reload signaling. |
idle |
delayed until other jobs are dispatched | cosmetic (avoids interleaving console output); do not use for real ordering. |
sd_notify(3) is the mechanism worth knowing cold. The daemon writes newline-separated KEY=value datagrams to a AF_UNIX/SOCK_DGRAM socket whose path systemd passes in $NOTIFY_SOCKET. Messages: READY=1 (I'm up), RELOADING=1 + MONOTONIC_USEC=… (reload started) then READY=1 (reload done), STOPPING=1 (going down), STATUS=… (free-text status shown in systemctl status), WATCHDOG=1 (pet the watchdog), MAINPID=…, and FDSTORE=1 for the fd store. It's a plain sendmsg(); you can emulate it with systemd-notify from a shell for testing.
The watchdog (WatchdogSec=) turns readiness into liveness: systemd expects a WATCHDOG=1 datagram at least every WatchdogSec; miss it and systemd considers the service hung and applies Restart=/WatchdogSignal=. Combined with a hardware watchdog (RuntimeWatchdogSec= in system.conf), a hung PID 1 or hung critical service can hard-reboot the box. This is how you build a self-healing appliance.
Restart= (no/on-failure/on-abnormal/on-watchdog/on-abort/always) plus RestartSec=, and the start-limit rate limiter (StartLimitIntervalSec=/StartLimitBurst= in [Unit]): if a service restarts more than burst times within the interval, systemd gives up and enters failed with "start request repeated too quickly." This is a common production trap: the fix flaps, hits the limit, and stays down until systemctl reset-failed.
cgroup integration and resource control (v2)¶
systemd is the single writer to the cgroup v2 unified hierarchy (/sys/fs/cgroup). It builds the tree from slices:
/sys/fs/cgroup/ (root; -.slice)
├── init.scope/ (PID 1 itself)
├── system.slice/ (all system services)
│ ├── sshd.service/
│ │ └── cgroup.procs (every pid of sshd, forks included)
│ ├── nginx.service/
│ └── …
├── user.slice/
│ └── user-1000.slice/
│ ├── user@1000.service/ (the per-user systemd --user manager)
│ └── session-3.scope/ (a login session; procs adopted by logind)
└── machine.slice/ (nspawn/libvirt VMs & containers)
The slice name encodes the path with - as separator: user-1000.slice lives at /user.slice/user-1000.slice/. Resource control directives (systemd.resource-control(5)) map directly onto cgroup v2 controller interface files, and setting any of them auto-enables the controller for that unit (systemd writes +cpu/+memory/+io to the parent's cgroup.subtree_control up the chain):
| Directive | cgroup v2 file | Mechanism |
|---|---|---|
CPUWeight= (1–10000, default 100) |
cpu.weight |
proportional share under contention (EEVDF/CFS). Weight, not a cap. |
CPUQuota= (e.g. 20%) |
cpu.max (quota period) |
hard cap: quota_us period_us. Exceeding it gets you throttled: the task is dequeued until the next period even if CPUs are idle. |
MemoryHigh= |
memory.high |
soft limit: throttle-via-reclaim. Over it, the cgroup is aggressively reclaimed and processes stalled, but not killed. |
MemoryMax= |
memory.max |
hard limit: the cgroup-local OOM boundary. Over it and unreclaimable → cgroup OOM kill (independent of global OOM). |
MemoryMin=/MemoryLow= |
memory.min/memory.low |
reclaim protection (guaranteed / best-effort). |
IOWeight= / IODeviceWeight= |
io.weight |
proportional I/O (needs bfq or the cost model). |
IOReadBandwidthMax= etc. |
io.max |
absolute I/O bandwidth/IOPS caps per device. |
TasksMax= |
pids.max |
fork-bomb containment (pids controller). |
AllowedCPUs=/AllowedMemoryNodes= |
cpuset.cpus/cpuset.mems |
CPU/NUMA pinning via the cpuset controller. |
The senior insight (straight from the shared research): CPUQuota throttling shows up as latency while CPU utilization looks low. A service capped at CPUQuota=50% on an 8-core box uses at most 0.5 cores; if its bursty workload wants 4 cores for 10ms, it runs for that quota then gets throttled for the rest of the period, so a request that should take 10ms takes 100ms+ while top shows the box 90% idle. The evidence is in cpu.stat: nr_throttled and throttled_usec climb. Dashboards showing "50% CPU, healthy" are lying; the cgroup cpu.stat throttle counters are the truth. This is the canonical "the metric is lying" story.
PSI (Pressure Stall Information) is the modern signal: cpu.pressure, memory.pressure, io.pressure per cgroup (and system-wide in /proc/pressure/) report the % of time tasks were stalled waiting for that resource. some avg10=… (some tasks stalled) vs full avg10=… (all tasks stalled) distinguishes contention from starvation. systemd-oomd uses memory+swap PSI to kill cgroups proactively before the kernel OOM killer engages, which gives you predictable victims instead of the kernel's heuristic pick.
Delegation (Delegate=yes, service/scope only, never slices) is how you hand a subtree to another manager (container runtime, a systemd --user). systemd then stops touching that subtree's attributes. Two mechanism traps: (1) the cgroup v2 "no processes in inner nodes" rule means once your delegated cgroup gains children it must not itself hold processes, so a delegated manager must migrate its own PID into a leaf sub-cgroup before creating children; (2) systemd makes the controllers available but does not enable them in your subtree, so you must write +cpu +memory to your own cgroup.subtree_control yourself. The nesting asymmetry (systemd insists on managing the top-level attributes of trees it delegates) is why a container manager running a full systemd payload needs an extra hierarchy level.
Socket and path activation (the parallelization engine)¶
A .socket unit makes systemd create and listen() on the socket during early boot, holding the listening fd in PID 1. The matching service is started on demand (first incoming connection) and inherits the fd. The wire protocol between systemd and the daemon (sd_listen_fds(3)):
- systemd passes the listening fds starting at fd 3 (
SD_LISTEN_FDS_START), consecutively (3, 4, 5, …). $LISTEN_FDS= the count.$LISTEN_PID= the PID that should own them (the daemon checksgetpid() == $LISTEN_PIDto avoid a forked child grabbing them).$LISTEN_FDNAMES= colon-separated names (fromFileDescriptorName=) so a daemon with multiple sockets can tell them apart.- The daemon calls
sd_listen_fds()which parses these, setsFD_CLOEXEC, and returns the count. It then justaccept()s on fd 3.
Boot: systemd socket() + bind() + listen() → holds fd in PID 1, service NOT running
│
Client connects ──▶ kernel queues SYN/data in the accept queue
│
systemd sees POLLIN via its event loop ──▶ fork/exec the .service
│ passes the listening fd as fd 3, sets LISTEN_FDS=1, LISTEN_PID=<child>
▼
daemon: sd_listen_fds() → accept(3) → serves the already-queued connection
Accept= selects the model. Accept=no (default, "inetd-in-listen-mode"): systemd passes the listening socket, one long-lived service instance handles all connections (nginx, most daemons). Accept=yes ("inetd per-connection"): systemd accept()s itself and spawns a templated instance foo@<conn>.service per connection, passing the connected socket as fd 3. Accept=yes scales terribly (a process per connection) and is mostly for legacy inetd-style tools.
Consequences you should be able to state: (1) socket activation enables zero-downtime restarts, because systemd holds the socket across the service restart, so connections queue in the kernel instead of getting refused; (2) it enables lazy start of rarely-used daemons (CUPS, D-Bus services), cutting boot time and idle memory; (3) it's why boot ordering mostly disappears (any client of a socket-activated service can start before it). The file descriptor store (FDSTORE=1 via sd_notify, FileDescriptorStoreMax=) lets a daemon stash fds in PID 1 across its own restart/crash so it can resume with live connections and no state loss, the mechanism behind crash-resilient stateful daemons.
.path units are the inotify analog: activate a service when a path appears/changes (PathExists=, PathChanged=, DirectoryNotEmpty=), driven by inotify. .timer units are the cron replacement: OnCalendar= (realtime, uses systemd-analyze calendar to validate), OnUnitActiveSec=/OnBootSec= (monotonic), Persistent=yes to run missed jobs after downtime (stores last-run timestamps under /var/lib/systemd/timers/), and RandomizedDelaySec= to de-thundering-herd fleets.
journald internals and the on-disk format¶
systemd-journald receives log records from four sources and stores them in an append-only, indexed, binary format: (1) the classic /dev/log AF_UNIX datagram socket (syslog compat), (2) the kernel ring buffer /dev/kmsg, (3) stdout/stderr of every service (systemd wires each service's stdout to a journald socket, tagged with the unit name automatically, which is why service output "just appears" in journalctl -u), and (4) the native protocol (sd_journal_send, structured key=value fields).
The structured model: each entry is a set of FIELD=value pairs. Fields with a leading underscore (_PID, _UID, _SYSTEMD_UNIT, _BOOT_ID, _SELINUX_CONTEXT, _CMDLINE) are trusted metadata that journald appends itself from /proc and the socket credentials (SO_PEERCRED) so they cannot be forged by the logging process; fields without the underscore came from the application. This trust boundary is why journald forensics are credible: _PID/_UID/_SELINUX_CONTEXT were stamped by PID-1's logger, not the sender.
On-disk (man journald.conf; files in /var/log/journal/<machine-id>/ for persistent, /run/log/journal/ for volatile). Each .journal file:
"LPKSHHRH" (8-byte signature)
Header {
file_id, machine_id, boot_id (128-bit IDs)
state: OFFLINE(0)/ONLINE(1)/ARCHIVED(2)
header_size, arena_size
n_objects, n_entries, n_data, n_fields
data_hash_table_offset/size ← direct pointer to the DATA hash table
field_hash_table_offset/size
tail_object_offset, entry_array_offset
head/tail_entry_seqnum, *_realtime, tail_entry_monotonic
}
Objects (each: {type, flags, size} header):
DATA "field=value" payload (may be XZ/LZ4/ZSTD compressed)
FIELD a field name ("_SYSTEMD_UNIT")
ENTRY one log record: binds DATA offsets + timestamps + seqnum
DATA_HASH_TABLE / FIELD_HASH_TABLE
ENTRY_ARRAY sorted array of entry offsets (chained, doubling size)
TAG Forward Secure Sealing HMAC
Mechanism that matters:
- De-duplication. Identical field values (e.g.
_SYSTEMD_UNIT=sshd.serviceon thousands of entries) are stored once as a single DATA object; each ENTRY just references its offset. This is why journals are far smaller than the equivalent text logs, and why field-value queries are fast. - Two-level hashing for O(1) field lookup. The FIELD hash table maps a field name to its object; each DATA object chains into a per-value hash bucket via
next_hash_offset. Hashing is keyed siphash24 (keyed byfile_id) on modern files, Jenkins lookup3 on old ones. Sojournalctl _SYSTEMD_UNIT=sshd.serviceis a hash lookup + a walk of that value's entry-array, not a scan. - Entry arrays for O(log n) time seeking. Entries are ordered by seqnum; the chained, size-doubling ENTRY_ARRAY structure lets
journalctl --sincebisect by timestamp. Each DATA object also has its own entry-array chain (the entries mentioning that value), which is what makes filtered queries fast. - Single-writer / multiple-reader, no locking. journald is the only writer; readers (
journalctl,sd_journal_*) mmap the file and validate offsets. Consistency across a crash relies onfdatasync()bracketing thestatefield flip and on the append-only arena. A file isONLINEwhile written,ARCHIVEDwhen rotated (then never mutated),OFFLINEwhen cleanly closed. Crash-truncated files are detected and the tail is discarded on next open. - Rotation & retention. Files rotate on size/time (
SystemMaxUse=,SystemMaxFileSize=,MaxFileSec=); old files areARCHIVEDand vacuumed by total size / age / count (SystemKeepFree=,MaxRetentionSec=).journalctl --disk-usage,--vacuum-size=,--vacuum-time=. - Forward Secure Sealing (FSS). With
journalctl --setup-keysandSeal=yes, journald periodically writes a TAG object: a SHA-256 HMAC over all objects since the last tag, keyed by an epoch key derived from FSPRG (a forward-secure PRG). The key evolves (one-way) each fixed time epoch, and the old key is destroyed. Consequence: an attacker who compromises the box at time T cannot forge or silently alter entries written before T, because the key that would sign them is gone.journalctl --verifychecks the chain. This is tamper-evidence, not tamper-prevention.
Query mechanics: journalctl is a client over the same files. -o verbose dumps all fields (including the trusted _ ones); -o json/json-pretty for machine parsing; -u, _PID=, _UID=, PRIORITY=, --boot/-b -1 (per-boot indexing via _BOOT_ID), -k (kernel), -f (follow), --grep, -p err. journalctl --list-boots uses the boot-id index. Forwarding: journald can relay to a classic syslog (ForwardToSyslog=) or you run systemd-journal-remote/-upload for centralization.
user vs system managers¶
There are two kinds of systemd manager. The system manager is PID 1. Each logged-in user also gets a user manager: systemd --user, running as user@<UID>.service inside user-1000.slice, managing that user's own unit tree from ~/.config/systemd/user/ and /usr/lib/systemd/user/. It has its own bus (the session/user bus), its own targets (default.target for a user is usually graphical-session.target), and its own drop-in/override rules. Key mechanism: user units normally die at logout unless lingering is enabled (loginctl enable-linger <user>), which keeps the user manager alive at boot so user services (a rootless container, a syncthing) run without an active session. systemctl --user talks to your user manager; systemctl (or --system) talks to PID 1. logind (systemd-logind) is the piece that tracks sessions/seats, creates the session-*.scope units, and handles HandlePowerKey=, KillUserProcesses=, and inhibitor locks.
Failure modes and behavior at scale¶
- Ordering cycle → dropped job or unbootable. systemd breaks cycles by deleting a non-essential job (logged), but a cycle through a
Requiresedge on a boot-critical unit lands you inemergency.target.systemd-analyze verify foo.servicecatches these offline;systemctl list-jobsshows a stuck transaction. Type=simplepremature-ready. Downstream units start before the daemon is listening → connection-refused races that "only happen on fast boots." Fix isType=notify+sd_notify, or a.socketunit.- Start-limit lockout. A crash-looping service hits
StartLimitBurstand stops retrying; looks "failed, not restarting" untilsystemctl reset-failed. At fleet scale a bad config push flaps every node into this state simultaneously. CPUQuotathrottling masquerading as idle (covered above): latency with low utilization; readcpu.statthrottle counters and PSI.- cgroup memory limits vs global OOM. A service under
MemoryMax=gets a cgroup-local OOM kill (only its own tasks are candidates), which is often more surgical than the global killer, but a too-tight limit turns a memory spike into a kill-loop.oom_score_adjstill applies within the cgroup.systemd-oomd+ PSI kills the whole offending cgroup earlier and more predictably. - journald backpressure & rate limiting. Under a log storm journald rate-limits per service (
RateLimitIntervalSec=/RateLimitBurst=) and drops with a "Suppressed N messages" marker; if/dev/logfills, a synchronous-logging service can block on its own log write. A runaway service can also blow the journal disk budget and force premature vacuuming of everyone else's logs (noisy-neighbor logging). - Generator failures are silent-ish. A broken generator (or a malformed
/etc/fstab) fails very early, before most logging is up; you find it injournalctl -bfromsystemd-fstab-generatoror dropped to the dracut/emergency shell. - Scale of the graph. Thousands of units (per-connection
Accept=yestemplates, transient scopes from a busy login/container host) make the transaction graph large;systemctl daemon-reloadre-parses everything and can briefly stall PID 1. Transient units (systemd-run) avoid on-disk churn but still live in the graph until they exit.
Hands-on labs¶
All labs assume a throwaway VM running a systemd distro with cgroup v2 unified hierarchy (any current Rocky/RHEL/Fedora/Debian/Ubuntu/Arch). Run as root (
sudo -i) unless noted. Verify the baseline first:systemctl --version | head -1 # expect v250+; note EEVDF vs CFS is kernel-side stat -fc %T /sys/fs/cgroup # expect "cgroup2fs" (unified). If "tmpfs", you're on v1/hybrid. mount | grep -w cgroup2 # confirm the v2 mountIf you see
tmpfsfor/sys/fs/cgroup, boot withsystemd.unified_cgroup_hierarchy=1on the kernel cmdline before doing the cgroup lab.
Lab 1 — The transaction/job engine: pull-in vs ordering, and breaking a cycle¶
Objective. Make the orthogonality of requirement vs ordering visible, watch systemd merge jobs into a transaction, and deliberately create an ordering cycle to watch the cycle-breaker fire.
Setup.
mkdir -p /etc/systemd/system
cat >/etc/systemd/system/lab-a.service <<'EOF'
[Unit]
Description=Lab A
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'echo A started at $(date +%s.%N); sleep 2'
EOF
cat >/etc/systemd/system/lab-b.service <<'EOF'
[Unit]
Description=Lab B
Requires=lab-a.service
# NOTE: deliberately NO ordering here
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'echo B started at $(date +%s.%N)'
EOF
systemctl daemon-reload
Steps.
1. Start B and observe that A is pulled in but ordering is undefined. Because there is no After=, B's echo can fire before A finishes its sleep 2:
systemctl reset-failed lab-a lab-b 2>/dev/null; systemctl stop lab-a lab-b 2>/dev/null
systemctl start lab-b
journalctl -u lab-a -u lab-b -o short-precise --no-pager | grep 'started at'
systemctl edit has no --stdin, so write the drop-in directly (this is exactly what systemctl edit lab-b.service would create interactively):
bash
mkdir -p /etc/systemd/system/lab-b.service.d
cat >/etc/systemd/system/lab-b.service.d/order.conf <<'DROP'
[Unit]
After=lab-a.service
DROP
systemctl daemon-reload
systemctl cat lab-b.service # observe the merged unit + drop-in source comment
3. Re-run and confirm ordering now holds (B strictly after A's 2s sleep):
systemctl stop lab-a lab-b; systemctl reset-failed lab-a lab-b 2>/dev/null
systemctl start lab-b
journalctl -u lab-a -u lab-b -o short-precise --no-pager | grep 'started at'
systemctl stop lab-a lab-b
systemctl start lab-b & # background so we can peek
systemctl list-jobs # see the start jobs and their "waiting"/"running" states
wait
bash
mkdir -p /etc/systemd/system/lab-a.service.d
cat >/etc/systemd/system/lab-a.service.d/cycle.conf <<'EOF'
[Unit]
After=lab-b.service
EOF
# lab-b has After=lab-a (from step 2), lab-a now has After=lab-b → cycle
systemctl daemon-reload
systemctl start lab-b
journalctl -b -u init.scope --no-pager | tail # or:
journalctl -b _PID=1 --no-pager | grep -i 'cycle' | tail
systemd logs something like Found ordering cycle on lab-b.service/start … Job lab-a.service/start deleted to break ordering cycle.
6. Catch the same problem offline, the way you'd want to in CI before shipping a unit:
systemd-analyze verify /etc/systemd/system/lab-b.service
Prove it.
# The offline verifier flags the ordering cycle without starting anything:
systemd-analyze verify lab-a.service lab-b.service 2>&1 | grep -i cycle
Cleanup: systemctl stop lab-a lab-b; rm -rf /etc/systemd/system/lab-{a,b}.service*; systemctl daemon-reload.
Lab 2 — Socket activation from scratch: prove the fd-passing ABI¶
Objective. Build a socket-activated service with no real daemon, and prove that systemd hands the listening socket to the service as fd 3 with LISTEN_FDS/LISTEN_PID set, and that connections queue in the kernel before the service exists.
Setup.
cat >/etc/systemd/system/lab-echo.socket <<'EOF'
[Unit]
Description=Lab echo socket
[Socket]
ListenStream=127.0.0.1:9999
Accept=no
[Install]
WantedBy=sockets.target
EOF
# A "daemon" that does nothing but report its inherited environment and fds,
# then serves one line via socat-on-fd-3-free bash /dev/tcp trickery is awkward,
# so we introspect instead of serving:
cat >/etc/systemd/system/lab-echo.service <<'EOF'
[Unit]
Description=Lab echo service (introspection only)
Requires=lab-echo.socket
After=lab-echo.socket
[Service]
Type=simple
ExecStart=/usr/local/bin/lab-echo.sh
EOF
cat >/usr/local/bin/lab-echo.sh <<'EOF'
#!/bin/sh
echo "PID=$$ LISTEN_PID=$LISTEN_PID LISTEN_FDS=$LISTEN_FDS LISTEN_FDNAMES=$LISTEN_FDNAMES"
echo "--- my fds ---"
ls -l /proc/$$/fd
# Confirm fd 3 is a socket bound to our port:
ss -lntp "sport = :9999" || true
sleep 60
EOF
chmod +x /usr/local/bin/lab-echo.sh
systemctl daemon-reload
Steps. 1. Start only the socket. The service is not running, yet the port is already listening, owned by PID 1:
systemctl start lab-echo.socket
systemctl status lab-echo.socket --no-pager
ss -lntp 'sport = :9999' # LISTEN, users:(("systemd",pid=1,...)) ← PID 1 holds it
systemctl is-active lab-echo.service # inactive: the daemon does NOT exist yet
( exec 3<>/dev/tcp/127.0.0.1/9999; echo hi >&3 ) 2>/dev/null &
sleep 1
systemctl is-active lab-echo.service # now active — activation happened on demand
journalctl -u lab-echo.service -o cat --no-pager | head -20
LISTEN_FDS=1, LISTEN_PID=<the service's PID>, and in the fd listing fd 3 is a socket (3 -> socket:[…]), while fd 0/1/2 are journald streams. The ss line inside confirms fd 3 is bound to :9999.
Prove it.
# fd 3 of the running service is the listening socket, and the count matches LISTEN_FDS:
svc_pid=$(systemctl show -p MainPID --value lab-echo.service)
ls -l /proc/$svc_pid/fd/3 # -> socket:[inode]
grep -a . /proc/$svc_pid/environ | tr '\0' '\n' | grep -E '^LISTEN_(FDS|PID)='
LISTEN_FDS=1, LISTEN_PID=$svc_pid, and /proc/$svc_pid/fd/3 -> socket:[…] proves the activation ABI end to end: systemd created and held the socket, passed it as fd 3, and told the child via env vars.
Bonus (understand Accept=yes): change the socket to Accept=yes, daemon-reload, and connect twice; observe templated per-connection instances lab-echo@<n>.service in systemctl list-units 'lab-echo@*' and that fd 3 is now the connected socket, not the listener.
Cleanup: systemctl stop lab-echo.socket lab-echo.service; rm -f /etc/systemd/system/lab-echo.* /usr/local/bin/lab-echo.sh; systemctl daemon-reload.
Lab 3 — CPU quota throttling: make "the metric is lying" visible¶
Objective. Reproduce the canonical senior scenario: a CPU-bound workload capped by CPUQuota= shows low utilization while suffering latency, and prove it with the cgroup cpu.stat throttle counters and PSI, not with top.
Setup. Need a busy-loop. Use systemd-run to launch a transient scope/service so systemd owns the cgroup:
nproc # note core count
# baseline: an unconstrained burner, see it eat a full core
systemd-run --unit=burn-free --scope /bin/sh -c 'while :; do :; done' &
sleep 3
cat /sys/fs/cgroup/system.slice/burn-free.scope/cpu.stat 2>/dev/null \
|| cat /sys/fs/cgroup/burn-free.scope/cpu.stat
systemctl stop burn-free.scope 2>/dev/null; kill %1 2>/dev/null
Steps. 1. Launch the same burner but hard-capped to 20% of one core, and let it run:
systemd-run --unit=burn-capped -p CPUQuota=20% \
/bin/sh -c 'while :; do :; done'
sleep 5
top/utilization: the process is pinned at ~20% of a core, the box looks nearly idle. That is the lie.
top -b -n1 | grep -E 'Cpu|burn|sh' | head
systemd-cgtop --iterations=1 # see burn-capped's CPU% capped
cpu.stat:
cg=/sys/fs/cgroup/system.slice/burn-capped.service
cat $cg/cpu.max # e.g. "20000 100000" = 20ms quota per 100ms period
cat $cg/cpu.stat # nr_throttled and throttled_usec are climbing
for i in 1 2 3; do
awk '/nr_throttled|throttled_usec/{print}' $cg/cpu.stat; echo ---; sleep 2
done
cat $cg/cpu.pressure # some avg10=... rising = tasks stalled waiting for CPU
Prove it.
cg=/sys/fs/cgroup/system.slice/burn-capped.service
before=$(awk '/nr_throttled/{print $2}' $cg/cpu.stat); sleep 3
after=$(awk '/nr_throttled/{print $2}' $cg/cpu.stat)
echo "throttle events in 3s: $((after-before))"
top shows the process at ~20% proves the point: utilization looked fine, but the workload was throttled tens of times a second. That gap is where the senior diagnosis lives.
Cleanup: systemctl stop burn-capped.service 2>/dev/null; systemctl reset-failed burn-capped.service 2>/dev/null.
Lab 4 — journald: trusted fields, structured queries, and tamper-evident sealing¶
Objective. Emit structured native journal fields, see journald stamp trusted metadata you cannot forge, exploit the indexed binary format for a field-value query, and set up Forward Secure Sealing and verify it.
Setup.
# Ensure persistent storage so sealing and per-boot indexing have somewhere to live:
mkdir -p /var/log/journal && systemctl restart systemd-journald
journalctl --disk-usage
Steps.
1. Emit a native structured entry with a custom field and a fake PID field, then read it back verbosely:
bash
systemd-cat -t labtest --priority=info <<'EOF'
Structured line one
EOF
# native fields via the socket-level tool:
logger --journald <<'EOF'
MESSAGE=native structured entry
PRIORITY=5
WIDGET_ID=42
_PID=999999
EOF
journalctl -t labtest -o verbose --no-pager | tail -30
journalctl WIDGET_ID=42 -o verbose --no-pager | tail -40
2. Note the trust boundary: your injected _PID=999999 is ignored/overridden; the real _PID, _UID, _SELINUX_CONTEXT, _COMM, _CMDLINE shown are the ones journald stamped from the socket credentials and /proc. Application fields (WIDGET_ID, MESSAGE, PRIORITY) are honored. Confirm:
journalctl WIDGET_ID=42 -o export --no-pager | grep -E '^_?PID='
journalctl _SYSTEMD_UNIT=systemd-journald.service --no-pager | wc -l
journalctl -o json _SYSTEMD_UNIT=systemd-journald.service | head -1 | tr ',' '\n' | head
_BOOT_ID index, and disk/rotation state:
journalctl --list-boots --no-pager
journalctl --header --no-pager | grep -E 'File Path|State|Sequential' | head
State: ONLINE = currently-written file; rotated files show ARCHIVED.
5. Set up Forward Secure Sealing. Generate the sealing key pair (prints a secret verification key / QR you'd store offline):
journalctl --setup-keys --force
# Turn on sealing and restart:
mkdir -p /etc/systemd/journald.conf.d
printf '[Journal]\nSeal=yes\nStorage=persistent\n' \
> /etc/systemd/journald.conf.d/seal.conf
systemctl restart systemd-journald
logger "sealed entry $(date)"
sleep 2
journalctl --verify
Prove it.
# The trusted _PID differs from the forged one, proving journald stamps identity:
journalctl WIDGET_ID=42 -o export --no-pager | grep -E '^_?PID=' | sort -u
# FSS verification passes (PASS / no tampering) across the sealed files:
journalctl --verify 2>&1 | tail -3
_PID= that is not 999999 proves the trusted-vs-untrusted field boundary; a clean --verify proves the FSS tag chain is intact and would flag any post-hoc alteration.
Cleanup: rm -f /etc/systemd/journald.conf.d/seal.conf; systemctl restart systemd-journald.
(Optional) Lab 5 — Sandboxing a service and scoring it, no SELinux required¶
Objective. Harden a unit purely with systemd's namespace/seccomp directives and quantify it with systemd-analyze security, demonstrating that SystemCallFilter= is seccomp-bpf and ProtectSystem=/PrivateTmp= are mount-namespace tricks.
Setup & steps.
systemd-run --unit=sbx -p Type=exec /usr/bin/sleep 300
systemd-analyze security sbx.service # high "exposure" score, unhardened
# Harden via drop-in-style properties on a fresh run:
systemctl stop sbx.service; systemctl reset-failed sbx.service 2>/dev/null
systemd-run --unit=sbx -p Type=exec \
-p ProtectSystem=strict -p ProtectHome=yes -p PrivateTmp=yes \
-p NoNewPrivileges=yes -p PrivateDevices=yes \
-p 'SystemCallFilter=@system-service' -p 'CapabilityBoundingSet=' \
/usr/bin/sleep 300
systemd-analyze security sbx.service # exposure score drops sharply
PrivateTmp=yes gives the service its own mount-namespace /tmp (prove it: ls /tmp on the host vs the service's private one via nsenter). SystemCallFilter=@system-service installs a seccomp-bpf allowlist filter (a program in the classic BPF VM) that the kernel evaluates on every syscall entry; a blocked syscall gets SIGSYS/EPERM.
Prove it.
systemd-analyze security sbx.service | tail -3 # overall exposure/score improved
pid=$(systemctl show -p MainPID --value sbx.service)
grep Seccomp /proc/$pid/status # Seccomp: 2 (filter mode active)
readlink /proc/$pid/ns/mnt # differs from PID 1's mnt namespace
Seccomp: 2 proves a seccomp-bpf filter is loaded; a distinct mount namespace inode proves the Protect*/Private* isolation is namespace-based. Cleanup: systemctl stop sbx.service; systemctl reset-failed sbx.service.
Curated resources¶
Primary docs / specifications (the definitive statements of behavior)
systemd.unit(5),systemd.service(5),systemd.socket(5),systemd.exec(5),systemd.resource-control(5)— https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html — The core reference set.systemd.unitis the dependency/ordering algebra and the load-path/drop-in precedence rules;systemd.serviceis theType=readiness matrix andRestart=/start-limit;systemd.execis the sandboxing knobs (namespaces, seccomp);systemd.resource-controlmaps every directive onto a cgroup v2 file. Read these as essays, not lookups.sd_notify(3)andsd_listen_fds(3)— https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html and https://www.man7.org/linux/man-pages/man3/sd_listen_fds.3.html — The two ABIs that define modern supervision and socket activation.sd_notifyis the readiness/watchdog/fdstore protocol;sd_listen_fdsis the fd-3/LISTEN_FDS/LISTEN_PIDcontract. These are the wire formats behindType=notifyand.socketunits.- systemd.io — CGROUP_DELEGATION — https://systemd.io/CGROUP_DELEGATION/ — The authoritative mechanism doc for the service/scope/slice split, the single-writer rule, the "no processes in inner nodes" consequence, and the
cgroup.subtree_control-you-must-enable-it-yourself gotcha. Essential for anyone reasoning about containers on systemd. - systemd.io — CONTROL_GROUP_INTERFACE and FILE_DESCRIPTOR_STORE — https://systemd.io/CONTROL_GROUP_INTERFACE/ and https://systemd.io/FILE_DESCRIPTOR_STORE/ — How systemd drives cgroup v2, and the fd-store mechanism behind crash-resilient stateful daemons.
- Journal File Format (upstream doc) — https://github.com/systemd/systemd/blob/main/docs/JOURNAL_FILE_FORMAT.md — The on-disk spec:
LPKSHHRHheader, DATA/FIELD/ENTRY/ENTRY_ARRAY/HASH_TABLE/TAG objects, siphash24 keyed hashing, entry-array bisection, compression flags, and the FSS TAG/HMAC chain. This is why journals are small (dedup) and queries are fast (indexed). - Control Group v2 — kernel admin-guide — https://docs.kernel.org/admin-guide/cgroup-v2.html — The kernel side of everything systemd's resource control drives:
cpu.maxvscpu.weight,memory.high(throttle-via-reclaim) vsmemory.max(OOM boundary),io.max/io.weight,pids.max, and PSI (*.pressure). Thenr_throttled/throttled_usecincpu.stat(Lab 3) are specified here.
Author's design rationale (the "why it works this way")
- Lennart Poettering — "systemd for Administrators" series (I–XXI) — http://0pointer.de/blog/projects/systemd-for-admins-3.html — The designer explaining the decisions: socket/bus activation removing ordering, cgroup-based tracking vs SysV double-fork, the unit dependency graph, drop-ins. The single best source for the parallelization-and-supervision argument. Read the whole series (each part links the rest).
- "Rethinking PID 1" — http://0pointer.de/blog/projects/systemd.html — The original manifesto. Why launchd-style socket activation and cgroups justify a new PID 1. Dated in specifics but the argument is the canonical "why systemd won."
Reference material and staying current
systemd-analyze(1)— https://www.man7.org/linux/man-pages/man1/systemd-analyze.1.html —blame,critical-chain(and why both mislead under parallelism/socket-activation),verify(offline cycle/typo detection),security(per-unit exposure scoring against the sandboxing directives),calendar/timestamp(validateOnCalendar=),dump,dot(graphviz of the dependency graph). Your primary boot-and-unit debugging surface.journald.conf(5)/journalctl(1)— https://www.freedesktop.org/software/systemd/man/latest/journald.conf.html — Storage/rotation/retention (SystemMaxUse=,MaxRetentionSec=), rate limiting (RateLimitBurst=), sealing (Seal=), forwarding. The query flags (-u,-b,_BOOT_ID,-o verbose/json,--verify,--vacuum-*) are the forensics toolkit.- Fedora Magazine — "systemd unit dependencies and order" — https://fedoramagazine.org/systemd-unit-dependencies-and-order/ — The clearest short treatment of the requirement-vs-ordering orthogonality with worked examples. Good calibration before the man page.
- Arch Wiki — systemd and cgroups pages — https://wiki.archlinux.org/title/Systemd and https://wiki.archlinux.org/title/Cgroups — The best-maintained practical reference: drop-in mechanics, user vs system managers, lingering, transient units, cgroup v2 verification. Distro-agnostic enough to trust.
- How Linux Works, 3rd ed — Brian Ward (Ch. on systemd and boot) — https://nostarch.com/howlinuxworks3 — Situates systemd in the full boot chain (firmware → GRUB → initramfs → PID 1 → targets), connecting this module to the boot module. Good structural glue.
- LWN.net kernel index — https://lwn.net/Kernel/Index/ — For cgroup v2, PSI, and systemd-oomd coverage as it evolved. How you keep this knowledge from going stale (the EEVDF scheduler change under
CPUWeight, MGLRU under memory reclaim). - rockyman.org — https://rockyman.org/ — authoritative Rocky Linux man-page index, versioned 8/9/10; verify exact flags/config keys here. This is where you confirm, for example, that
systemctl edit --stdindoes not exist on Rocky 9's systemd 252 before you put it in a runbook.
Senior signal¶
- Treats requirement and ordering as independent axes and never conflates them. A mid-level writes
Requires=db.serviceand assumes their service waits for it; a senior knows that withoutAfter=db.serviceboth start in parallel, and reaches forWants=+After=(soft) orBindsTo=+After=(hard, runtime-coupled) deliberately per the failure semantics they actually want. - Reads
cpu.statthrottle counters and PSI instead of trusting utilization. They know aCPUQuota='d service can be crippled by CFS/EEVDF throttling whiletopshows the box idle, and they prove it withnr_throttled/throttled_usecandcpu.pressure. Same instinct formemory.high(reclaim stall) vsmemory.max(cgroup OOM) vs the global OOM killer. - Never edits package-owned unit files; overrides with drop-ins. They use
systemctl edit/.d/*.conffragments, know the list-append-vs-scalar-replace rule (empty assignment to clear), and usesystemctl cat/systemd-deltato audit the merged result. This is the systemd-specific expression of the "don't modify RPM-owned files" rule. - Chooses
Type=from the readiness contract, not by habit. They reach forType=notify+sd_notify(READY=1)(or a.socketunit) to eliminate premature-ready races, and they knowType=simplemarks a service "up" the instantexecvereturns, before it can possibly be listening. - Can explain a container in systemd terms and reason about delegation. Scope vs service,
Delegate=yeson scopes/services only (never slices), the v2 "no processes in inner nodes" rule forcing self-migration to a leaf, and having to enable controllers yourself viacgroup.subtree_control. They know the single-writer rule is why you don't hand-create cgroups under a systemd-managed root. - Understands socket activation as the parallelization mechanism, not just a feature. They can state that the kernel accept queue is the synchronization primitive that made boot parallel, that the fd arrives as fd 3 with
LISTEN_FDS/LISTEN_PID, and that holding the socket in PID 1 is what enables zero-downtime restarts and the fd store enables crash-resilient stateful daemons. - Does journald forensics with the trust boundary in mind. They know
_-prefixed fields (_PID,_UID,_SELINUX_CONTEXT) are stamped by PID 1 from socket credentials and can't be forged, that field-value queries are indexed (siphash + entry arrays, not a grep), that FSS makes the log tamper-evident not tamper-proof, and they checkjournalctl --verifyand rate-limit/vacuum behavior under a log storm. - Debugs the transaction, not the symptom. Ordering cycles caught with
systemd-analyze verifybefore shipping; stuck boots read viasystemctl list-jobsandjournalctl _PID=1; crash-loops diagnosed as start-limit lockout (reset-failed) rather than "the service is broken"; and they knowcritical-chain/blamemislead under parallelism and socket activation.
See also¶
- 03 - Processes, Scheduling and Signals — systemd is PID 1: the process/signal mechanics here (fork/exec,
SIGCHLDreaping of reparented orphans, the CFS/EEVDF scheduler thatCPUWeight=/CPUQuota=steer) are exactly what the unit model and cgroup resource control sit on top of. - 08 - Boot and Init — where PID 1 comes from: the firmware → GRUB → initramfs → PID 1 → default target chain, generators running before unit load, and
sysinit.target/basic.targetordering are the boot-side context for this module's transaction engine. - 10 - Namespaces and cgroups v2 — the kernel primitives underneath systemd: the cgroup v2 unified hierarchy, controllers, and PSI that resource control writes to, plus the mount/PID/network namespaces and seccomp that
ProtectSystem=/PrivateTmp=/SystemCallFilter=(Lab 5) are built from. - 03 - Containers from the Ground Up — container runtimes register their payloads as systemd scopes with
Delegate=yes; the single-writer rule and the "no processes in inner nodes" constraint here directly shape how a runtime lays out its cgroup subtree. - 04 - Kubernetes Control-Plane Internals — the kubelet's systemd cgroup driver and the unit-based node components (kubelet, containerd) build on this unit/cgroup model;
CPUQuotathrottling is the pod-limit throttling that pages SREs. - 09 - Observability and SRE — journald's structured/indexed logs,
Restart=policies, the start-limit rate limiter, andWatchdogSec=are the supervision and telemetry substrate SRE builds alerting and self-healing on. - 02 - Warewulf Stateless Provisioning — stateless HPC nodes boot straight into this target/unit/socket-activation model; understanding generators and
sysinit.targetis what lets you debug a node that provisions but won't reachmulti-user.target.