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

Directories, Paths, and Mounting

Guide 2 left you holding a file descriptor — but how did open() ever find the file in the first place? This guide follows a path name through the directory tree component by component, shows how sharing reshaped that tree, and reveals the quiet trick that splices every disk and USB stick into one seamless tree: mounting.

A directory is just a special file

By the end of guide 2 you could open a file, read it, and hold an file descriptor that indexed into the open-file table. But we skipped over a small miracle: how did the call "open(\"/home/jo/notes.txt\")" turn that string of characters into the right file on a disk full of millions of files? The answer is the directory — and the first thing to unlearn is the idea that a directory is some magical container that holds files inside it. It does not. A directory is itself just a file; it is special only in what it contains. Where an ordinary file holds your text or pixels, a directory holds a little table that maps human-readable names to the on-disk identity of each entry.

What is that "on-disk identity"? On most Unix-style systems it is a number called an inode number — think of it as the file's true serial number, the thing that actually owns the file's bytes and its attributes (size, owner, timestamps, permission bits). A directory entry is therefore astonishingly thin: it is little more than the pair (name, inode number). The name "notes.txt" lives in the directory; everything else about the file lives in the inode the directory points at. Keep this split in mind, because it is the single idea that makes guide 4's hard links and symbolic links suddenly obvious instead of mysterious.

From one flat list to a tree — and then a graph

The earliest systems had a single directory for the whole machine — one flat list of every file. That works until two users both want a file called "report", or until you have ten thousand files and naming them all uniquely becomes a nightmare. The fix was to let a directory entry point at another directory, so directories could nest. That gives the tree-structured directory you know in your bones: a single root at the top (written "/" on Unix, or "C:\\" on a Windows volume), branching into subdirectories, branching again, with files as the leaves. The tree structure is the directory structure almost every system uses, and its great virtue is that each file has exactly one place and one full name.

But a pure tree has a limit. Suppose two programmers want to genuinely share one source file — not two copies that drift apart, but the one true file, so a change by either is seen by both. A tree forbids this, because in a tree every node has exactly one parent and one name. The next step in the evolution is to relax that rule into an acyclic-graph directory: allow a file (or even a whole directory) to be reachable by more than one path. Now "the" file can sit under both /home/jo/project/main.c and /home/sam/shared/main.c and be literally the same bytes. This is the structural reason file sharing is possible at all, and it is the doorway into guide 4: a hard link is precisely a second name in the graph pointing at the same inode.

The word "acyclic" is doing honest work and is worth a beat. A graph that allows multiple paths could, if you are careless, contain a cycle — directory A links to B and B links back to A. Cycles are poison: a program walking the tree to compute a folder's size or to delete it recursively would loop forever, and reference-counting an inode (how many names point at me?) breaks when a structure can point at itself. So systems work hard to keep the directory graph acyclic, which is one big reason hard links to directories are usually forbidden, while hard links to ordinary files are fine. We are being honest here because the textbook diagrams rarely tell you why the rules exist.

Walking a path, component by component

Now we can answer the question we dodged: how does open() find a file from a string? A path name is a route through the directory graph, written as a sequence of names separated by slashes. There are two flavours. An absolute path starts at the root, like /home/jo/notes.txt — it gives complete directions from the top of the tree, so it means the same thing no matter where you are standing. A relative path, like project/main.c, is read starting from your current working directory — the folder your process is "in" right now, which the kernel remembers per process. Relative paths are shorthand, the way "two doors down" only makes sense once someone knows which door you are standing at.

The kernel resolves a path by a patient loop called path-name resolution or walking. It is not one lookup; it is one lookup per component. To resolve /home/jo/notes.txt the kernel starts at the root inode, reads the root directory's table to find the entry named "home" and its inode, reads that inode (it had better be a directory), reads its table to find "jo", and so on down the chain. Each "/" is a step deeper, each step is a directory read plus a name lookup, and the walk fails the instant a component is missing or you lack permission to enter a directory along the way. This is the source of a few honest costs and the reason deep paths can be slightly slower to open.

resolve  /home/jo/notes.txt   (an absolute path)

  start: root inode "/"
   |  read root's table -> "home" => inode 2
   v
  inode 2  (dir)
   |  read its table -> "jo"   => inode 9
   v
  inode 9  (dir)
   |  read its table -> "notes.txt" => inode 47
   v
  inode 47 (regular file)  ==>  open it, hand back an fd

  one component  =  one directory read + one name lookup
  a missing name OR no 'x' permission on a dir  =>  walk fails
Resolving a path is a loop, not a single step: one directory read and one name lookup per slash-separated component, ending at the file's inode.

Mounting: stitching many disks into one tree

Here is something you use constantly without noticing. Your machine has more than one storage device — a main drive, maybe a second SSD, a USB stick, a network share — yet on Unix you do not see "drive 1" and "drive 2". You see one single tree starting at "/", and the USB stick simply appears as a folder, say /media/usb. How can two physically separate disks, each with its own root directory, live inside one tree? The trick is mounting, and it is one of the most elegant ideas in the whole file-system interface.

Mounting says: take the root of a second file system and graft it onto an empty directory in the tree you already have. That chosen directory is the mount point. After you mount the USB stick at /media/usb, the rule changes for path resolution: whenever the walk reaches /media/usb, the kernel quietly redirects from the original (empty) directory to the root of the USB stick's file system and keeps walking there. Think of it like grafting a branch from one tree onto another — once spliced, you cannot tell from the outside where one tree ends and the next begins; it is all just one continuous trunk of branches. The seam is invisible by design.

  1. Pick a mount point: an existing directory in the current tree, e.g. /media/usb. By convention it is empty, because mounting hides whatever was there until you unmount.
  2. Read the new file system's superblock — the small header on the device that says "I am an ext4 file system, my root inode is here, my block size is this." The kernel uses it to understand the disk's layout.
  3. Record the splice in an in-memory mount table: "the directory at inode X is now covered; resolution that reaches it should jump to root inode Y on device Z."
  4. From now on, every path that crosses the mount point is silently redirected. Userspace sees one tree; the kernel knows it spans several devices.

Why this layout is a triumph (and where it leaks)

Step back and admire what the directory-and-mount design buys you. A program can open "/etc/config" without knowing or caring whether that lives on the boot SSD, a network share, or a RAM disk — the path is the only address it needs, and the kernel handles the rest behind the mount table. This is the same generosity you saw with file sharing and the file abstraction itself in guide 1: a clean name on top, messy hardware underneath. It is also why the same path can mean different physical things on different machines, which is a feature for portability and a trap if you assume a path is universal.

Now the honest corners, because the seamless tree hides real seams. First, you genuinely cannot make a hard link across a mount point: a hard link is just a second name pointing at an inode number, but inode numbers are only unique within one file system, so a name on disk A cannot point at an inode on disk B. (This is precisely a gap that symbolic links fill, since a symlink stores a path string, not an inode — guide 4 leans hard on this distinction.) Second, the redirection has a cost: crossing a mount point during a walk is a little extra work, and walking a long path is many directory reads, which is why systems cache resolved path components aggressively.

One more honest point that links forward to guide 5. Walking a path is not just a data lookup; it is a series of permission checks. To pass through a directory on the way to a file, your process needs the directory's execute ("x") bit — even just to traverse it, not to list it. So a file can be perfectly readable yet utterly unreachable because some ancestor directory denies you entry, like a building where your key works on the office door but not on the lobby. We will unpack the permission bits and access-control lists fully in guide 5; for now, just hold onto the surprise that reaching a file and reading a file are two separate permissions, decided at two different points in the walk.