If you have worked with BusyBox init or System V init in earlier lectures of this free linux kernel development course, systemd will feel like a different universe at first. Instead of numbered rc scripts, systemd describes the system as a graph of small, declarative unit files, and it figures out the correct start order itself. In this lecture we open up that graph: what a unit file looks like, how services and targets differ, and how systemd walks the dependency tree to bring a board from power-on to a running shell.
This matters directly for embedded work. Almost every modern Yocto or Buildroot-based systemd image you will bring up on a custom board boots this way, so understanding the unit graph is what lets you add your own daemon, delay it correctly behind a network or storage mount, or debug why a service silently never started.
What You Will Learn
- Where systemd looks for unit files, and in what order
- The anatomy of a unit file: [Unit], [Service], [Install]
- Requires, Wants, and Conflicts — dependency vs ordering
- Before and After — controlling start/stop order
- The difference between a service unit and a target unit
- How systemd walks from default.target to a booted system
- Inspecting the graph with systemctl
Prerequisites
This lecture assumes you already know why an init system is needed and how it fits into the boot sequence, which we covered in the earlier lectures of this free embedded systems course on BusyBox init and System V init. You should also be comfortable running commands on a booted Linux target, either real hardware or QEMU.
Where systemd Looks for Unit Files
systemd does not keep configuration in one monolithic file. Every service, mount point, timer, or target is its own small text file, called a unit file, and systemd searches three locations for it, in a fixed priority order:
| Directory | Purpose | Priority |
|---|---|---|
/etc/systemd/system/ | Local, machine-specific overrides | Highest |
/run/systemd/system/ | Runtime-generated units, gone after reboot | Middle |
/lib/systemd/system/ or /usr/lib/systemd/system/ | Units shipped by the distribution or your rootfs image | Lowest |
systemd stops at the first match it finds. This gives you a clean override mechanism on a board: if your rootfs build ships a generic getty@.service under /lib/systemd/system/, you can override just your board without touching the build recipe, by dropping a same-named file in /etc/systemd/system/. Linking a unit name to /dev/null in /etc/systemd/system/ disables it outright, which is a common trick for stripping unwanted services off a production image.
Anatomy of a Unit File
Every unit file, regardless of type, starts with a [Unit] section describing what it is and what it needs. Here is a small original example for an on-board temperature logging tool:
[Unit]
Description=EP Temperature Logger
Documentation=man:ep-templog(8)
Requires=ep-sensors.target
Description and Documentation are purely informational — they show up when you run systemctl status. Requires is where the real dependency logic begins.
Requires, Wants, and Conflicts
These three directives describe the dependency graph, not the order:
| Directive | Meaning | Failure behaviour |
|---|---|---|
Requires | Hard dependency — this unit needs the listed unit(s) | If the dependency fails, this unit is stopped too |
Wants | Soft dependency — start the listed unit(s), but don’t insist | If the dependency fails, this unit keeps running |
Conflicts | Negative dependency | Starting one automatically stops the other, in either direction |
In practice, Wants is used far more often than Requires on embedded images, precisely because a single misbehaving peripheral driver service shouldn’t be able to bring down the whole boot.
Before and After — Controlling Order
Requires/Wants only decide which units get pulled in. They say nothing about the order they start in — by default, systemd starts everything in parallel. Before and After are what actually fix the ordering:
[Unit]
Description=EP Temperature Logger
After=ep-sensors.target network-online.target
Wants=ep-sensors.target
This says: try to bring up the sensors target, and once it (and the network) are up, start this logger — not before. Stopping runs in the reverse order automatically, so you rarely need to think about shutdown ordering separately.
Requires= alone gives you ordering. It does not — a unit with only Requires=foo.service and no After=foo.service may start at the same time as foo.service, or even before it, causing races that only show up intermittently on real hardware.
Service Units
A service is a unit whose name ends in .service, and it represents a daemon — the direct systemd equivalent of a System V service. The [Service] section tells systemd how to run it:
[Unit]
Description=EP Temperature Logger
[Service]
Type=simple
ExecStart=/usr/bin/ep-templog --interval=5
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
ExecStart is the command systemd runs to start the daemon. ExecReload defines what happens on systemctl reload. Restart tells systemd what to do if the process exits — on-failure restarts it only on a non-zero exit code, while always restarts unconditionally, which is common for watchdog-style daemons on embedded boards.
Target Units
A target is a unit whose name ends in .target. Unlike a service, a target has no executable of its own — it only exists to group other units and represent a desired system state, filling the role that runlevels played under System V init. multi-user.target (text login) and graphical.target (graphical login) are the two you will meet most often on embedded boards.
How systemd Boots the System
The kernel starts /sbin/init, which on a systemd image is a symlink to the systemd binary itself. systemd then activates one single unit: default.target, which is itself a symlink to whichever target represents the desired boot state:
$ ls -l /etc/systemd/system/default.target
/etc/systemd/system/default.target -> /lib/systemd/system/multi-user.target
$ systemctl get-default
multi-user.target
You can override this from the kernel command line without touching the rootfs, which is handy when debugging a board that won’t boot to a shell:
systemd.unit=rescue.target
Starting multi-user.target triggers a chain: it typically depends on basic.target, which depends on sysinit.target, which pulls in the early services (udev, mount points, and so on). systemd resolves this whole chain and starts everything it can in parallel, respecting only the Before/After constraints you saw above.
Inspecting the Graph with systemctl
A few commands you will use constantly while bringing up a new board:
# Print the full dependency tree for the default target
systemctl list-dependencies
# List every service unit and its current state
systemctl list-units --type service
# List every target unit and its current state
systemctl list-units --type target
Common Mistakes and Troubleshooting
- Forgetting
After=alongsideWants=/Requires=, causing intermittent startup races. - Editing a unit under
/lib/systemd/system/directly instead of overriding it under/etc/systemd/system/— the change gets lost on the next rootfs rebuild. - Assuming a
Requires=failure is silent — it will actively pull your unit down too, which is easy to misdiagnose as your own service crashing.
Best Practices
On embedded images
Prefer Wants= over Requires= for optional peripherals, always keep an explicit After= for anything you have an ordering dependency on, and override rather than edit vendor-shipped units so your changes survive a rootfs rebuild.
Summary / Key Takeaways
What we covered
systemd searches /etc, then /run, then /lib for unit files, stopping at the first match. Requires/Wants/Conflicts decide the dependency graph; Before/After decide the order. Services describe daemons, targets group units into named states, and the whole boot begins from a single unit: default.target.
This is the foundation of any free linux kernel development course module on modern init systems — once the unit graph clicks, adding, delaying, or overriding a boot-time service on your own board becomes straightforward. In the next lecture we put this to work by writing and enabling a real service unit from scratch.
FAQ
What’s the difference between Requires and Wants in systemd?
Requires is a hard dependency: if the required unit fails, systemd stops this unit too. Wants is a soft dependency: the wanted unit is started, but if it fails, this unit keeps running regardless.
Does Requires also control start order?
No. Requires only controls which units get pulled in, not the order they start. You must add an explicit After= directive to control ordering.
What is a systemd target used for?
A target groups other units and represents a desired system state, similar to a System V runlevel. It has no executable of its own — only dependencies.
How do I find the current default boot target?
Run systemctl get-default, or inspect the symlink at /etc/systemd/system/default.target.
Can I override the default target without editing the rootfs?
Yes — pass systemd.unit=<target> on the kernel command line to boot into a different target for that session only.
Where should I place a unit file override on an embedded board?
Under /etc/systemd/system/, using the same filename as the unit you want to override. systemd checks that directory before /lib/systemd/system/.
How do I disable a unit shipped by my rootfs build?
Create an empty file, or a symlink to /dev/null, with the same name under /etc/systemd/system/. systemd will find it first and treat the unit as masked.
Continue the Free Embedded Linux Course
Next up: writing, enabling, and hardening your own systemd service unit on a real board.
Next Lecture Browse the Full Course
2 Comments