In the previous lecture of this free linux device drivers course we walked through how systemd organizes unit files, dependencies, and ordering. Now we put that into practice: you will write a real unit file for a small original daemon, start and stop it by hand, make it persistent across reboots, and read its status the way you would while bringing up a new embedded board.
This is one of the most common tasks in embedded Linux work — every custom sensor daemon, watchdog kicker, or board-bring-up helper you write eventually needs a systemd unit to launch it reliably at boot.
What You Will Learn
- Writing a minimal but complete service unit file
- The difference between Type=simple and Type=forking
- Starting and stopping a service by hand with systemctl
- Reading systemctl status output correctly
- Making a service persistent with the [Install] section
- Automatically restarting a service that crashes
Prerequisites
This lecture builds directly on the unit file anatomy covered in the previous lecture of this free embedded systems course — make sure you’re comfortable with Requires, Wants, and Before/After before continuing.
Writing the Unit File
Let’s write a unit for a small original demo daemon, ep-heartbeat, which just writes a timestamp to a log file every few seconds — simple enough to reason about, but structurally identical to a real sensor or watchdog daemon you’d write for a board.
[Unit]
Description=EP Heartbeat Daemon
[Service]
Type=simple
ExecStart=/usr/bin/ep-heartbeat --interval=3
[Install]
WantedBy=multi-user.target
Save this as /etc/systemd/system/ep-heartbeat.service. The [Unit] section here only has a description, so it displays cleanly in systemctl output — no dependencies are needed for this simple daemon.
Type=simple vs Type=forking
The Type= field tells systemd how to know the daemon has actually started, which changes how it tracks the process:
| Type | When to use it | How systemd tracks it |
|---|---|---|
simple | Process stays in the foreground (most modern daemons) | The process started by ExecStart is the main process |
forking | Process forks and the parent exits, classic double-fork daemon | systemd waits for the parent to exit, then tracks the child |
If ep-heartbeat were written as a classic forking daemon instead, the unit would need Type=forking so systemd waits correctly instead of assuming the process died the moment the parent exits.
Starting and Stopping It by Hand
$ sudo systemctl start ep-heartbeat
$ sudo systemctl stop ep-heartbeat
At this point the service only runs when you tell it to — a reboot will not bring it back, because we haven’t enabled it yet.
Reading systemctl status
$ systemctl status ep-heartbeat
● ep-heartbeat.service - EP Heartbeat Daemon
Loaded: loaded (/etc/systemd/system/ep-heartbeat.service; disabled)
Active: active (running) since Mon 2026-08-24 10:02:11 UTC; 4s ago
Main PID: 913 (ep-heartbeat)
Tasks: 1
Memory: 412.0K
CGroup: /system.slice/ep-heartbeat.service
└─913 /usr/bin/ep-heartbeat --interval=3
Aug 24 10:02:11 board systemd[1]: Started EP Heartbeat Daemon.
Three fields matter most when debugging on a board: Loaded tells you which file systemd actually picked up and whether it’s enabled, Active tells you the current run state, and Main PID confirms which process systemd is tracking.
Making It Persistent
The [Install] section is what turns a manually-started daemon into a real boot-time service. WantedBy=multi-user.target means: when this unit is enabled, add it as a dependency of multi-user.target, so it starts automatically every time the system reaches that target.
$ sudo systemctl enable ep-heartbeat
Created symlink /etc/systemd/system/multi-user.target.wants/ep-heartbeat.service
→ /etc/systemd/system/ep-heartbeat.service
Notice what actually happened: systemd created a symlink inside a multi-user.target.wants/ directory. This is the same mechanism as writing Wants=ep-heartbeat.service directly into the target, just done at runtime instead of by editing a file. You can inspect or remove this link manually if you ever need to debug why a service is or isn’t starting at boot.
Restarting on Failure
For anything important — a sensor daemon or a health-check process — you generally don’t want it to silently stay dead after a crash. Add a Restart policy to the [Service] section:
[Service]
Type=simple
ExecStart=/usr/bin/ep-heartbeat --interval=3
Restart=on-failure
| Restart value | Restarts when… |
|---|---|
on-success | The process exits cleanly (exit code 0) |
on-failure | The process exits with a non-zero code, is killed by a signal, or times out |
on-abnormal | The process is killed by a signal, times out, or is aborted — but not on a clean exit |
on-abort | Only when the process receives an uncaught signal |
always | Any exit at all, clean or not |
Common Mistakes and Troubleshooting
- Forgetting to run
systemctl daemon-reloadafter hand-editing a unit file directly on the target — systemd caches unit files and won’t notice the change otherwise. - Using
Type=simplefor a daemon that actually double-forks — systemd will think it exited immediately and mark it failed. - Enabling a service but never starting it (or vice versa) —
enableonly wires it into the boot target; it does not start it on its own unless combined with--now.
systemctl enable --now ep-heartbeat does both steps — enabling for next boot and starting it immediately — in one command.
Best Practices
For embedded daemons
Prefer Type=simple for any daemon you control the source of — it’s simpler for systemd to track correctly than a forking daemon. Set an explicit Restart= policy for anything safety- or health-critical, and always test systemctl enable --now plus a real reboot on hardware, not just a start/stop cycle, before shipping a unit in your image.
Summary / Key Takeaways
What we covered
A minimal service unit needs only [Unit] and [Service] to run manually. The [Install] section with WantedBy= is what makes systemctl enable wire the service into a target so it survives reboots. Type= tells systemd how to track the process, and Restart= controls resilience after a crash.
With this, you can take any daemon you write for a board and turn it into a properly managed systemd service. Embedded devices often need one more layer of resilience on top of this — a hardware watchdog that can recover the whole system if a critical service ever truly hangs, which we’ll cover in the next lecture of this free linux kernel development course.
FAQ
What does the [Install] section actually do?
It defines what happens when you run systemctl enable — typically creating a symlink that adds this unit as a dependency of the target named in WantedBy=, so it starts automatically at boot.
Does systemctl enable start the service immediately?
No, enable only wires the unit into the boot target for the next boot. Use systemctl enable –now to enable and start it in one step.
When should I use Type=forking instead of Type=simple?
Use Type=forking when your daemon calls fork() and the parent process exits immediately, leaving the child running in the background. Use Type=simple when the process itself stays in the foreground.
Why isn’t systemd picking up my edited unit file?
Run systemctl daemon-reload after manually editing a unit file. systemd caches unit definitions and needs to be told to re-read them from disk.
What’s the difference between Restart=on-failure and Restart=always?
on-failure only restarts the service if it exits with an error, is killed by a signal, or times out. always restarts it regardless of how or why it exited, including a clean exit.
How do I check if my service is enabled for boot?
Run systemctl status ep-heartbeat and check the Loaded line, or run systemctl is-enabled ep-heartbeat directly.
Continue the Free Linux Device Drivers Course
Next: adding a hardware watchdog to recover your board automatically if a critical service ever hangs.
Next Lecture Browse the Full Course
2 Comments