Everything Is a File
The phrase "everything is a file" is one of those ideas that sounds trivial until you realize how much of a modern operating system it quietly explains. It is not a literal claim about physics — not every resource on a running Linux machine is a file on disk. It is a claim about interface: the operating system presents hardware, processes, and network connections through a single, uniform abstraction, so that the same handful of system calls work regardless of what is on the other end.
That uniformity is the real payload. A programmer who learns open, read, write, and close has learned most of what they need to talk to a disk, a keyboard, a printer, a pipe between two processes, or a socket carrying traffic across the internet. No separate API per device. No plugin framework, no object model to learn, no driver-specific headers. The result is a system that is small in concept space and enormous in practical reach.
When a program issues read(fd, buffer, n), the kernel does not care what fd refers to. It might be a byte range in an ext4 partition, a line arriving from a USB keyboard, or the first 512 bytes of a disk sector. The kernel's job is to route that request to whatever subsystem owns the descriptor and hand back bytes.
Because the interface is fixed, the interface is reusable. Consider how a shell pipeline works:
cat access.log | grep "GET /api" | wc -l
There are no messages passed and no shared memory. Each stage receives a file descriptor for its input, treats it as a stream of bytes, and exposes another descriptor as its output. The plumbing is the abstraction. Swap cat for tail -f, or the file for a network socket, and nothing else changes.
The same principle shows up in ordinary command-line usage:
ls -l > listing.txt
ssh host "df -h" | less
tar czf backup.tgz --exclude='*.tmp' ./project/
Redirection, pipes, and remote command execution are all just descriptor manipulation. Once that clicks, a lot of Unix tooling stops looking arbitrary.
What the Kernel Actually Sees
Behind the polished abstraction, the kernel tracks distinct kinds of files, distinguished largely by inode type and the operations they support.
- Regular files — the everyday case: source code, images, databases, binaries. Their contents are a linear sequence of bytes, which makes seeking (
lseek) meaningful. - Directories — not a special container type in the way they are on some other systems, but file-like structures whose contents map names to inode numbers. Because directories are readable, listing them can reuse ordinary file-reading code.
- Block devices — storage that supports random access in fixed-size units, such as
/dev/sda1. Reads and writes land on block boundaries, which is why raw device access is dangerous and rarely needed outside recovery tools. - Character devices — byte streams without random access: terminals, keyboards, mice, serial ports, and
/dev/null. Everything flows in one direction, at whatever rate the device can manage. - Pipes and FIFOs — the IPC workhorses. An anonymous pipe lives in memory and dies with the processes holding it; a named FIFO occupies a path in the filesystem and can outlive any single process.
- Sockets — endpoints for network communication. A TCP connection, a UDP datagram, and a Unix domain socket are all descriptors, and all can be handled with standard I/O calls.
A useful test for whether an interface is genuinely unified is whether it composes with the rest of the system. Sockets pass the test well: curl writing to a socket, nc reading from one, and ssh -L forwarding one are all the same mechanism wearing different clothes.
File Descriptors: The Handle in Your Hand
The user-visible handle for all of this is the file descriptor (FD) — a small non-negative integer that acts as an index into the per-process descriptor table. Open a file and you get back an integer. Duplicate it and you get another integer pointing at the same underlying open file description.
Every process starts life with three descriptors already wired up:
| FD | Name | Usual source |
|---|---|---|
0 |
stdin | terminal, pipe, or file |
1 |
stdout | terminal, pipe, or file |
2 |
stderr | terminal, log file, or /dev/null |
Shell redirection is nothing more than rebinding these integers. In ls -l > listing.txt, the shell opens listing.txt, points descriptor 1 at it, and then execs ls — which never learns that its output is no longer going to a screen.
Because every I/O source is a descriptor, multiplexing falls out naturally. select, poll, and Linux's epoll all wait on sets of descriptors, letting one thread service many connections without the thread-per-connection overhead of simpler designs. This is the mechanism behind event-driven web servers handling tens of thousands of simultaneous clients, and behind tools like xargs -P or a terminal multiplexer juggling dozens of live output streams.
Descriptors are also the unit of inheritance. fork() duplicates the descriptor table; exec() keeps the descriptors that are not marked close-on-exec; a shell's 2>&1 merges stderr into stdout. Debugging a program that "loses" its output often means tracing exactly how those integers were wired together.
The Layer That Hides the Difference
Uniformity at the user-space interface would be of little value if every filesystem and driver behaved differently underneath. Linux closes that gap with the Virtual File System (VFS), a dispatch layer that gives every mounted filesystem the same shape.
A program using standard POSIX calls can move data between ext4, XFS, tmpfs (RAM-backed), a network filesystem like NFS, or a FUSE mount implemented in userspace, without recompiling or changing a line. The VFS routes the request, enforces permissions, manages page cache coherency, and translates the request into whatever the underlying implementation expects.
The same idea extends to kernel state and device control. The /proc filesystem exposes live process information as readable files: /proc/[pid]/status, /proc/meminfo, /proc/self/fd. Administrators and monitoring tools inspect the kernel by reading text instead of writing bespoke kernel modules. /sys exposes device and driver attributes in a hierarchy that can be browsed and, in many cases, written to — toggling a GPIO pin or checking a power setting can be as simple as echoing a value into a file under /sys/class/. /run holds volatile runtime state as ordinary files.
This is arguably the most striking expression of the philosophy: control flow and configuration flow through the same interface as data.
Where the Slogan Breaks
Slogans are useful precisely because they are slightly wrong. "Everything is a file" does not mean everything can be treated as a file. Signals arrive asynchronously; page faults are exceptional conditions; shared memory mappings and some synchronization primitives are not expressible as byte streams. Even within the file model, ioctl remains a necessary escape hatch for device-specific operations that have no sensible byte-stream equivalent.
What the slogan really promises is narrower and stronger: that the majority of interaction between a program and the outside world can be expressed as reading bytes from, and writing bytes to, descriptors. The exceptions are few enough that the abstraction pays for itself many times over.
Why It Still Matters
The payoff of this design is composability. Small programs that do one thing, connected by pipes, redirected into files, and monitored through /proc, produce tools of surprising power — and an operating system that new programs can be written against without asking permission from the kernel team.
It also explains Linux's durability. When a new device, filesystem, or network protocol appears, it rarely requires a new user-space programming model. It requires an implementation that presents itself as a file. Most of what the operating system does for you is already covered by the same four calls.
For developers, internalizing "everything is a file" is less about memorizing a phrase than about changing what you reach for first. When output misbehaves, inspect the descriptors. When concurrency gets hard, multiplex them. When you need system state, read /proc. When you want a program that composes with the rest of the world, give it a file interface — and let the shell, the pipes, and the ecosystem do the rest.