Every time your program talks to a file, there’s a number doing the heavy lifting. Let’s see what it really means.
Introduction
You know that moment when you write open(“myfile.txt”, O_RDONLY) and get back some random integer like 3 or 7? That number isn’t random at all. It’s your golden ticket to everything that happens with that file from that point forward. Every read(), every write(), every close() you do depends on that little integer.
File descriptors are one of those things that seem simple on the surface (it’s just a number, right?) but hide layers of complexity underneath. Understanding them changes how you think about I/O in Linux. When Dennis Ritchie and Ken Thompson designed Unix, they made a choice: abstract everything as files, and give processes simple integer handles to work with them. Brilliant in its simplicity.
We’re going to check open what’s really happening when you get that integer back. Next up, we’ll write some actual code to see these descriptors in action.
What Exactly Is a File Descriptor?
Think of a file descriptor as a ticket stub you get at a coat check. You hand over your coat (open a file), they give you a numbered stub (the file descriptor), and whenever you want your coat back (read from the file), you show that stub. The person behind the counter (the kernel) looks up your number and knows exactly which coat is yours.
![]()
Generated Using Grok
That’s it. File descriptors are integers, nothing more. But they’re integers with meaning. When you call open(), the kernel creates all sorts of internal bookkeeping structures to manage that file, then hands you back the simplest possible reference: a number.
Here’s a tiny program that opens a file and shows you the descriptor:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("test.txt", O_RDONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open failed");
return 1;
}
printf("Got file descriptor: %d\n", fd);
close(fd);
return 0;
}Compile and run this:
gcc -o fdtest main.c
./fdtestYou’ll probably see “Got file descriptor: 3”. Why 3? Well, that brings us to something important.
The Three Amigos: stdin, stdout, stderr
Every process starts life with three file descriptors already open. Always. No exceptions.
- 0 = stdin (standard input, usually your keyboard)
- 1 = stdout (standard output, usually your terminal)
- 2 = stderr (standard error, also usually your terminal)
These exist before your code even runs. When you call printf(), it’s writing to file descriptor 1. When you call scanf(), it’s reading from file descriptor 0. When your program crashes and spits out an error, that goes to file descriptor 2.
So when you open your first file, the kernel looks at your process’s file descriptor table and says “0, 1, and 2 are taken. Here, have 3.” Open another file? You get 4. And so on.
The Per-Process File Descriptor Table
Every running process gets its own file descriptor table. This is just an array, sitting in kernel memory, indexed by those integers we keep talking about. You can think of it like this:
Index (FD) Points To
---------- ---------
0 [stdin entry]
1 [stdout entry]
2 [stderr entry]
3 [your file]
4 [another file]The table has a maximum size. You can check yours:
ulimit -nOn my machine, that returns 1048575. That’s 2^20, which means I could theoretically have over a million files open at once in a single process. (Don’t try this at home. Or do, if you want to watch your system grind to a halt.)
The kernel uses this table as its first stop when you make any I/O system call. You say read(fd, buffer, size), and the kernel thinks “okay, what’s at index fd in this process’s table?” Whatever it finds there tells it where to actually read the data from.
Here’s a visual of how this looks:
Process File Descriptor Table
Why This Matters
You might be thinking, “Okay, so it’s a number in a table. So what?” Fair question. Here’s why it matters: this simple abstraction hides mountains of complexity. The kernel manages permissions, buffering, disk I/O, network protocols, all of it. You just get a number and a handful of system calls. That’s the Unix philosophy in action.
Also, understanding this table is the foundation for everything that comes next. Want to redirect output? You’re manipulating this table. Want to create pipes between processes? You’re manipulating this table. Want to understand why forking processes behave weirdly with open files? Yep, it’s because of how this table gets copied (or doesn’t).
Wrapping It Up
File descriptors are deceptively simple. They’re just integers that let your process talk to files, devices, sockets, and more. Every process gets its own table of these descriptors, and the kernel uses that table as the starting point for all I/O operations. We’ve got our three defaults (stdin, stdout, stderr), and everything else gets numbered sequentially from there.
Try running that sample code. Open a few files, print their descriptors, see what numbers you get. Get a feel for it. Next part, we’re going deeper to see what’s hiding under the hood: the kernel structures that make all this actually work.