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.
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.
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.
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.