Vivian Voss

Service Management: init vs systemd

linux freebsd unix

The Unix Way ■ Episode 04

A service starts. It stops. It restarts if it crashes. It declares its dependencies so the system knows the order. That is the job description. It has not changed since the 1980s. What has changed, rather dramatically, is the amount of C required to accomplish it.

On FreeBSD, a service is a shell script. Ten lines, typically. It sources one shared library, rc.subr, declares REQUIRE, BEFORE, and AFTER for dependency ordering, reads its configuration from a single file (/etc/rc.conf), and is sorted at boot by one C utility: rcorder. The entire init system amounts to 178 shell scripts. You debug it with sh -x, cat, and grep.

On Linux, since 2010, systemd has replaced this arrangement with approximately 690,000 lines of compiled C, 2.06 million total lines across 6,363 files, and roughly 150 compiled binaries. In 2024 alone, it received 8,397 commits. Both systems start services. One of them also replaced sudo.

The Anatomy

The comparison is best understood structurally. FreeBSD’s init system is composed of precisely five elements: shell scripts, one library, one configuration file, one ordering utility, and the shell itself. Each is inspectable, replaceable, and debuggable with tools that predate the engineer using them.

systemd is composed of, well, rather more. The binary count stood at 69 in 2013, which prompted some concern. By 2024, it had doubled. The project absorbed fifteen distinct tools that previously existed as independent, single-purpose programs, each maintained by specialists who understood them intimately.

FreeBSD init 5 components 178 shell scripts rc.subr (1 shared library) rc.conf (1 config file) rcorder (1 C binary) /bin/sh Debug: sh -x, cat, grep Tools you already know. systemd 150+ compiled binaries systemd (PID 1) journald (logs) logind (sessions) networkd (DHCP, DNS) timesyncd (NTP) udevd (devices) run0 +140 more 690,000 SLOC • 6,363 files 2.06M total lines • 8,397 commits (2024) FreeBSD init systemd

The ratio is instructive not because bigger is necessarily worse (complexity has legitimate uses) but because both systems solve the same problem. The question is whether the additional 689,800 lines of C purchase something the shell scripts cannot provide, or whether they purchase something nobody asked for.

The Absorption

systemd did not merely replace SysVinit. It absorbed the tools that surrounded it. One by one, independent utilities that had operated perfectly well for decades were subsumed into a single project under a single maintainer. The toll:

syslog became journald. cron became systemd timers. inetd became socket activation. udev was absorbed wholesale. ConsoleKit became logind. NTP became timesyncd. DHCP became networkd. And in 2024, sudo, a thirty-four-year-old privilege escalation tool, was deemed insufficiently integrated. Enter run0, systemd’s replacement for the programme that lets you become root.

Fifteen tools absorbed. Each previously maintained by domain experts. Each with its own release cycle, its own bug tracker, its own community of people who understood it deeply. All now governed by one project, one repository, one set of conventions, and one opinion about how a Linux system should behave.

On FreeBSD, syslog is still syslog. cron is still cron. ntpd is still ntpd. Each does one thing. Each is replaceable without rearchitecting the rest. This is not conservatism. It is the Unix philosophy operating as designed: small tools, loosely joined, each earning its place.

The Log Problem

Your server crashes. The filesystem is intact, but the service that was running has stopped and you need to know why. On FreeBSD, the answer is waiting for you in /var/log/messages. Plain text. You open it with grep, awk, or tail. The tools work because they always work. They have no dependencies, no state, no opinions about the health of the system they are inspecting.

On a systemd machine, the logs are in the journal. The journal is binary. To read it, you need journalctl, which requires a functioning systemd, which requires a functioning D-Bus, which requires the very system you are trying to diagnose. If the journal has corrupted (and it does) you are reading binary with hexdump and rather wishing you were not.

