File Descriptors¶
What is it: a non-negative integer indexing into the per-process open file table. Every I/O resource is a file descriptor: regular files, sockets, pipes, devices.
Why "everything is a file": uniform interface (read/write/close) across all I/O resources. ls | sort is two processes connected by a pipe fd. The same read() syscall works on disk files, TCP sockets, and /dev/random.
Standard fds: 0=stdin, 1=stdout, 2=stderr. A process closing fd 1 and opening a file gets fd 1 — the file is now stdout (shell redirection works this way).
fd table mechanics: kernel maps integer → file table entry (file position + flags + inode reference). dup() creates two fds sharing one file table entry (shared position). Fork duplicates the fd table — parent and child share the same underlying entries.
Limits: RLIMIT_NOFILE (default 1024 soft). Raise for high-connection servers: ulimit -n 65536. Each fd: ~300 bytes kernel memory.
fd leak: opening file/socket without closing → fd count grows → hits limit → all open/socket/accept fail with EMFILE. In Go: defer resp.Body.Close() must always be called or the TCP connection fd is leaked. Detection: ls /proc/$PID/fd | wc -l.
Real-world usage: Proxel had fd leaks in proxy connection tests due to missing defer conn.Close() in error paths. Caught by a fd-count Prometheus metric that grew monotonically.