JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Opening a File: Descriptors and the Open-File Table

A file just sits on disk until a program opens it. Meet the small integer the kernel hands back, the bookkeeping it sets up behind that number, and the moving cursor that decides where your next read lands.

From a name on disk to a thing you can use

In the previous guide we said a file is a named, durable stream of bytes plus its attributes — its size, its owner, its timestamps. But all of that just sits passively on the disk. A program cannot read a file by knowing its name any more than you can drink from a bottle by knowing the bottle's label. First you have to open it. Opening is the moment the operating system turns a static name into a live, working connection that your program can actually use.

Why bother with an open step at all — why not just say "read 100 bytes from /home/me/notes.txt" every single time? Because finding a file by name is slow work. The kernel must walk the directory structure, check that you are allowed in, and locate the file's on-disk record. Doing that on every read would be madness. So open() pays the lookup cost once, sets up some bookkeeping, and hands you back a cheap shortcut to use from then on. Every later read or write rides on that shortcut instead of re-doing the search.

The file descriptor: a number that means "that file, for you"

When open() succeeds, the kernel does not hand you the file itself, nor a chunk of its bytes. It hands you a small non-negative integer called a file descriptor — often 3, 4, 5, and so on. Think of it like a coat-check ticket. You give the cloakroom your heavy coat (the file, with all its messy on-disk detail) and walk away holding only a flimsy numbered stub. The stub is tiny and easy to carry, yet whenever you present it the attendant knows exactly which coat you mean. Your descriptor is that stub, and the kernel is the attendant.

Why start at 3? Because by long tradition every process begins life with three descriptors already open: 0 is standard input (usually the keyboard), 1 is standard output (usually the screen), and 2 is standard error. That is why printing to the screen and reading from the keyboard feel so effortless — those connections were handed to your program before its first line ran. The next file you open simply takes the lowest free number, which is 3.

Behind the number: the open-file table

So what does that small integer actually index into? Each process owns a private little array, the per-process descriptor table. Slot 3 holds a pointer to a richer record living in a system-wide open-file table shared by the whole kernel. That richer record is where the real state of an open file lives: which file it is, whether you opened it for reading or writing, and the position cursor we will meet next. The filename itself is not stored here at all — once the file is open, its name has already done its only job (helping find it) and is no longer needed.

Follow that record one level deeper and you reach the file's true on-disk identity — its inode, the compact record holding the file's attributes and the map of where its blocks physically sit. We will dig into inodes in the implementation rung; for now the key idea is the chain. Your fd is just the first link: descriptor number leads to an open-file-table entry, which leads to the inode, which leads to the actual bytes. The descriptor is the cheap handle at the near end of that whole chain.

process A                    process B
  fd table                     fd table
  +----+                       +----+
  | 0  |--> stdin              | 0  |--> stdin
  | 1  |--> stdout            | 1  |--> stdout
  | 2  |--> stderr            | 2  |--> stderr
  | 3  |---------+   +---------| 3  |
  +----+         |   |         +----+
                 v   v
      system-wide open-file table
      +---------------------------+
      | mode: read  | pos: 1024   |---> inode ---> [ disk blocks ]
      +---------------------------+
Two private descriptor tables point into one shared open-file table; each entry remembers the mode and position, then points on to the inode and finally the blocks on disk.

The file position: a cursor that quietly moves

Inside each open-file-table entry sits one of the most useful and most overlooked numbers in the system: the file position — sometimes called the offset or the cursor. It records how many bytes into the file your next read or write will begin. When you open a file for reading, it starts at 0, the very first byte. This is exactly the blinking cursor in a text editor, but for raw bytes: it marks where the action will happen.

The clever part is that the position advances itself. A call like read(fd, buf, n) does two things: it copies up to n bytes into your buffer, and it pushes the cursor forward by exactly how many bytes it delivered. So you do not have to track where you are — the kernel does. Call read again and you seamlessly get the next chunk. Let us trace it on a 4000-byte file.

  1. open("data.bin") returns fd 3; its open-file-table entry sets the position to 0.
  2. read(3, buf, 1000) copies bytes 0 through 999 into buf and advances the position to 1000.
  3. read(3, buf, 1000) again copies bytes 1000 through 1999 — it picks up exactly where it left off — and the position becomes 2000. You never told it where to start; it remembered.
  4. To jump around instead of reading straight through, call lseek(3, 0, start) to slam the cursor back to 0, or lseek to any offset you like; the very next read begins there.

Here is a subtle but important point. Because the position lives in the shared open-file-table entry and not in your private descriptor slot, two descriptors that point to the same entry share one cursor — read through one and the other sees the position move too. But if two processes each open the same file separately, they get two different entries and therefore two independent cursors, each reading the file at its own pace without disturbing the other.

Reading sequentially, or jumping straight there

The fact that you can either let the cursor crawl forward or fling it anywhere is the heart of the file's access method — the pattern by which a program moves through a file's bytes. The most common pattern is sequential access: start at the beginning and read straight to the end, the cursor advancing on its own. This is how you read a log file, play a song, or stream a video — front to back, like reading a novel page by page.

The other pattern is direct (or random) access: jump straight to any byte you want, in any order, by setting the position with lseek first. This is how a database leaps to record number 5000 without reading the 4999 before it, or how a media player skips to the middle of a movie. The OS supports both atop the very same machinery — a single movable cursor. Sequential is just the special case where you happen never to jump.

When your program is finished, it calls close(fd). This is not just tidiness. Close tears down the open-file-table entry (if no one else is sharing it), frees the descriptor number so a future open() can reuse it, and importantly flushes any buffered writes safely to disk. Forgetting to close leaks descriptors — a process is allowed only a limited number of them, and a long-running program that opens without closing will eventually run out and fail. Open, use, close: the whole life of a working connection to a file.