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).

graph LR fd0["fd 0 (stdin)"] --> ft["per-process fd table"] fd1["fd 1 (stdout)"] --> ft fd2["fd 2 (stderr)"] --> ft fd3["fd 3 (socket)"] --> ft ft --> kft["kernel file table entry<br/>(position, flags)"] kft --> inode["inode / socket buffer"]

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.