Skip to main content

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:

fdnametypical meaning
0stdinkeyboard, or redirected input
1stdoutterminal, or redirected output
2stderrterminal (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:

  1. What does each fd in this function point to? (Read it from open, pipe, socket, accept, or the environment.)
  2. Who closes it, and when? An fd that is not closed is leaked; ulimit -n is the cap.
  3. Does this fd need O_CLOEXEC so a child exec'd later does not inherit it?
  4. Does it need O_NONBLOCK so read/write return EAGAIN instead of blocking?

Check Yourself

  1. What are fds 0, 1, and 2 by convention?
  2. Give two different kinds of thing that can be behind the same read(fd, ...) call.
  3. Why does mixing printf(stdout, ...) with write(1, ...) produce garbled output?

Mini Drill or Application

Do all four:

  1. Run the whatfd program above redirected to a file, a pipe, and the terminal. Capture the output.
  2. Modify it to report fd 0 instead. Run with ./whatfd < /dev/null and with a here-doc (./whatfd <<< hello).
  3. Write a 10-line program that opens /etc/hostname, prints the fd number, then closes it. Repeat in a loop 1,024 times. Explain why it does not run out of fds.
  4. In one sentence, explain how "everything is a file" simplifies writing a generic copy utility.

Read This Only If Stuck