The Handles to Everything
When a process wants to interact with a file — or anything file-like — it doesn't work with the file directly. Instead, it asks the kernel to "open" the resource, and the kernel returns a small non-negative integer called a File Descriptor (FD). This integer is the process's handle — its ticket to interact with that resource for the duration of the open connection.
Think of it like a coat check at a restaurant. You hand over your coat (the file path), and the attendant gives you a numbered ticket (the file descriptor). From that point on, you use the ticket number to refer to your coat — you don't need to describe the coat again. The kernel maintains a table mapping each FD number to the actual open file, its current position, and access mode.
File descriptors are per-process. Process A's FD 5 is completely unrelated to Process B's FD 5.
The Magic Three: 0, 1, and 2
Every Linux process starts life with three file descriptors already open, inherited from its parent:
| FD | Name | Default Connection | Purpose |
|---|---|---|---|
| 0 | stdin (Standard Input) | Keyboard/terminal | Where the process reads input from |
| 1 | stdout (Standard Output) | Terminal screen | Where normal output goes |
| 2 | stderr (Standard Error) | Terminal screen | Where error messages go |
Redirection: Rewiring the Connections
Because everything is a file, you can redirect these standard streams to point anywhere: > sends stdout to a file (creating or overwriting it), >> appends instead of overwriting, 2> redirects stderr alone, and 2>&1 redirects stderr to wherever stdout currently points — which lets a single command send both streams to the same file, route them to separate files, or discard one of them entirely by pointing it at /dev/null, a black-hole device that silently accepts and discards any data written to it.
The 2>&1 syntax means "redirect FD 2 to wherever FD 1 is currently pointing." Order matters: command > file 2>&1 works correctly (stderr goes to the file), but command 2>&1 > file does not (stderr goes to the terminal because FD 1 still pointed there when 2>&1 was evaluated).
Why Separate stdout and stderr?
This separation is crucial for scripting and pipelines. Without it, you couldn't distinguish program output from error messages: a script's actual output can be captured into a variable while its errors still surface to the operator's terminal, a pipeline that filters one command's output into another only pipes stdout (errors from the first command still print to the screen, since they were never in the pipe), and a production job can log its output and its errors to two separate files for easier post-mortem debugging.
Viewing Open File Descriptors
Every process's open file descriptors are visible through /proc/[pid]/fd/, where each entry is a symlink named by its FD number pointing at whatever it's connected to. A typical process shows the standard three FDs pointing at a terminal device, plus additional entries for anything else it has open — a log file, a network socket (shown as socket:[inode]), and so on. The socket is also "a file" — just a different kind.
Key Point: File descriptors are a finite resource. Each process has a per-process limit (check with ulimit -n, typically 1024 by default on older systems, 65536 on modern ones) and there's a system-wide limit (/proc/sys/fs/file-max). A process that opens files or network connections without closing them — a file descriptor leak — will eventually hit this limit and fail with "Too many open files." This is one of the most common failure modes for long-running servers. Detect leaks with lsof -p <pid> | wc -l and watch the count grow over time.