After a crash: reading the logs FreeBSD /var/log/messages Plain text. Always there. grep | awk | tail No dependencies. Works offline. Answer in seconds. systemd /var/log/journal/* Binary format. Requires tooling. journalctl systemd D-Bus Requires the system you are diagnosing. Corrupts? Good luck with hexdump.

Linus Torvalds was characteristically direct: “I think some of the design details are insane. I dislike the binary logs.” One does not often find oneself in the position of calling the Linux kernel maintainer a master of understatement, but here we are.

Plain text logs are not a limitation. They are a feature. They compose with every tool in the Unix ecosystem. They survive filesystem damage that binary formats do not. They can be shipped, piped, searched, and read by a human being with no special tooling and no functioning init system. The decision to replace them with a proprietary binary format was not an upgrade. It was a dependency injection.

The PID 1 Question

PID 1 is the first process the kernel starts. If PID 1 crashes, the kernel panics. There is no recovery. There is no fallback. The machine stops. This is not a design flaw. It is a contract: PID 1 must be so simple, so minimal, so thoroughly understood that it cannot fail.

FreeBSD’s init honours this contract. Rich Felker, the author of musl libc, observed that a correct PID 1 can be written in roughly fifteen lines of C. Fork. Reap zombies. Wait. That is the entire job. FreeBSD’s init is not much larger. It does not parse D-Bus messages. It does not manage network interfaces. It does not handle device hotplug. It starts processes and reaps their children. Full stop.

systemd’s PID 1 is a D-Bus client. It manages a state machine. It processes notifications. It handles socket activation. It is, by any reasonable measure, a complex application running as the one process that must never fail.

In 2016, this architectural choice bore its most instructive fruit. An unprivileged user, not root, not a daemon, an ordinary user, could hang PID 1 by sending an empty D-Bus message. One message. Zero bytes of payload. PID 1 stopped responding. SSH hung. A clean reboot became impossible. The machine required a hard power cycle.

SIGKILL will not help. PID 1 is exempt from signals it does not explicitly handle. The process did not crash. It simply stopped doing its job, which is arguably worse.

PID 1: the process that must never fail Crash = kernel panic. No recovery. FreeBSD init fork() reap zombies wait() ~15 lines of C Attack surface: effectively zero systemd PID 1 D-Bus client State machine Socket activ. Notifications cgroup mgmt Devices Logging Mount mgmt DNS, NTP, DHCP, containers, sudo... 2016: empty D-Bus message hung PID 1 SSH dead. Clean reboot impossible. Unprivileged user. Zero bytes payload.

The question is not whether systemd’s PID 1 is well-written. The question is whether PID 1 should also be a DNS resolver, a log daemon, a device manager, a container runtime, a session tracker, and, as of 2024, a sudo replacement. The Unix answer is no. Not because these are bad functions, but because PID 1 is the wrong place to put them. A fifteen-line process cannot have a D-Bus vulnerability. It cannot be hung by a zero-byte message. It cannot fail in ways its authors did not anticipate, because there is almost nothing there to fail.

The Numbers, Plainly

The Phoronix end-of-year statistics for systemd in 2024 read less like a service manager and more like a mid-sized application framework:

690,000 lines of source code. 2,060,000 total lines including tests, documentation, and build configuration. 6,363 files. 8,397 commits in a single calendar year. The project has more lines of code than many of the applications it manages.

FreeBSD’s rc.d system, by contrast, has not required a rewrite since it was introduced. The shell scripts are the same shell scripts. rc.subr is the same library. rcorder is the same binary. The system is not maintained because it is loved. It is stable because there is nothing left to break.

The Uncomfortable Inheritance

The most instructive aspect of systemd is not what it does but what it reveals about the industry’s relationship with complexity. When Lennart Poettering proposed systemd, he argued that shell scripts were slow, fragile, and unparseable. These were legitimate criticisms. SysVinit had genuine weaknesses. The response, however, was not to fix the shell scripts. It was to replace the entire ecosystem with a monolithic suite that now performs the functions of fifteen previously independent tools.

This is not the Unix way. The Unix way is to fix the tool, not absorb the toolbox. When cron is insufficient, you improve cron or write a better cron. You do not fold job scheduling into PID 1. When syslog is inadequate, you write a better logger. You do not replace text files with a binary format that requires the system it logs to be functional before the logs can be read.

FreeBSD demonstrates that the alternative is not theoretical. It is running. It has been running. The services start. The dependencies resolve. The logs are readable after a crash. The init process is small enough to audit in an afternoon. No D-Bus. No binary journals. No sudo replacement in PID 1.

The job of an init system is to start services and stay out of the way. FreeBSD’s init does this with shell scripts, one library, and one C binary. systemd does this with 690,000 lines of C and a growing conviction that every system utility should live under one roof. The services start either way. The question is what else you are running and what happens when it breaks.