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/.eli, a transparent overlay you can newfs() or hand to ZFS as a vdev. ZFS volumes (zvol) appear under /dev/zvol//; 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.
Read the full article on vivianvoss.net →
By Vivian Voss, System Architect and Software Developer. Follow me on LinkedIn for daily technical writing.

Top comments (0)