Before you start#
Review PID 1 and userspace, userspace project baselines, and D-Bus. This chapter reads systemd v256, commit 5c79cdec10a547a866764a66e1e14898112a00cd, as a historical userspace baseline. It is independent of the Linux v6.12 source baseline; neither version establishes what is running on your machine. The chapter 08 QEMU build has not been boot verified in the recorded environment.
Learning goals#
Trace a request to load a unit through the manager's queue and type-specific loader. Distinguish a unit's loaded configuration from its activation state and a job. Explain configuration precedence, instance names, drop-ins and command parsing, including a missing executable and a malformed setting. Produce an offline configuration explanation without starting or modifying a host service.
Source walkthrough#
- Prepare
The manager finds or prepares a unit for a requested name.
- Dispatch
Newly queued units are loaded by the manager.
- Type loader
The unit vtable selects its type-specific load function.
- Merge or error
The caller follows a merged unit or handles a load failure.
The system manager, when systemd is actually running as PID 1, is a userspace process responsible for managing units and coordinating their jobs. Linux starts the first userspace program but does not require that program to be systemd. A user manager can also run with a different scope and authority; it is not the system's PID 1. The Manager object holds manager-wide state, including maps and queues used for units and jobs. A Unit represents a named piece of configuration and runtime state. Service and socket units add type-specific state and operations through unit-type vtables; their resources and callbacks must remain valid while the manager uses them. A unit can be known to the manager without being active.
The following exact excerpt from systemd v256 src/core/manager.c, function manager_load_unit, is LGPL-2.1-or-later as stated in the file's SPDX notice. The function's assertions and return are omitted:
r = manager_load_unit_prepare(m, name, path, e, ret);
if (r <= 0)
return r;
/* Unit was newly loaded */
manager_dispatch_load_queue(m);
*ret = unit_follow_merge(*ret);
The preparation step uses the manager, name or path, optional error and output pointer. A nonpositive result returns immediately under this function's contract, including failures and cases requiring no new queue dispatch. On the path marked newly loaded, the manager dispatches its load queue and then follows a merged-unit reference before returning the resulting unit. The queue is manager-owned work: obtaining a unit reference does not by itself launch its service command. A caller must inspect the result and unit load state rather than assuming a non-null-looking name means a valid startable unit.
The next exact excerpt is from v256 src/core/unit.c, unit_load, also LGPL-2.1-or-later. Earlier validation and later dependency setup are omitted:
r = UNIT_VTABLE(u)->load(u);
if (r < 0)
goto fail;
assert(u->load_state != UNIT_STUB);
u is the common unit object. Its type-specific vtable selects the loader appropriate to that unit type; the call is an indirect dispatch, not a claim that every unit is a service. A negative result follows the function's failure path. On success this assertion requires that loading no longer leave the unit in the stub state. The following code treats UNIT_LOADED specially and queues or adds dependencies. This contract is about configuration loading; a later job can still fail when starting the unit or executing a configured binary. Assertions and exact branches are specific to this source version and build configuration.
Manager startup and unit ownership#
The v256 src/core/main.c calls manager_new after earlier argument and environment setup. The created manager has a runtime scope and test-run flags; subsequent startup and event-loop behavior depend on whether the process is the system manager, a user manager, a reexecution or a test. Normal boot can enumerate and load units, restore serialized state and coldplug objects under the selected path. Coldplug means reconstructing relevant state and event sources during startup; it does not mean every device or service is already ready. Reading a call site does not prove that a particular machine booted through every branch.
A Unit has a name, a load state, dependency relationships and type-specific state. A Service can hold command and process information, while a Socket can own listening descriptors and event sources. Jobs are separate objects representing requested operations such as start or stop; a manager can know a loaded unit without having a job for it. The manager's event loop serializes much of its own dispatch work, but the processes it supervises and asynchronous kernel events run independently. Do not infer that all external resources are protected by one global lock, or assume a service's callback cannot arrive after a state transition. Unit references, event sources and jobs must be released or detached under their own lifecycle rules.
manager_load_unit_prepare can find an existing object or arrange a new one. The queued load operation selects the unit type from the name, calls the type's loader and resolves a fragment and dependencies. A fragment is the selected main unit file, not a process or a binary. An alias can resolve to the same unit object; after merging, callers must follow the manager's resulting object rather than retaining a stale assumption that two names mean two independent services. A failed load may leave a unit represented with an error state for reporting. A stub is an incompletely loaded unit object, not proof that its service started.
The pinned src/core/load-fragment.c implements many directive parsers. A parser can reject a value or warn and ignore a setting according to the particular directive and context. An unknown directive, an invalid dependency, an unreadable fragment and a nonexistent executable do not all fail at the same stage. Some are detectable during loading or verification; an executable can be missing when an otherwise parseable service later tries to start. Check load state, diagnostics and job result separately rather than reducing them to a single “systemd failed” state.
Search paths, names and drop-ins#
Unit files are selected from ordered search paths. In a typical system manager configuration, administrator configuration under /etc, runtime configuration under /run, and vendor-provided files under /usr have different precedence. Exact compiled paths and distribution layout vary; consult the v256 systemd.unit manual source and the running manager's documented paths before making a claim about a host. User managers have separate user and XDG-related search paths and authority. This explanation does not tell the learner to write any of those host directories.
After selecting a fragment, the manager can load applicable drop-ins from directories named for the unit or its hierarchy. A drop-in changes selected settings according to directive-specific merging and reset rules. Multiple files are ordered by documented precedence and names; an empty assignment can reset certain lists, but not every directive behaves as an appendable list. A service with a single ExecStart= restriction may require resetting an inherited command before adding a replacement. A reader must distinguish the file that supplied a value from the final effective setting and avoid assuming that editing a lower-priority file overrides a higher-priority one.
An instance name such as worker@blue.service can use a template fragment worker@.service when an appropriate instance-specific file is not selected. Specifiers in supported directive contexts can expand using unit identity, but escaping and allowed specifiers depend on the setting and version. An alias is a naming relationship, not an independent process with a separate status merely because another filename exists. A symlink to a mask target can deliberately prevent activation; a missing fragment, masked unit and syntactically bad fragment are different results. A unit can also be generated or transient in supported configurations, so a simple search of one directory is not a universal inventory.
Consider a synthetic template worker@.service with ExecStart=/usr/bin/worker --name %i and a drop-in for worker@blue.service that first resets the inherited command list and then sets ExecStart=/usr/bin/worker --name blue --mode safe. Assume both paths are valid and this directive supports the stated expansion and reset. The selected fragment supplies the baseline; the instance drop-in changes the effective command. The resulting conceptual argument vector has the executable and separate arguments, with no implicit shell pipeline. This is a source-based scenario, not an observed host unit. If the binary is absent, loading the text and resolving the name do not establish that a later start will succeed.
From a request to a retained object#
A useful investigation separates name resolution from lifetime. Suppose the manager receives two requests in sequence, first for an alias and then for the canonical service. The lookup table may identify the same underlying unit after the manager merges names. A later request can therefore find an already existing unit instead of constructing a second service object. The caller should not count the two requested names as two process instances. Conversely, two template instances with different escaped instance identifiers normally represent different unit names and can have different runtime state, even if their configuration comes from the same template fragment.
In the same v256 manager source src/core/manager.c (LGPL-2.1-or-later), the end of manager_load_unit_prepare queues a new or pending unit. Its own comment says that it does not load anything from disk. Lines 2405-2416 are quoted exactly:
r = unit_add_name(unit, name);
if (r < 0)
return r;
unit_add_to_load_queue(unit);
unit_add_to_dbus_queue(unit);
unit_add_to_gc_queue(unit);
*ret = unit;
TAKE_PTR(cleanup_unit);
return 1; /* The unit was added the load queue */
A return value of 1 means the unit was added to the load queue, and manager_load_unit then dispatches that queue. A return value of 0 means an already loaded unit was reused. The excerpt stops before the dispatch, the fragment parse and any dependency setup, so it says nothing about when a service process could start.
The load queue prevents the caller from treating parsing and dependency discovery as an arbitrary recursive expansion with no state. A unit may refer to another unit that refers back to it; the manager represents relationships among objects and handles loading and jobs in controlled stages. A reference to a dependency does not imply that its process is alive. If parsing allocates a dependency or an event source and then encounters an error, the appropriate manager and unit cleanup paths must release or retain that state according to the object's lifecycle. The two excerpts above only establish the selected dispatch and failure branches; they are not a proof of every reference count, garbage-collection condition or shutdown path.
Execution syntax and error paths#
ExecStart= has systemd-specific command-line parsing, quoting, substitution and executable lookup rules. It is not implicitly a shell command line. A pipe character does not automatically make a pipeline, and redirection syntax is not a substitute for a shell unless an explicit shell is deliberately invoked with a correctly quoted command. Even then, the shell's own safety and expansion rules apply. The v256 service manual source and systemd.exec manual are the authority for that release's supported syntax.
In the v256 parser src/core/load-fragment.c, config_parse_exec validates each command path after it resolves unit specifiers. The file is LGPL-2.1-or-later. Lines 975-999 are quoted exactly; the earlier prefix-flag and specifier code is omitted:
if (isempty(path)) {
log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0,
"Empty path in command line%s: %s",
ignore ? ", ignoring" : "", rvalue);
return ignore ? 0 : -ENOEXEC;
}
if (!string_is_safe(path)) {
log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0,
"Executable path contains special characters%s: %s",
ignore ? ", ignoring" : "", path);
return ignore ? 0 : -ENOEXEC;
}
if (path_implies_directory(path)) {
log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0,
"Executable path specifies a directory%s: %s",
ignore ? ", ignoring" : "", path);
return ignore ? 0 : -ENOEXEC;
}
if (!(path_is_absolute(path) ? path_is_valid(path) : filename_is_valid(path))) {
log_syntax(unit, ignore ? LOG_WARNING : LOG_ERR, filename, line, 0,
"Neither a valid executable name nor an absolute path%s: %s",
ignore ? ", ignoring" : "", path);
return ignore ? 0 : -ENOEXEC;
}
Each branch rejects an empty path, a path containing characters the parser does not treat as safe, a path that names a directory, or a value that is neither a valid absolute path nor a valid filename. Without a - prefix the rejection is an error (-ENOEXEC); with the prefix the setting is ignored with a warning. None of these branches tests whether the file exists, which is why a syntactically valid unit can still fail when its start job runs the executable.
- Expand specifiers
unit_path_printf resolves unit specifiers; a failure is an error unless the command has the ignore prefix.
- Reject empty or unsafe
An empty path, or one containing characters the parser rejects, returns an error.
- Reject directories
A path that implies a directory is refused.
- Check name form
An absolute path must be valid; otherwise the value must be a valid filename.
- Build argument vector
Words become argv entries; the value is not handed to a shell.
- Leave existence to start
This branch does not test that the file exists, so a missing binary is a later start failure.
An unknown unit name can result in a not-found load state; a unit may be loaded with warnings for an unsupported setting, depending on parser behavior. A malformed setting can be ignored or render the unit unusable depending on its semantics. A manager can merge aliases and follow the canonical object. In contrast, a valid loaded service whose executable later fails to execute can enter a failed activation path. A D-Bus client receiving acknowledgement that a request was accepted must still observe the job and unit state before claiming successful service startup. Configuration load, job installation, process creation and application readiness are distinct stages.
The manager must also cope with changes over time. A file can be replaced while an administrator is preparing configuration, a path can vanish, or an executable can be updated between verification and a later start. Offline verification of a private file does not reserve its future executable or prove the manager will use that file. Avoid a time-of-check/time-of-use claim: record the file, search path and snapshot examined, and state whether a running manager ever loaded it. A configuration check is not a runtime experiment, and a successful parse is not evidence of permissions, readiness or long-term service health.
Practice#
The following uses private files and systemd-analyze verify only if it is installed. It does not start a manager, enqueue a job, call systemctl, write host unit directories or require root. A verifier may consult installed unit search paths and report missing host dependencies; treat its result as host-version-dependent rather than a v256 runtime result.
work=$(mktemp -d)
cat > "$work/example.service" <<'UNIT'
[Unit]
Description=Private verification example
[Service]
Type=oneshot
ExecStart=/usr/bin/true
UNIT
cat > "$work/missing.service" <<'UNIT'
[Service]
Type=oneshot
ExecStart=/definitely-not-present/example-binary
UNIT
if command -v systemd-analyze >/dev/null; then
systemd-analyze verify "$work/example.service" || printf '%s\n' 'valid example not verified on this host'
systemd-analyze verify "$work/missing.service" || printf '%s\n' 'missing executable reported or verifier unavailable'
else printf '%s\n' 'systemd-analyze unavailable'; fi
rm -rf -- "$work"
A host with a compatible verifier and /usr/bin/true may accept the first file silently and report the missing executable for the second. It may instead report an unavailable manager environment or other host-specific prerequisite; record the actual output without changing the host to satisfy it. The temporary directory is deleted. The lab does not prove v256 executed or that either unit was started. If the verifier is absent or denied, use the supplied template/drop-in example: write the selected fragment, reset and final argument vector, then mark the missing-binary start as a distinct later failure.
Deliver a diagram containing manager, load queue, common unit, type loader, selected fragment and drop-in; annotate the r <= 0 and goto fail branches. Add a two-column table showing “loaded” versus “started” for the synthetic template. The deliverable is a source-reading artifact, not a runtime qualification.
Synthetic teaching data, not a device capture.
- Loaded
- worker@blue.service has a valid argument vector after the drop-in reset; no process exists yet.
- Started
- A start job runs /usr/bin/worker with the effective arguments; a missing binary fails here.
- Ready
- Start results and any readiness protocol are separate evidence; a loaded unit does not show them.
Knowledge check#
- What does
manager_load_unitdo after preparation reports a newly loaded unit, and what does it not do? - Why does
UNIT_VTABLE(u)->load(u)require knowing the unit type before predicting a particular loader? - How can a valid template and drop-in still lead to a later failed service start?
- Why is
ExecStart=/bin/program | othernot automatically a shell pipeline? - How do a stub, a merged alias and a loaded-but-inactive unit differ?
Answers#
- It dispatches the load queue and follows a possible merge before returning the unit. That function loads configuration; it does not start the service or establish application readiness.
- The common unit delegates loading through its type-specific vtable. A service and socket have different type implementations and resources, so the caller's indirect call must be resolved under the actual unit type.
- Parsing and merging can produce a valid command line even when the selected executable is missing or later becomes inaccessible. Starting is a separate operation whose process and job failures require separate evidence.
- The directive has systemd's own command parsing rules and does not implicitly launch a shell. A pipeline requires an explicit supported shell invocation or another design, with its own quoting and trust considerations.
- A stub is not fully loaded, a merged alias follows another unit object, and a loaded inactive unit has configuration but no active service merely because of that load. These states answer different questions.
Checkpoint#
Submit the manager-to-loader diagram and the synthetic template/drop-in table, with either private verifier output or the supplied source-only explanation. Ready means explaining the vtable call, one failure branch, precedence assumptions and why loading is not starting. If unclear, trace only manager_load_unit and unit_load first, then retry the instance example. No host service change is required.