Systems programming · lesson 04

File descriptors

"Everything is a file" is the Unix philosophy in four words. Files, pipes, sockets, terminals, and devices are all accessed through the same interface: a small integer called a file descriptor.

in progress
12 min

The fd table

Every process has a file descriptor table — an array maintained by the kernel, indexed by small non-negative integers. Each entry points to an open file description (the kernel's object tracking an open file: its position, flags, and the underlying file or resource).

When you call open, the kernel creates a file description and returns the lowest available index in your fd table — that index is the file descriptor. When you call read(fd, ...) or write(fd, ...), the kernel looks up entry fd in your table to find the file description, then performs the I/O.

The three standard descriptors

Every process starts with three fds already open, set up by the shell (or whoever launched the process) before calling exec:

fd name C constant default target
0 stdin STDIN_FILENO terminal keyboard
1 stdout STDOUT_FILENO terminal screen
2 stderr STDERR_FILENO terminal screen
💡
stdin and stdout point to the same terminal by default, but they're separate fds. This is why you can redirect one without affecting the other. ./prog > output.txt replaces fd 1 with a file while fd 0 still reads from the terminal.

open and close

c
#include <fcntl.h> #include <unistd.h> // O_RDONLY, O_WRONLY, O_RDWR — access mode (required) // O_CREAT — create if doesn't exist (needs mode arg) // O_TRUNC — truncate to zero on open // O_APPEND — writes always go to end int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); return 1; } write(fd, "log entry\n", 10); close(fd); // always close — leaking fds exhausts the table
⚠️
File descriptor leaks are a real bug class. Each process has a limited fd table (typically 1024 entries by default on Linux). If you open files without closing them — especially in long-running servers or loops — you'll eventually hit the limit and open will return -1 with errno == EMFILE. Use lsof -p <pid> to inspect a process's open fds.

dup and dup2: the mechanism behind redirection

dup2(oldfd, newfd) makes newfd refer to the same open file description as oldfd. If newfd is already open, it's closed first. This is the atomic operation that shells use to implement > and | redirection.

c
// implement: ./prog > output.txt int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); dup2(fd, 1); // fd 1 (stdout) now points to output.txt close(fd); // original fd no longer needed // now printf / write(1, ...) writes to the file

After dup2(fd, 1), both fd and 1 point to the same file description. Closing fd leaves the description open — it's only freed when all descriptors pointing to it are closed. The reference count on the file description decrements with each close.

Fds are inherited across fork

When a process forks, the child inherits a copy of the parent's fd table. Both parent and child share the same open file descriptions — including their file positions. This is how pipes work: the parent creates a pipe (two fds: read end and write end), forks, and each process closes the fd it doesn't need. You'll see this in the pipes lesson.

one-line takeaway

A file descriptor is just an integer index into a per-process table — and dup2 lets you rewire any fd to any open file, which is how shells implement redirection.