Chapter 33: Init systems¶
What: PID 1, what it does, what it should do, and the three real choices for an embedded Linux system: BusyBox
init(tiny, traditional),sysvinit(the classical desktop init from the 90s), andsystemd(the modern service manager that runs on basically every desktop distro).Why: PID 1 is special: the kernel panics if it dies, and every other process on the system descends from it. The choice you make here determines how you write boot scripts, how you crash-restart services, how logs are collected, and how much disk and RAM the system uses just to “be up.”
Focus: the trade-off triangle: simplicity, capability, and footprint. BusyBox wins on simplicity and footprint. Systemd wins on capability. Sysvinit is the historical middle. For most embedded products in 2025, BusyBox init is the right answer. Knowing why is the goal of this chapter.
33.1 What PID 1 actually does¶
The minimum PID-1 job description:
Run startup scripts (mount filesystems, configure network, start daemons).
Spawn login prompts (one per console / serial line).
Reap zombies. When a process’s parent dies, its children get reparented to PID 1. When those children eventually exit, PID 1 must
wait()for them or they become zombies forever.Handle signals.
SIGTERM→ shut down cleanly.SIGINT(Ctrl-Alt-Del on a physical keyboard) → reboot or do whatever the policy says.Coordinate shutdown. Stop services in reverse start order, unmount filesystems, sync disks, then
reboot()orpoweroff().
That’s it. Any program that does these five things is a legitimate PID 1. The choice is how much beyond this it does.
33.2 BusyBox init¶
We’ve been using this since Chapter 29. It is ~1500 lines of C, statically linked into the BusyBox binary as one of its applets, and does exactly the five things in §33.1, no more, no less. Configuration is one file: /etc/inittab (Chapter 31 §31.5).
Features it has:
inittab-based:sysinit,respawn,askfirst,once,wait,shutdown, etc. (the 8 actions from Ch 31)Reads
/etc/inittabonce at boot. Re-reads on SIGHUPReaps zombies. Signals shutdown properly
Features it does not have:
No service dependency tracking. If service A needs service B running, you have to encode that in shell scripts yourself.
No automatic restart count limit. If a service crashes 1000 times in 1 second, busybox-init dutifully restarts it 1000 times.
No socket activation. (Systemd’s headline feature. Nice to have for embedded? Rarely.)
No structured logging.
printfto the console. That’s it.No cgroup-based resource isolation per service.
When to choose BusyBox init:
Single-purpose appliance. The system runs one or two daemons + a shell.
Boot time matters. BusyBox init starts within 100 ms of
kernel_init.RAM and flash budget are tight. The init itself adds 0 bytes (it’s inside busybox).
You want to understand every line that runs at boot. You can read all the init code in an hour.
For most i.MX6ULL-class embedded products, this is the right answer.
33.3 sysvinit¶
The classical Unix init from the 80s/90s. Still around, still on some Debian systems if you uninstall systemd.
The model:
/etc/inittab(different syntax from BusyBox. Runs/etc/rc.d/rc <N>for each runlevel)/etc/init.d/: one shell script per service. Each script acceptsstart,stop,restart,statusas arguments./etc/rc<N>.d/: symlinks to/etc/init.d/scripts, namedS<NN><name>(start) orK<NN><name>(kill). Init runs them in numeric order when entering runlevel N.
Runlevels: a number 0-6 representing system states. By convention:
0 = halt
1 = single-user maintenance
2 = multi-user, no networking (rarely used)
3 = multi-user, networking, text console
5 = multi-user, networking, graphical login
6 = reboot
You change runlevel with init <N> or telinit <N>. The current level is in /var/run/utmp.
A sysvinit service script:
#!/bin/sh
### BEGIN INIT INFO
# Provides: my-daemon
# Required-Start: $network
# Required-Stop: $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: My daemon
### END INIT INFO
case "$1" in
start)
start-stop-daemon --start --background --make-pidfile \
--pidfile /var/run/my-daemon.pid \
--exec /usr/bin/my-daemon
;;
stop)
start-stop-daemon --stop --pidfile /var/run/my-daemon.pid
;;
restart) $0 stop; $0 start ;;
*) echo "Usage: $0 {start|stop|restart}"; exit 1 ;;
esac
The LSB info block in the comment header gives dependency hints, sysvinit can read these and compute service order (or you arrange the symlink names manually).
When to choose sysvinit:
You’re maintaining an existing system that already uses it.
You explicitly want runlevels and you’d find shell scripts comforting.
When not to choose sysvinit:
You’re starting fresh in 2025. BusyBox init does the same job with one tenth the bytes. Systemd does more if you need it. Sysvinit has little reason to exist in a new design.
33.4 systemd¶
Systemd is the giant in the room. Every major desktop distro (Debian, Ubuntu, Fedora, Arch) ships systemd by default. It is the default for general-purpose Linux in 2025.
What you get:
Unit files instead of shell scripts. Declarative, in
/etc/systemd/system/:[Unit] Description=My daemon After=network.target [Service] ExecStart=/usr/bin/my-daemon Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target
Dependency-driven ordering.
After=,Before=,Requires=,Wants=,Conflicts=. Systemd computes the right parallel start order.Per-service resource limits via cgroups:
CPUQuota=,MemoryMax=,IOWeight=.Sandbox options:
PrivateTmp=true,ProtectSystem=strict,NoNewPrivileges=true,CapabilityBoundingSet=.Socket activation: a service starts when something connects to its socket, not at boot.
Timer units: like cron jobs, but unit-managed.
journald: a binary, queryable log database.journalctl -u my-daemonshows your service’s logs.A few hundred more features.
The cost is footprint. Systemd itself plus its required satellites (udev, systemd-journald, systemd-logind, …) is ~6 MB on disk and ~30 MB RAM at idle on a minimal install. On i.MX6ULL’s 512 MB DRAM that’s tolerable but not negligible.
udev: the user-space device manager that reacts to kernel device events and creates policy-driven /dev nodes.
When to choose systemd:
Your rootfs is Ubuntu-base or Debian (Chapter 35A). They already use systemd. Fighting it is more work than embracing it.
You need socket activation, advanced sandboxing, or per-service cgroup limits.
Your team is Linux-distro-experienced and
systemctl statusis muscle memory.
When not to choose systemd:
You’re on a Buildroot or yocto-core-image-minimal-style image. They default to BusyBox init for good reason.
Buildroot: a configuration-driven build system that produces a complete root filesystem and related images.
Your boot-time budget is < 2 seconds. Systemd needs 3-5 seconds on i.MX6ULL just for itself.
RAM is tight (< 256 MB).
33.5 Comparing them at a glance¶
BusyBox init |
sysvinit |
systemd |
|
|---|---|---|---|
Lines of code |
~1.5 K |
~5 K |
~600 K |
Disk footprint |
0 (part of busybox) |
~100 KB |
~6 MB |
RAM footprint (idle) |
< 100 KB |
~1 MB |
~30 MB |
Boot time on i.MX6ULL |
~100 ms |
~300 ms |
~3-5 s |
Config format |
|
|
unit files (ini-style) |
Service dependencies |
manual (script order) |
LSB headers |
declarative |
Auto-restart |
yes (respawn) |
manual |
yes ( |
Resource limits |
no |
no |
yes (cgroup) |
Sandbox features |
no |
no |
yes |
Logging |
console |
console + syslog |
journald (binary) |
Industry default for embedded |
yes |
rare |
growing |
33.6 Why embedded might want no init system at all¶
For some products the simplest answer is: don’t have one. The kernel’s init=/path/to/myapp cmdline argument tells the kernel to run your application as PID 1. No shell, no inittab, no service manager. Your application:
int main(void) {
/* set up signal handlers for SIGTERM */
/* mount /proc, /sys, /dev */
/* configure network */
/* run forever, waiting on hardware */
}
This is the embedded equivalent of an MCU’s main(). Boot time: under 500 ms total (kernel + your app). Footprint: whatever your app is. No /etc/, no /sbin/init, no nothing.
When this works:
The system has one job (industrial controller, single-purpose sensor).
No need to ssh in, no need to inspect anything live.
Failure mode is “reboot”, your app crashes, PID 1 crashes, kernel panics, watchdog (Ch 51A) reboots.
When this doesn’t work:
You need diagnostics. Without
/bin/shyou can’t poke around.Multi-service. If you have two daemons, you need something to start the second one.
For a developer board you almost always want a shell. For a shipping single-purpose appliance, “no init” is a real choice and surprisingly common.
33.7 Recommendation¶
For the rest of this book, and for most readers’ real products, BusyBox init is the default. We use it in every chapter from here through Part VI. When we need to talk about a feature only systemd has (Chapter 51A’s watchdog daemon, Chapter 35C’s container manager), we’ll note it explicitly. When we get to Chapter 35A (Ubuntu-base), we’ll meet systemd in its natural habitat.
33.8 Lab¶
Read your BusyBox
inittab. Identify which lines run at boot, which respawn, which run only on shutdown.Add a respawning service. Edit
/etc/inittabto add::respawn:/usr/bin/my-counter. Writemy-counteras a tiny script that prints the date every 10 seconds. Save, reboot, watch.Trigger a manual shutdown. From the console:
halt. Read what BusyBox init does in response (it runs theshutdownlines from inittab). Compare withreboot.Read a systemd unit. If you have a Debian/Ubuntu host nearby,
cat /lib/systemd/system/ssh.service. Note the syntax differences vs BusyBox.Estimate the boot-time difference. Boot your BusyBox rootfs and note the time from
kernel_initto the login prompt. Thedmesgtimestamps are your clock. Now do the same on the Ubuntu-base rootfs (Ch 35A). Typical figures: ~2 s BusyBox vs ~8 s Ubuntu-base. Most of the gap is systemd.
33.9 Pitfalls¶
Zombies accumulating. Some daemons double-fork and detach. The grandchild then gets reparented to PID 1. If PID 1’s
wait()loop is correct, the kernel hands it the SIGCHLD and the child is reaped. BusyBox init does this correctly. Custom PID-1 binaries often forget thewait()loop. The symptom is<defunct>processes piling up inps.Respawn storm. A
respawnline for a service that immediately exits causes infinite restart loop, burning CPU. BusyBox init doesn’t rate-limit. Add asleep 5to your service or use systemd’sRestartSec=./etc/inittabsyntax differences. sysvinit uses runlevels in the second field. BusyBox ignores that field entirely. Don’t copy/paste between init implementations.init=cmdline overrides everything. Even if/sbin/initexists, if you boot withinit=/bin/sh, the kernel runs the shell directly. Useful for recovery. Surprising if you forgot you set it.systemd in a 256 MB system. It will boot but everything will be sluggish. Choose BusyBox or sysvinit instead.
systemd unit-file ordering bugs. Putting both
After=andWants=on a unit can produce unexpected orderings if the targets aren’t carefully chosen. When in doubt, readsystemd.unit(5).
33.10 Going deeper¶
BusyBox init source:
init/init.cin the BusyBox tree. ~1500 lines. Read it.Documentation/admin-guide/initrd.rst(kernel) for the init handoff details.man systemd.unit,man systemd.service, canonical systemd reference.Lennart Poettering’s “Rethinking PID 1” blog post, the original systemd design rationale. Worth reading for context.Rich Felker’s posts on musl + init systems: the minimalist’s counter-argument.
Next chapter: Chapter 34: libc, dynamic linking, and the loader. Down one more level: what the libraries are that everything else depends on, and how the kernel hands control to user space’s first instruction.