File Descriptors
A descriptor is a small integer indexing a per-process table whose entries point at system-wide open file descriptions (offset, flags) which in turn point at file objects — and on Linux sockets, pipes, timers and epoll instances are descriptors too.
The problem
EMFILE: too many open files — but it only opens one config file at startup. What is being counted, who is counting, and why does the number include your database connections?Three tables, not one
When open() succeeds it returns an int. That integer is an index into the calling process’s descriptor table, which the kernel keeps per process. Each slot points at an open file description — a system-wide kernel object holding the current offset, the status flags (O_APPEND, O_NONBLOCK, read/write mode) and a reference count. The open file description in turn points at the file object itself: the inode on Unix (Inodes), or a socket, pipe or device.
The separation is the whole story. The descriptor is cheap and per-process; the open file description is shared state that can be reached from several descriptors — in the same process via dup, or in different processes via fork. When two descriptors share one description they share one offset: a write through one moves the position seen by the other. When two processes each open() the same path independently, they get two descriptions and two independent offsets over the same object.
By convention, and enforced by every shell, the first three slots are 0 stdin, 1 stdout, 2 stderr. open always returns the lowest free slot, so a program whose stdin was closed will get 0 back from its first open and then read its config from what it thinks is the terminal. Real programs guard against this by reopening /dev/null on 0–2 at startup if they are closed.
dup, dup2 and what `2>&1` really does
dup(fd) copies a descriptor into the lowest free slot; dup2(old, new) copies it into a specific slot, silently closing whatever was there. Both new slots point at the same open file description, so they share offset and flags. Shell redirection is nothing but open + dup2 performed by the shell in the child process between fork and exec: cmd > out.txt is "open out.txt, dup2(that, 1), then exec cmd". The command never knows; it writes to 1 as always.
2>&1 is dup2(1, 2): make slot 2 a copy of whatever slot 1 currently is. Order matters because dup2 copies the current pointer, not a future one. cmd > out.txt 2>&1 first points 1 at the file and then copies that into 2 — both go to the file. cmd 2>&1 > out.txt first copies the terminal into 2, then points 1 at the file — stderr stays on the terminal. Reading redirections as a sequence of dup2 calls resolves every such puzzle.
Pipelines are the same trick with a pipe (Pipes: A Kernel Buffer Between Two Processes): the shell creates a pipe (two descriptors), forks twice, dup2s the write end onto 1 in the left child and the read end onto 0 in the right child, and closes the originals. That last step is not optional: a forgotten write end keeps the pipe open and the reader never sees end-of-file.
1int fd = open("server.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);2dup2(fd, 1); // stdout -> server.log (slot 1 now shares fd's open file description)3dup2(1, 2); // stderr -> whatever stdout is now, i.e. the file4close(fd); // slot 3 no longer needed; the description lives on via 1 and 25execv("./server", argv);Inheritance across fork and exec, and O_CLOEXEC
fork copies the descriptor table. Every open description gains a reference and both processes see the same offsets — this is why a parent and child that both write to an inherited log file interleave correctly instead of overwriting each other, and why a child that reads from an inherited file advances the parent’s position. exec replaces the program but keeps the table unless a slot is marked close-on-exec. That default is what makes redirection and pipelines work; it is also a leak: every descriptor you forget to mark leaks into every child you spawn.
The leak is worse than it sounds. A web server that spawns convert to resize an image hands it every client socket it has open; if convert hangs, those TCP connections cannot fully close, and if it is a long-lived helper it holds the listening socket so a restart of the server fails with EADDRINUSE. O_CLOEXEC on open, SOCK_CLOEXEC on socket, pipe2 with O_CLOEXEC, and accept4 exist so you can set the flag atomically at creation instead of racing with a concurrent fork in another thread. Modern runtimes (Python since 3.4 by PEP 446, Go, Node) set it by default on everything they open.
The alternative to a table copy is Windows: CreateProcess takes bInheritHandles and only handles explicitly marked inheritable (HANDLE_FLAG_INHERIT) cross over; there is no fork. The mechanism differs, the failure is identical: a helper process inherits a handle it should not have, and a file cannot be deleted or a port cannot be rebound until that helper exits.
- Inherited descriptors share an open file description with the parent: shared offset, shared
O_NONBLOCK. - The fork-then-exec window is a race in multithreaded programs; set
O_CLOEXECat creation, not with a laterfcntl. ls -l /proc/<pid>/fdshows exactly what a running process holds, deleted files included.
Limits: EMFILE, ENFILE, ulimit
There are two ceilings. The per-process limit, RLIMIT_NOFILE, is what ulimit -n shows: a soft value the process may raise itself up to a hard value. Exceeding it makes open, socket, accept, pipe, epoll_create, dup and timerfd_create fail with EMFILE. Historically the soft limit is 1024, which is why every Node, nginx and PostgreSQL tuning guide starts with raising it; on recent systemd-based distributions the default soft limit is still 1024 while the hard limit is 524288, so a process can raise its own limit but must do so explicitly. The system-wide limit (fs.file-max, and fs.nr_open as the per-process hard ceiling) yields ENFILE, which is rarer and means the whole machine is out.
Everything with a descriptor counts: every accepted connection, every outbound connection to Postgres or Redis, every open log file, every inotify watch instance, the epoll instance itself, and — the one people forget — every pipe pair used to talk to a child process (two per pipe). A server handling 1,000 concurrent clients that also holds a 50-connection database pool and a 100-connection HTTP client pool needs more than 1,150 slots before it opens a single file. On a 1024 soft limit that server fails under mild load, and it fails at accept, so the symptom is clients seeing connection resets or timeouts while the process is otherwise healthy.
Leaks are the chronic form. A file opened on the exception path and never closed, a socket whose close is skipped when a timeout fires, an HTTP client created per request instead of shared: each costs one slot forever, and the counter only goes one way. lsof -p <pid> | wc -l sampled every hour turns "it crashes every nine days" into a straight line you can extrapolate. Languages help — Python with open(...), C++ RAII wrappers, Go defer f.Close(), Java try-with-resources — but only on paths that actually run them.
$ ulimit -n # soft limit of this shell
1024
$ ulimit -Hn # hard limit this process may raise to
524288
$ cat /proc/sys/fs/file-max
9223372036854775807
$ ls /proc/41822/fd | wc -l
1019
$ ls -l /proc/41822/fd | awk '{print $NF}' | sed 's/:.*//' | sort | uniq -c | sort -rn | head -3
961 socket
41 /var/log/app.log (deleted)
9 anon_inodeSockets, pipes, timers and epoll are all descriptors
On Linux the descriptor abstraction extends far past files. socket() returns a descriptor with buffers behind it (The Socket: A Descriptor With Two Kernel Buffers Behind It); pipe() returns two; eventfd, timerfd_create, signalfd, inotify_init, epoll_create1, memfd_create and pidfd_open all return descriptors. The point of that uniformity is that one waiting primitive — poll, epoll_wait (I/O Multiplexing: select, poll, epoll, kqueue, IOCP) — can watch a socket, a timer and a signal in a single call, and one close() releases any of them. Everything Is I/O takes this to its conclusion.
This is why a database connection is an OS-level resource and not a library concept. Your driver opens a TCP socket to 10.0.0.7:5432; that is slot 4 in your table, a socket object in the kernel with send and receive buffers, and one entry in the server’s table at the other end. A connection pool of 50 is 50 descriptors on your side and 50 on the database’s, plus whatever a proxy in between holds. Leaking connections leaks descriptors; exhausting descriptors makes new connections fail with EMFILE before the network is ever involved.
Windows is not built this way and it is worth being precise. Windows has HANDLEs to kernel objects — files, events, mutexes, processes, threads, pipes — managed in a per-process handle table, and Winsock SOCKETs, which are handles too but are serviced by a different subsystem. select on Windows accepts only sockets; a file handle and a socket cannot be waited on with the same readiness call, and WaitForMultipleObjects tops out at 64 objects. The equivalent scalable primitive is I/O completion ports, which are completion-based rather than readiness-based. The C runtime emulates _open/_read and the 0/1/2 numbering on top of handles, so a Windows fd is a library fiction over a HANDLE.
- One namespace (small integers), one
close, one waiting primitive — that is what "everything is a file" buys on Linux. - A database connection = a socket = a descriptor on each end. Pool size is a descriptor budget.
- Windows: HANDLE table plus Winsock SOCKETs, no fork, inheritance by explicit flag, IOCP instead of readiness polling. The concepts rhyme; the internals do not match.
Key points
- fd → per-process descriptor table → shared open file description (offset, status flags) → file object. Three levels, each with its own sharing rules.
dup2makes two slots share one description. Shell redirection isopen+dup2in the child beforeexec;2>&1isdup2(1, 2)and its position in the command line is its order of execution.forkcopies the table (shared offsets);execkeeps it unlessO_CLOEXEC. Set close-on-exec at creation to avoid races and leaks into children.EMFILEis the per-process limit (ulimit -n);ENFILEis the machine-wide one. Sockets, pipes, timers and epoll instances all count.- A database connection is a socket is a descriptor. Connection pools, HTTP client pools and accepted clients together must fit under the limit.
- Windows uses HANDLEs and Winsock SOCKETs with explicit-inheritance and completion ports; do not assume Unix descriptor semantics there.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why an integer instead of a pointer?
A small index into a kernel-owned table is a capability the process cannot forge or dereference: user code cannot reach kernel memory, and the kernel validates the index on every call. It is also trivially inheritable and serialisable — you can pass "3" to a child process or over a Unix socket.
▸Why does the offset live in a shared description rather than in the descriptor?
So that a parent and child writing to the same inherited log append after each other, and so dup2 redirection sees one consistent stream. If each descriptor had its own offset, cmd > log 2>&1 would have stdout and stderr overwriting each other from position 0.
▸Why do descriptors have a limit at all?
Each open description pins kernel memory and, for sockets, buffers; an unbounded table lets one leaking process consume the machine. The per-process limit is a blast-radius control; the system-wide one is the hard floor.
File descriptor table
How it fails
What the failure looks like from inside real software.
EMFILE: too many open filesin a network server that opens no files: sockets, pool connections and pipes to child processes exhausted the 1024 soft limit.- Server restart fails with
EADDRINUSEbecause a spawned helper inherited the listening socket withoutO_CLOEXECand is still running. 2>&1 > fileleaves errors on the terminal; the redirections were applied in the order written, so 2 was copied before 1 was moved.- A pipeline hangs forever: a process still holds the pipe’s write end (often an inherited copy), so the reader never gets EOF.
- First
open()in a daemon returns 0 because stdin was closed by the launcher; the program then reads its config from the terminal or writes binary data to it. - Descriptor count climbs linearly over days in
lsof -poutput — a leak on an error path — until the process dies at the limit.