By Design ⊣ Episode 07
In the summer of 1969, Ken Thompson had three weeks of uninterrupted time. His wife and infant son were visiting relatives in California, and the AT&T research division at Murray Hill had recently withdrawn from the Multics project, leaving Thompson with an empty PDP-7 (an 18-bit minicomputer with four kilobytes of memory) and the lingering question of what a smaller, cleaner operating system might look like. By the end of those three weeks Thompson had written the first version of what became Unix. With Dennis Ritchie and Rudd Canaday over the following months, the small Bell Labs group built a hierarchical filesystem, the notion of computer processes, pipes for inter-process communication, a command interpreter, and one architectural idea that has carried half a century without much fading: the file as the universal interface. A device, a pipe, a socket, a process listing, all opened, read, written and closed through the same system calls.
The idea did not arrive as a slogan. It arrived as the path of least resistance: if you had a kernel that already knew how to give a userspace program a numbered handle to a piece of named state, and a small set of operations on that handle, why invent another kind of handle for the next piece of state? Devices got file paths. Pipes got file descriptors without paths. Sockets, once they arrived in 4.2BSD in 1983, got the same file descriptors after a special creation call. The shape held, and the shape was the point.
The Complaint
"Sockets are not files. GPUs are not files. Hardware
state does not fit a read()."
The complaint is older than the design, and it is not
unfounded. Sockets carry connection state, message
boundaries, and asynchronous events; representing them
as a stream of bytes throws information away. GPUs have
command queues, memory layouts, and a parallel execution
model that no sequence of read() and
write() calls naturally captures. A
terminal has modes, signal characters and a baud rate
that nobody types into the data stream. The escape
hatch, ioctl, exists precisely because
some operations refuse to pretend they are bytes
flowing in and out, and the Unix designers did not
pretend otherwise.
The complaint, taken seriously, has produced several alternative designs over the decades. Microkernels handed each device its own service. Modern operating-system research has proposed message-passing kernels, capability-based systems, and explicit object models in place of file descriptors. Each is more expressive in some specific direction. None has displaced the file as the lingua franca of Unix-like systems, and the reason is not that the alternatives are wrong; it is that the file is unreasonably composable.
The Decision
The decision, plainly: when in doubt, give it a path.
Devices live in /dev/, with paths the user
can read aloud (/dev/null,
/dev/random, /dev/zero) and
that even a shell script can open. Pipes are anonymous
file descriptors created by pipe(2); the
shell binds them between processes with the
| character that became the most-copied
piece of syntax in the history of computing. Sockets are
created with socket(), bound and connected
with their own calls, and then used through the same
read/write/close as everything else. Processes are
inspectable as /proc entries on Linux and
several other systems, optionally on FreeBSD; the
kernel exposes itself as a filesystem because once you
have decided that files are how state is named, you may
as well expose your own state that way.
Ritchie and Thompson's 1974 ACM paper, "The UNIX Time-Sharing System", presented in CACM volume 17 number 7, codified what had already been running on the PDP-7 since 1969 and was, by then, also running on the PDP-11. Plan 9 from Bell Labs (Pike, Thompson, Presotto, Winterbottom, first edition 1992) pushed the idea to its logical end. In Plan 9, the window system is a file system; the keyboard is a file system; remote machines are mounted as file systems through the 9P protocol; even computation can be a file system. Dennis Ritchie said of Plan 9 that "Unix had the right idea that just about everything in the system is accessible through a file" but did not, in the end, follow through. Plan 9 followed through.
It is rather a sweeping promise, on the face of it. The decision was to keep it.
The Trade-Off
Not every operation fits a sequence of bytes. The Unix
designers knew this from the start, and
ioctl is the honest answer. A terminal
driver has a stty configuration that no
read() will reveal; a network interface
has an IP address that you do not write into a file the
way you write into one. ioctl is the call
you make when the file model has carried you as far as
it can, and the rest needs structured arguments and
structured replies.
GPU drivers, in the modern era, are the most striking
case. A modern GPU exposes a command queue, a memory
allocator, a synchronisation primitive set and a shader
compiler interface. The path-and-handle model can carry
the open-the-device step, but the rest is
ioctl calls that look more like RPC than
like file IO. DRM (Direct Rendering Manager) on Linux
and FreeBSD is the practical compromise: the GPU is a
file in /dev/, opened like everything
else, and then almost all the work happens through
ioctl.
Hardware that refuses the model outright lives behind
its own ABI. Some embedded firmware, some legacy
industrial controllers, some accelerator cards reach
userspace through libraries that bypass the filesystem
entirely. The Unix answer is to extend the file when
possible (memory mapping, asynchronous IO via fd,
kqueue/epoll on file descriptors), accept the
ioctl when the file abstraction cannot
stretch further, and keep the path-and-handle
vocabulary as the default.
The trade is honest. The file model is not free; it is cheap because the alternatives are expensive.
The Proof
Fifty-six years of Unix, and the file is still the unit.
On every Unix-like system shipped since the early
1970s, the same paths exist: /dev/null
discards what is written to it, /dev/random
and /dev/urandom give cryptographic
randomness, /dev/zero gives an
inexhaustible stream of zero bytes. Pipes hold the
shell together; a one-liner like
ps ax | grep nginx | awk '{print $1}'
composes three programs into one query because every
one of them reads from a file descriptor and writes to
a file descriptor without caring whether the other end
is a terminal, a file or another process.
On FreeBSD the discipline is intact.
devfs
(in-base since FreeBSD 5.0, 2003) presents the kernel's
device tree as a regular filesystem mounted on
/dev/; you can stat a device,
change its permissions with chmod, and
follow symbolic links to it like any other file. GELI,
the FreeBSD disk-encryption framework, exposes
encrypted volumes as /dev/<name>.eli,
a transparent overlay you can newfs or hand
to ZFS as a vdev. ZFS volumes (zvol) appear under
/dev/zvol/<pool>/<name>; you
can dd to one, partition it, export it via
iSCSI, all through the file interface. bhyve, FreeBSD's
hypervisor, presents virtual machine handles under
/dev/vmm/. Plan 9 is the pure version of
the idea and is still actively maintained at
9front.org,
on the same architectural premise as in 1992.
Linux started the same way and has, in some corners,
drifted. /sys, the sysfs hierarchy added in
2.6, is in the spirit of the idea, extending
/proc with kernel object trees. So far, so
good. The drift sits elsewhere. D-Bus, originating in
2002, carries the message bus that earlier Unix work
would have built as files and sockets; systemd, from
2010, builds its own interfaces around cgroups, sockets,
the journal, and a large set of dbus services.
Netlink sockets
are the alternative kernel channel for routing, audit
and firewall configuration that traditional Unix would
have done through paths and ioctls.
eBPF
is a programmable layer that runs verified bytecode
inside the kernel rather than a path that names a piece
of state; it is, in many ways, a different
architectural idea.
None of these are wrong. They solve real problems that the simple file model handles awkwardly. But the cumulative effect is that the Linux of 2026 has, in several important corners, become a system in which the file is one of several interfaces rather than the interface. Dennis Ritchie's verdict on Plan 9 was that "Unix had the right idea but didn't follow through". The same sentence, read against 2026 Linux, has rotated by 180 degrees: the right idea has been followed through in some corners and quietly diluted in others. The FreeBSD line is closer to the original commitment, deliberately so.
The Principle
One interface, infinite implementations.
The lesson, made portable: when a small set of operations
can address an arbitrarily large set of resources, the
operations compose without limit. Pipes, redirections,
file-descriptor passing, the entire shell tradition of
composing tools by gluing their inputs and outputs
together, rest on this property. The protocol the
kernel offers (open, read,
write, close,
ioctl as the escape) and the abstraction
the shell composes (the |, the
<, the >, the
2>&1) are the same protocol viewed
from different ends. There is no impedance to match
because there is no impedance to bridge.
A Unix one-liner reads almost like a sentence because every noun in it is a file. The verbs are universal. The grammar is small. The expressive range is, in practice, vast. Half a century of operational evidence sits behind the idea, and the corners of the modern Unix world that have departed from it tend to discover, after a while, that they have rebuilt parts of it with worse vocabulary.
That is what an architectural decision worth keeping looks like. Not perfection, not freedom from trade-offs, not absence of escape hatches. A small, honest, composable shape that does not cease to pay for fifty-six years.