Handbooks  /  Linux Internals
Handbook~16 min readSystemsworked math + runnable code
The Linux Internals Handbook

Everything is
a file.

Linux runs the internet, your phone, and most of the cloud on a single, almost suspiciously simple idea: everything is a file. A document, a keyboard, a network socket, another program's output — all of them are just a stream of bytes you reach through a small integer called a file descriptor, using the same four calls: open, read, write, close. That uniformity is why tiny tools compose into anything, why a | b works, and why the whole system feels like one thing. This handbook is those small mechanisms — descriptors and pipes — worked out and made runnable.

01

One interface

Linux's defining design choice is "everything is a file." Nearly every resource — a regular file, a directory, a hardware device, a network socket, a pipe between programs, even live kernel state under /proc — is exposed through the same interface: the file. You touch all of them with the same small set of system calls: open, read, write, close. A program reading input doesn't know or care whether the bytes come from a disk file, the keyboard, a socket, or another program — they all arrive as a stream behind a descriptor.

That single abstraction is Linux's great multiplier. Learn one interface and it works everywhere; write one tool and it composes with every other, because they all speak "bytes through a descriptor." It's why the command line is so powerful and why decades-old Unix tools still slot into modern pipelines. Everything below — descriptors, pipes, redirection, fork/exec — is just the machinery that makes this uniform "file" idea real and cheap. Understand the abstraction and the internals stop being a pile of trivia and become variations on one theme.

The one-sentence version

Linux models every resource as a file reached through a small-integer descriptor, so open/read/write/close work on everything — and pipes wire one process's output descriptor to another's input, which is the whole of a | b.

02

The descriptor table

A file descriptor is a small non-negative integer that names an open resource within a process. Each process owns a file-descriptor table — effectively an array — where the kernel tracks everything it has open. Three descriptors are set up before your code even runs: 0 is standard input, 1 is standard output, 2 is standard error. That's why printf writes to descriptor 1 and errors go to 2.

The rule for new descriptors is beautifully simple and worth remembering: open returns the lowest-numbered free descriptor. Open a file with 0,1,2 taken and you get 3; open another and you get 4; close(3) and the next open reuses 3. This lowest-free rule is exactly what makes redirection work: to send a program's output to a file, the shell closes descriptor 1, then opens the file — which, being the lowest free number, becomes descriptor 1 — so everything the program writes to "stdout" now lands in the file, without the program knowing. A tiny, deterministic allocation rule turns into one of the shell's most-used features.

03

Pipes compose processes

A pipe is an in-kernel FIFO buffer with two ends: write bytes into one end, read them out the other, in order. It's the mechanism that lets one process's output become another's input — the beating heart of the Unix philosophy of small tools joined together.

How the shell wires a | b
1 · pipe()Kernel makes a FIFO buffer with a read end and a write end.
2 · a's stdoutProcess a's descriptor 1 is set to the pipe's write end.
3 · b's stdinProcess b's descriptor 0 is set to the pipe's read end.
ResultWhatever a prints, b reads — neither knows the other exists.

The elegance is that a and b are ordinary programs that just read stdin and write stdout — they have no idea they've been chained. The shell did all the wiring with descriptors before either started. That's why you can compose cat log | grep error | wc -l out of three tools that were never designed to know about each other: each speaks only "bytes in, bytes out," and pipes carry the bytes between them. The same primitive underlies networking too — a socket is just another two-ended byte stream behind a descriptor.

04

The fd & pipe rules

Descriptor allocation is deterministic — always the lowest free integer — and closing frees a number for reuse, which is the whole trick behind redirection:

open()  =  min{ n ≥ 0 : n ∉ table }     0,1,2 = stdin, stdout, stderr     close(fd) → fd reusable

With 0,1,2 taken, open→3, open→4; close(3) then open→3 again. Redirection = close(1) then open(file) so the file lands on descriptor 1.

