File Descriptors as a Unified I/O Handle
What This Concept Is
A file descriptor (fd) is a small non-negative integer that a UNIX process uses to refer to something it has opened. Its core property -- and the reason the UNIX I/O model is so small -- is that the same integer works for an astonishing variety of things:
- files on disk
- directories
- pipes (one process -> another)
- sockets (network or local)
- terminals (
/dev/tty) - memory-mapped devices
- timers, signal queues, event queues (
timerfd,signalfd,epoll)
Every process starts with three FDs already open:
| fd | name | typical meaning |
|---|---|---|
| 0 | stdin | keyboard, or redirected input |
| 1 | stdout | terminal, or redirected output |
| 2 | stderr | terminal (unbuffered), for errors |
The kernel keeps a per-process table that maps each fd to an "open file description," which in turn points at the underlying resource. That indirection is what makes dup, pipe, and mmap work: you are manipulating the mapping in the table, not the resource itself.
Why It Matters Here
The unification is the point. Because a socket is an fd and a file is an fd, the same read/write/close/poll syscalls work for both. Shell redirection works because fd 1 can be rebound to point at a file instead of the terminal. Pipes work because a pair of related fds can be wired so that what one process writes, another reads.
If you think of "files," "sockets," and "terminals" as three different things, every later concept (redirection, pipes, servers, mmap) requires extra scaffolding in your head. If you think of them all as fds, they collapse into one model.
Concrete Example
A program that prints what fd 1 is actually pointing at:
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
int main(void) {
struct stat st;
if (fstat(1, &st) < 0) { perror("fstat"); return 1; }
const char *kind = "?";
if (S_ISREG(st.st_mode)) kind = "regular file";
else if (S_ISCHR(st.st_mode)) kind = "character device (e.g., terminal)";
else if (S_ISFIFO(st.st_mode)) kind = "pipe";
else if (S_ISSOCK(st.st_mode)) kind = "socket";
dprintf(2, "fd 1 is a %s\n", kind);
return 0;
}
Run it three ways:
$ ./whatfd # fd 1 is a character device (the terminal)
$ ./whatfd > out.txt # fd 1 is a regular file
$ ./whatfd | cat # fd 1 is a pipe
The same fd 1 reaches three different kinds of endpoint. The program's write(1, ...) does not need to know which.
Common Confusion / Misconception
"File descriptors and FILE * are the same thing." They are related but distinct. An fd is the raw kernel handle (an int). A FILE * is a C-library wrapper around an fd that adds userspace buffering, locale handling, and printf formatting. Use fileno(fp) to get the underlying fd; use fdopen(fd, "r") to wrap an existing fd in a FILE *. Mixing them -- printf and write interleaved to fd 1 -- produces out-of-order output because one is buffered and one is not.
Another trap: "fds start at 3 for my files." Usually, but not guaranteed. If you close fd 0 and then open a file, the kernel reuses the lowest free slot, which is now 0. That trick is the entire mechanism behind shell redirection (Concept 6) -- but it means you cannot hard-code fd numbers.
How To Use It
When reading or writing any I/O code, ask:
- What does each fd in this function point to? (Read it from
open,pipe,socket,accept, or the environment.) - Who closes it, and when? An fd that is not closed is leaked;
ulimit -nis the cap. - Does this fd need
O_CLOEXECso a childexec'd later does not inherit it? - Does it need
O_NONBLOCKsoread/writereturnEAGAINinstead of blocking?
Check Yourself
- What are fds 0, 1, and 2 by convention?
- Give two different kinds of thing that can be behind the same
read(fd, ...)call. - Why does mixing
printf(stdout, ...)withwrite(1, ...)produce garbled output?
Mini Drill or Application
Do all four:
- Run the
whatfdprogram above redirected to a file, a pipe, and the terminal. Capture the output. - Modify it to report fd 0 instead. Run with
./whatfd < /dev/nulland with a here-doc (./whatfd <<< hello). - Write a 10-line program that opens
/etc/hostname, prints the fd number, thencloses it. Repeat in a loop 1,024 times. Explain why it does not run out of fds. - In one sentence, explain how "everything is a file" simplifies writing a generic copy utility.
Read This Only If Stuck
- K&R 8.1: File Descriptors -- the original, one-page introduction
- Code: Peripherals (Part 1) -- hardware-side picture
- Man page:
man 2 open - Man page:
man 2 fstat - Man page:
man 2 fcntl-- fd flags:FD_CLOEXEC,O_NONBLOCK