A pipe is a FIFO: bytes come out in the order they went in, and reading an empty pipe returns nothing (or blocks, in the kernel):

write(pipe, [b1…bk])  then  read(pipe, n)  =  first n bytes, in order     read(empty) = [ ]

Write [1,2,3,4], read 2 → [1,2], read 2 → [3,4]: first-in-first-out. The runnable version below implements the fd table and a pipe and exercises both rules.

RUN IT YOURSELF

A descriptor table and a pipe

Two small Linux mechanisms that compose into everything. The file-descriptor table maps small integers to open resources; 0/1/2 are the standard streams, open() returns the lowest free descriptor, and closing one frees that number for reuse — which is exactly how the shell redirects output. A pipe is a kernel FIFO buffer: write bytes in one end, read them out the other in order, and reading an empty pipe yields nothing. Together they're how a | b works. Change the opens/closes or the pipe reads and watch the descriptors and the byte order.

CPython · WebAssembly
05

Fork & exec

How does a new program start? Linux splits it into two deliberately separate calls, and the split is where the descriptor magic happens. fork() clones the current process — after it, there are two nearly identical processes (parent and child) continuing from the same line, differing only in fork's return value so each knows which it is. exec() then replaces a process's program with a new one: same process identity (same PID, same open file descriptors), entirely new code.

The shell runs a command by combining them: it forks a child, and in the child — before exec — it sets up file descriptors (this is where redirection and pipe wiring happen, since the child still shares the layout), then execs the target program, which inherits those descriptors. That window between fork and exec is precisely why the design is split: it's the moment to arrange "stdout goes to this pipe" before the new program starts and takes over. It's a gorgeous consequence of "everything is a file" — because descriptors survive exec, you can wire a program's inputs and outputs from the outside, and the program just finds its streams already connected. Processes, redirection, and pipelines all fall out of this one two-step dance.

06

Pitfalls

The first practical trap is the file-descriptor leak. Descriptors are a finite per-process resource (there's a hard limit, often ~1024 by default), and every open, socket, or accepted connection you forget to close holds one forever. A long-running server that leaks descriptors eventually hits EMFILE — too many open files and can't accept new connections, a classic production outage. Always close what you open (or use language constructs that close automatically).

Two more that catch people. Pipe buffers are finite and can block: if a reader stops reading, the writer eventually fills the pipe buffer and blocks waiting for space — and if two processes each wait to read what the other must write, you get a deadlock. Pipelines feel like magic until backpressure bites. And zombie/orphan processes: after a child exits, its parent must "reap" it (read its exit status) or it lingers as a zombie in the process table; forget this in a server that spawns children and you leak process slots. The unifying lesson is that Linux's power comes from small, composable mechanisms with sharp edges — descriptors, pipes, processes are cheap and everywhere, which also means you must account for every one you create. Internalize "everything is a file," respect the lifetimes of descriptors and processes, and the system that runs the world stops being a mystery: it's open, read, write, close, forked and piped, all the way down.

Worth knowing

Close every descriptor you open (leaks end in EMFILE), remember pipes have finite buffers that block under backpressure, and reap child processes so they don't zombie. The uniform "everything is a file" model is powerful precisely because these resources are cheap — so you must track each one's lifetime.

Frequently asked

Quick answers

What does "everything is a file" mean?

Files, devices, sockets, pipes, kernel state — all reached through the same interface (open/read/write/close) behind a file descriptor, so one API works everywhere.

What is a file descriptor?

A small integer indexing a process's table of open resources. 0/1/2 are stdin/stdout/stderr; open returns the lowest free number, close frees it.

How does a | b work?

A pipe (kernel FIFO) connects a's stdout to b's stdin, so a's output becomes b's input — neither program knows the other exists.

What are fork and exec?

fork clones a process; exec replaces its program while keeping identity and descriptors. Between them, the shell wires up redirection and pipes.

Finished this one? 0 / 99 Handbooks done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.